在之前的一篇文章BrowserSync,迅捷从免F5开始中,我介绍了BrowserSync这样一个出色的开发工具。经过BrowserSync我感觉到了这样一个理念:若是在一次ctrl + s
保存后能够自动刷新,而后当即看到新的页面效果,那会是很棒的开发体验。javascript
如今,webpack能够说是最流行的模块加载器(module bundler)。一方面,它为前端静态资源的组织和管理提供了相对较完善的解决方案,另外一方面,它也很大程度上改变了前端开发的工做流程。在应用了webpack的开发流程中,想要继续“自动刷新”的爽快体验,就可能得额外作一些事情。css
本文并不打算介绍webpack,若是你还不清楚它是什么,推荐阅读下面几篇入门文章:html
webpack要求静态资源在被真正拿来访问以前,都要先完成一次编译,即运行完成一次webpack
命令。所以,自动刷新须要调整到适当的时间点。也就是说,修改了css等源码并保存后,应该先触发一次webpack编译,在编译完成后,再通知浏览器去刷新。react
如今有这样的一个应用了webpack的Express项目,目录结构以下:webpack
其中,client
内是前端的静态资源文件,好比css、图片以及浏览器内使用的javascript。server
内是后端的文件,好比express的routes、views以及其余用node执行的javascript。根目录的app.js
,就是启动express的入口文件了。git
开发的时候咱们会怎样作呢?github
先启动Express服务器,而后在浏览器中打开某个页面,接下来再编辑源文件。那么,问题就来了,好比我编辑.scss
源文件,即便我只改了一小点,我也得在命令行里输入webpack
等它编译完,而后再切到浏览器里按一下F5,才能看到修改后的效果。
再好比,我修改了routes
里的.js
文件想看看结果,我须要到命令行里重启一次Express服务器,而后一样切到浏览器里按一下F5。
这可真是太费事了。
因此,咱们要让开发过程愉快起来。
咱们但愿的Express&Webpack项目的开发过程是:
若是修改的是client
里的css文件(包括.scss
等),保存后,浏览器不会整页刷新,新的样式效果直接更新到页面内。
若是修改的是client
里的javascript文件,保存后,浏览器会自动整页刷新,获得更新后的效果。
若是修改的是server
里的文件,保存后,服务器将自动重启,浏览器会在服务器重启完毕后自动刷新。
通过屡次尝试,我最终获得了一个实现了以上这些目标的项目配置。接下来,本文将说明这个配置是如何作出来的。
首先,webpack已经想到了开发流程中的自动刷新,这就是webpack-dev-server。它是一个静态资源服务器,只用于开发环境。
通常来讲,对于纯前端的项目(所有由静态html文件组成),简单地在项目根目录运行webpack-dev-server,而后打开html,修改任意关联的源文件并保存,webpack编译就会运行,并在运行完成后通知浏览器刷新。
和直接在命令行里运行webpack
不一样的是,webpack-dev-server会把编译后的静态文件所有保存在内存里,而不会写入到文件目录内。这样,少了那个每次都在变的webpack输出目录,会不会以为更清爽呢?
若是在请求某个静态资源的时候,webpack编译尚未运行完毕,webpack-dev-server不会让这个请求失败,而是会一直阻塞它,直到webpack编译完毕。这个对应的效果是,若是你在不恰当的时候刷新了页面,不会看到错误,而是会在等待一段时间后从新看到正常的页面,就好像“网速很慢”。
webpack-dev-server的功能看上去就是咱们须要的,但如何把它加入到包含后端服务器的Express项目里呢?
Express本质是一系列middleware的集合,所以,适合Express的webpack开发工具是webpack-dev-middleware和webpack-hot-middleware。
webpack-dev-middleware是一个处理静态资源的middleware。前面说的webpack-dev-server,其实是一个小型Express服务器,它也是用webpack-dev-middleware来处理webpack编译后的输出。
webpack-hot-middleware是一个结合webpack-dev-middleware使用的middleware,它能够实现浏览器的无刷新更新(hot reload)。这也是webpack文档里常说的HMR(Hot Module Replacement)。
参考webpack-hot-middleware的文档和示例,咱们把这2个middleware添加到Express中。
首先,修改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启动文件内添加如下代码:
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
部分。
server
部分的自动刷新,会面临一个问题:自动刷新的消息通知依靠的是浏览器和服务器之间的web socket链接,但在server
部分修改代码的话,通常都要重启服务器来使变动生效(好比修改routes
),这就会断开web socket链接。
因此,这须要一个变通的策略:浏览器这边增长一个对web socket断开的处理,若是web socket断开,则开启一个稍长于服务器重启时间的定时任务(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,能够很容易地结合、协做的缘故吧。
(从新编辑自个人博客,原文地址:http://acgtofe.com/posts/2016...)