Express结合Webpack的全栈自动刷新


Posted in Javascript onMay 23, 2019

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

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

webpack与自动刷新

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

开发Express项目的问题

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

 Express结合Webpack的全栈自动刷新

其中,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的引入做成这样:

Express结合Webpack的全栈自动刷新

这种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="/reload/reload.js"></script>
<% } %>

所有的views都需要这样一段代码,因此最好借助模板引擎用include或extends的方式添加到公共位置。

这里的reload.js和前面webpack的开发环境bundle.js并不冲突,它们一个负责前端源文件变更后进行编译和刷新,另一个负责在服务器发生重启时触发延时刷新。

到此,server也完成了。现在,修改项目内的任意源文件,按下ctrl + s,浏览器里的页面都会对应地做一次“适当”的刷新。

完整示例

完整示例已经提交到github:express-webpack-full-live-reload-example

效果如下:

Express结合Webpack的全栈自动刷新

附加的可选方案

前面说的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,可以很容易地结合、协作的缘故吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript获取XML数据附示例截图
Mar 05 Javascript
JavaScript ParseFloat()方法
Dec 18 Javascript
分享jQuery插件的学习笔记
Jan 14 Javascript
原生JavaScript实现Ajax的方法
Apr 07 Javascript
Servlet实现文件上传,可多文件上传示例
Dec 05 Javascript
用纯Node.JS弹出Windows系统消息提示框实例(MessageBox)
May 17 Javascript
JSON 数据格式详解
Sep 13 Javascript
微信小程序实现美团菜单
Jun 06 Javascript
Vue-cli3多页面配置详解
Mar 22 Javascript
Vue项目前后端联调(使用proxyTable实现跨域方式)
Jul 18 Javascript
微信小程序通过websocket实时语音识别的实现代码
Aug 19 Javascript
微信小程序中wxs文件的一些妙用分享
Feb 18 Javascript
ajax跨域访问遇到的问题及解决方案
May 23 #Javascript
简单了解JavaScript异步
May 23 #Javascript
vue项目添加多页面配置的步骤详解
May 22 #Javascript
vue elementUI table 自定义表头和行合并的实例代码
May 22 #Javascript
微信小程序使用websocket通讯的demo,含前后端代码,亲测可用
May 22 #Javascript
微信小程序登录态和检验注册过没的app.js写法
May 22 #Javascript
element-ui上传一张图片后隐藏上传按钮功能
May 22 #Javascript
You might like
我用php+mysql写的留言本
2006/10/09 PHP
php数组去重的函数代码
2013/02/03 PHP
自编函数解决pathinfo()函数处理中文问题
2014/11/03 PHP
Thinkphp5.0 框架Model模型简单用法分析
2019/10/11 PHP
用JS实现的一个include函数
2007/07/21 Javascript
javascript 用记忆函数快速计算递归函数
2010/03/15 Javascript
Javascript 设计模式(二) 闭包
2010/05/26 Javascript
jQuery实现 注册时选择阅读条款 左右移动
2013/04/11 Javascript
JS在IE下缺少标识符的错误
2014/07/23 Javascript
js实现从右向左缓缓浮出网页浮动层广告的方法
2015/05/09 Javascript
javascript实现选中复选框后相关输入框变灰不可用的方法
2015/08/11 Javascript
JS实现自动固定顶部的悬浮菜单栏效果
2015/09/16 Javascript
AngularJS使用拦截器实现的loading功能完整实例
2017/05/17 Javascript
webpack学习--webpack经典7分钟入门教程
2017/06/28 Javascript
详解React中的组件通信问题
2017/07/31 Javascript
vue+axios+mock.js环境搭建的方法步骤
2018/08/28 Javascript
JavaScript使用Math.random()生成简单的验证码
2019/01/21 Javascript
vue自定义指令之面板拖拽的实现
2019/04/14 Javascript
微信小程序整合使用富文本编辑器的方法详解
2019/04/25 Javascript
小程序云开发教程如何使用云函数实现点赞功能
2019/05/18 Javascript
vue中使用v-for时为什么不能用index作为key
2020/04/04 Javascript
解决antd的Form组件setFieldsValue的警告问题
2020/10/29 Javascript
python网络编程学习笔记(一)
2014/06/09 Python
python提取页面内url列表的方法
2015/05/25 Python
Python使用cx_Oracle调用Oracle存储过程的方法示例
2017/10/07 Python
python生成密码字典的方法
2018/07/06 Python
深入浅析Python2.x和3.x版本的主要区别
2018/11/30 Python
python RC4加密操作示例【测试可用】
2019/09/26 Python
Python基础之高级变量类型实例详解
2020/01/03 Python
Python ORM编程基础示例
2020/02/02 Python
cookies应对python反爬虫知识点详解
2020/11/25 Python
python多线程和多进程关系详解
2020/12/14 Python
自荐信格式的六要素
2013/09/21 职场文书
应届生财务会计求职信
2013/11/05 职场文书
社区文艺活动方案
2014/08/19 职场文书
大学生个人学年总结
2015/02/15 职场文书