本文详解如何在Express与Webpack全栈项目中实现智能自动刷新。通过配置webpack-dev-middleware、webpack-hot-middleware以及reload和supervisor,分别处理前端CSS/JS的热更新与后端服务器重启后的页面刷新,提升开发效率。
引言:自动刷新的开发体验
此前在《自动刷新:从BrowserSync开始》一文中,我介绍了BrowserSync这一优秀的开发工具。借助它,我深刻体会到一种高效的理念:如果在按下Ctrl+S保存代码后,页面能自动刷新并立即展示最新效果,那将是极佳的开发体验。
如今,Webpack已成为最流行的模块打包工具(module bundler)。它不仅为前端静态资源的组织与管理提供了相对完善的解决方案,也在很大程度上重塑了前端开发的工作流程。在采用Webpack的开发流程中,若想继续享受“自动刷新”带来的便捷,往往需要进行额外的配置与调整。
Webpack与自动刷新
本文旨在探讨如何结合Webpack实现自动刷新,而非介绍Webpack的基础知识。Webpack要求静态资源在访问前必须经过编译,即执行一次webpack命令。因此,自动刷新的触发时机需要调整:修改CSS等源码并保存后,应先触发Webpack编译,待编译完成后,再通知浏览器进行刷新。
开发Express项目时遇到的问题
假设有一个应用了Webpack的Express项目,其目录结构如下:

其中,client目录存放前端静态资源,如CSS、图片以及浏览器端使用的JavaScript;server目录包含后端文件,例如Express的路由(routes)、视图(views)以及其他Node.js执行的JavaScript代码。根目录下的app.js则是启动Express服务的入口文件。
在开发过程中,我们通常会怎么做呢?
通常的做法是先启动Express服务器,然后在浏览器中打开目标页面,接着编辑源文件。然而,问题随之而来:例如,当我修改.scss源文件时,即使只改动了一小部分,我也必须在命令行中输入webpack命令等待编译完成,然后切换到浏览器按下F5刷新,才能看到修改后的效果。
再比如,如果我修改了routes目录下的.js文件想要查看结果,我需要到命令行中重启Express服务器,同样切换到浏览器按下F5刷新。
这实在太过繁琐。
因此,我们需要让开发过程变得更加愉快。
改进目标
我们期望的Express与Webpack项目的开发流程如下:
- 如果修改的是client目录下的css文件(包括.scss等),保存后,浏览器不会整页刷新,而是直接将新的样式效果更新到页面中。
- 如果修改的是client目录下的javascript文件,保存后,浏览器会自动进行整页刷新,以获取更新后的效果。
- 如果修改的是server目录下的文件,保存后,服务器将自动重启,浏览器会在服务器重启完成后自动刷新。
经过多次尝试,我最终得到了一个实现了上述目标的项目配置。接下来,本文将详细说明该配置的实现过程。
前端自动刷新:Webpack中间件配置
从webpack-dev-server开始
首先,Webpack已经考虑到了开发流程中的自动刷新需求,这就是webpack-dev-server。它是一个仅用于开发环境的静态资源服务器。
一般来说,对于纯前端项目(全部由静态html文件组成),只需在项目根目录运行webpack-dev-server,然后打开html文件。修改任意关联的源文件并保存后,Webpack会执行编译,并在编译完成后通知浏览器刷新。
与直接在命令行中运行webpack不同,webpack-dev-server会将编译后的静态文件全部保存在内存中,而不会写入到文件目录内。这样,每次都在变化的webpack输出目录消失了,是否感觉更清爽呢?
如果在请求某个静态资源时,Webpack编译尚未完成,webpack-dev-server不会让该请求失败,而是会一直阻塞它,直到编译结束。这意味着,如果在不恰当的时机刷新页面,不会看到错误,而是会等待一段时间后重新看到正常页面,感觉就像“网速很慢”。
webpack-dev-server的功能看起来正是我们所需要的,但如何将其加入到包含后端服务器的Express项目中呢?
webpack-dev-middleware和webpack-hot-middleware
Express本质上是一系列中间件(middleware)的集合,因此,适合Express的Webpack开发工具是webpack-dev-middleware和webpack-hot-middleware。
webpack-dev-middleware是一个处理静态资源的中间件。前面提到的webpack-dev-server,实际上是一个小型的Express服务器,它也是使用webpack-dev-middleware来处理Webpack编译后的输出。
webpack-hot-middleware是一个与webpack-dev-middleware配合使用的中间件,它可以实现浏览器的无刷新更新(hot reload)。这也是Webpack文档中常说的HMR(Hot Module Replacement,热模块替换)。
参考webpack-hot-middleware的文档和示例,我们将这两个中间件添加到Express中。
Webpack配置文件部分
首先,修改Webpack的配置文件(为了方便查看,这里列出了webpack.config.js的全部代码):
var webpack = require('webpack');
var path = require('path');
var publicPath = 'http://localhost:3000/';
var hotMiddlewareScript = 'webpack-hot-middleware/client?reload=true';
var devConfig = {
entry: {
page1: ['./client/page1', hotMiddlewareScript],
page2: ['./client/page2', hotMiddlewareScript]
},
output: {
filename: './[name]/bundle.js',
path: path.resolve('./public'),
publicPath: publicPath
},
devtool: 'source-map',
module: {
loaders: [{
test: /\.(png|jpg)$/,
loader: 'url?limit=8192&context=client&name=[path][name].[ext]'
}, {
test: /\.scss$/,
loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'
}]
},
plugins: [
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
};
module.exports = devConfig;
这是一个包含多个entry的较复杂示例。其中与webpack-hot-middleware相关的有两处:一是plugins部分,增加了3个插件;二是entry部分,在每个entry后都增加了一个hotMiddlewareScript。
hotMiddlewareScript的值是webpack-hot-middleware/client?reload=true,其中?后的内容相当于为webpack-hot-middleware设置参数,这里reload=true的意思是,如果遇到无法进行hot reload的情况,则进行整页刷新。
在该配置文件中,还有一个要点是publicPath不是/这样的值,而是http://localhost:3000/这样的绝对地址。这是因为,在使用?sourceMap时,style-loader会将css的引入做成如下形式:

这种blob的形式可能会导致css中url()引用的图片失效,因此建议使用带http的绝对地址(这也仅在开发环境中使用)。有关此问题的详情,您可以查看github上的相关issue。
Express启动文件部分
接下来,在Express启动文件中添加以下代码:
var webpack = require('webpack'),
webpackDevMiddleware = require('webpack-dev-middleware'),
webpackHotMiddleware = require('webpack-hot-middleware'),
webpackDevConfig = require('./webpack.config.js');
var compiler = webpack(webpackDevConfig);
// attach to the compiler & the server
app.use(webpackDevMiddleware(compiler, {
// public path should be the same with webpack config
publicPath: webpackDevConfig.output.publicPath,
noInfo: true,
stats: {
colors: true
}
}));
app.use(webpackHotMiddleware(compiler));
以上这段代码应位于Express的routes代码之前。其中,webpack-dev-middleware配置的publicPath应与webpack配置文件中的保持一致。
webpack-dev-middleware和webpack-hot-middleware的静态资源服务仅用于开发环境。到了线上环境,应使用express.static()。
至此,client部分的目标已完成。现在打开网页控制台,应该可以看到[HMR] connected的提示。在这个项目中,我只要求css使用HMR,如果您希望javascript也使用HMR,一个简单的做法是在entry文件内添加以下代码:
if(module.hot) {
module.hot.accept();
}
这样,与该entry相关的所有.js文件都会使用hot reload的形式。关于这一点的更多详情,请参考hot module replacement。
接下来是server部分。
后端自动刷新:服务器重启与浏览器联动
reload和supervisor
server部分的自动刷新面临一个问题:自动刷新的消息通知依赖于浏览器和服务器之间的websocket连接,但在server部分修改代码时,通常需要重启服务器以使变更生效(例如修改routes),这会断开websocket连接。
因此,需要一个变通策略:在浏览器端增加对websocket断开的处理,如果websocket断开,则开启一个稍长于服务器重启时间的定时任务(setTimeout),相当于等到服务器重启完毕后,再进行整页刷新。
reload是一个应用此策略的组件,它可以帮我们处理服务器重启时的浏览器刷新。
现在,还差一个监听server文件、如果有变更就重启服务器的组件。参考reload的推荐,我们选用supervisor。
下面将reload和supervisor引入到Express项目内。
监听文件以重启服务器
通过以下代码安装supervisor(是的,必须使用-g参数):
npm install supervisor -g
然后,在package.json里设置新的scripts:
"scripts": {
"start": "cross-env NODE_ENV=dev supervisor -i client app"
}
这里的主要变化是从node app改为supervisor -i client app。其中-i等于--ignore,这里表示忽略client目录,显然,我们不希望在修改前端代码时服务器也重启。
这里的cross-env也是一个npm组件,它可以处理Windows和其他Unix系统在设置环境变量写法上不一致的问题。
把会重启的服务器和浏览器关联起来
将Express启动文件最后的部分做如下修改:
var reload = require('reload');
var http = require('http');
var server = http.createServer(app);
reload(server, app);
server.listen(3000, function(){
console.log('App (dev) is now running on port 3000!');
});
Express启动文件的最后通常是app.listen()。参照reload的说明,需要用http再增加一层服务。
然后,再到Express的视图文件views里,在底部增加一个<script>:
<% if (env !== "production") { %>
<script src="/reload/reload.js"></script>
<% } %>
所有的views都需要这样一段代码,因此最好借助模板引擎,使用include或extends的方式添加到公共位置。
这里的reload.js和前面webpack的开发环境bundle.js并不冲突,它们一个负责前端源文件变更后进行编译和刷新,另一个负责在服务器发生重启时触发延时刷新。
至此,server部分也完成了。现在,修改项目内的任意源文件,按下Ctrl+S,浏览器里的页面都会对应地做一次“适当”的刷新。
完整示例
完整示例已经提交到github:express-webpack-full-live-reload-example
效果如下:

可选方案:针对视图文件的快速刷新
附加的可选方案
前面提到的server部分,分为views和routes,如果只修改views,那么服务器并不需要重启,直接刷新浏览器就可以了。
针对这样的开发情景,可以把views文件的修改刷新变得更快。这时候我们不用reload和supervisor,改为使用browsersync,在Express的启动文件内做如下修改:
var bs = require('browser-sync').create();
app.listen(3000, function(){
bs.init({
open: false,
ui: false,
notify: false,
proxy: 'localhost:3000',
files: ['./server/views/**'],
port: 8080
});
console.log('App (dev) is going to be running on port 8080 (by browsersync).');
});
然后,使用browsersync提供的新的访问地址就可以了。这样,修改views(html)的时候,由browsersync帮忙直接刷新,修改css和javascript的时候继续由webpack的middleware来执行编译和刷新。
结语
有了Webpack后,没有自动刷新怎么干活?
说起来,能做出像这样的全栈刷新,大概也是得益于Express和Webpack都是javascript,可以很容易地结合、协作的缘故吧。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。