视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
Express结合Webpack的全栈自动刷新
2020-11-27 21:56:13 责编:小采
文档

在以前的一篇文章自动刷新 从BrowserSync开始中,我介绍了BrowserSync这样一个出色的开发工具。通过BrowserSync我感受到了这样一个理念:如果在一次ctrl + s保存后可以自动刷新,然后立即看到新的页面效果,那会是很棒的开发体验。

现在,webpack可以说是最流行的模块加载器(module bundler)。一方面,它为前端静态资源的组织和管理提供了相对较完善的解决方案,另一方面,它也很大程度上改变了前端开发的工作流程。在应用了webpack的开发流程中,想要继续“自动刷新”的爽快体验,就可能得额外做一些事情。

webpack与自动刷新

本文并不打算介绍webpack,webpack要求静态资源在被真正拿来访问之前,都要先完成一次编译,即运行完成一次webpack命令。因此,自动刷新需要调整到适当的时间点。也就是说,修改了css等源码并保存后,应该先触发一次webpack编译,在编译完成后,再通知浏览器去刷新。

开发Express项目的问题

现在有这样的一个应用了webpack的Express项目,目录结构如下:

 

其中,client内是前端的静态资源文件,比如css、图片以及浏览器内使用的javascript。server内是后端的文件,比如express的routes、views以及其他用node执行的javascript。根目录的app.js,就是启动express的入口文件了。

开发的时候我们会怎样做呢?

先启动Express服务器,然后在浏览器中打开某个页面,接下来再编辑源文件。那么,问题就来了,比如我编辑.scss源文件,即使我只改了一小点,我也得在命令行里输入webpack等它编译完,然后再切到浏览器里按一下F5,才能看到修改后的效果。

再比如,我修改了routes里的.js文件想看看结果,我需要到命令行里重启一次Express服务器,然后同样切到浏览器里按一下F5。

这可真是太费事了。

所以,我们要让开发过程愉快起来。

改进目标

我们希望的Express&Webpack项目的开发过程是:

  • 如果修改的是client里的css文件(包括.scss等),保存后,浏览器不会整页刷新,新的样式效果直接更新到页面内。
  • 如果修改的是client里的javascript文件,保存后,浏览器会自动整页刷新,得到更新后的效果。
  • 如果修改的是server里的文件,保存后,服务器将自动重启,浏览器会在服务器重启完毕后自动刷新。
  • 经过多次尝试,我最终得到了一个实现了以上这些目标的项目配置。接下来,本文将说明这个配置是如何做出来的。

    从webpack-dev-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项目里呢?

    webpack-dev-middleware和webpack-hot-middleware

    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的配置文件(为了方便查看,这里贴出了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部分的自动刷新,会面临一个问题:自动刷新的消息通知依靠的是浏览器和服务器之间的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="https://www.gxlcms.com/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,可以很容易地结合、协作的缘故吧。

    下载本文
    显示全文
    专题