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 相关文章推荐
在你的网页中嵌入外部网页的方法
Apr 02 Javascript
Javascript valueOf 使用方法
Dec 28 Javascript
js动态给table添加/删除tr的方法
Aug 02 Javascript
jquery $.fn $.fx是什么意思有什么用
Nov 04 Javascript
JavaScript实现获取dom中class的方法
Feb 09 Javascript
推荐10 款 SVG 动画的 JavaScript 库
Mar 24 Javascript
checkbox批量选中,获取选中项的值的简单实例
Jun 28 Javascript
js智能获取浏览器版本UA信息的方法
Aug 08 Javascript
vue使用原生js实现滚动页面跟踪导航高亮的示例代码
Oct 25 Javascript
JavaScript中的垃圾回收与内存泄漏示例详解
May 02 Javascript
ES6 Generator基本使用方法示例
Jun 06 Javascript
vue二选一tab栏切换新做法实现
Jan 19 Vue.js
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 将bmp图片转为jpg等其他任意格式的图片
2009/06/29 PHP
PHP正则的Unknown Modifier错误解决方法
2010/03/02 PHP
php URL跳转代码 减少外链
2011/06/25 PHP
php中如何判断一个网页请求是ajax请求还是普通请求
2013/08/10 PHP
常见PHP数据库解决方案分析介绍
2015/09/24 PHP
php如何控制用户对图片的访问 PHP禁止图片盗链
2016/03/25 PHP
session 加入redis的实现代码
2016/07/15 PHP
jQuery()方法的第二个参数详解
2015/04/29 Javascript
iframe跨域通信封装详解
2015/08/11 Javascript
使用控制台破解百小度一个月只准改一次名字
2015/08/13 Javascript
跟我学习javascript的隐式强制转换
2015/11/16 Javascript
JavaScript结合Bootstrap仿微信后台多图文界面管理
2016/07/22 Javascript
NodeJS与HTML5相结合实现拖拽多个文件上传到服务器的实现方法
2016/07/26 NodeJs
xtemplate node.js 的使用方法实例解析
2016/08/22 Javascript
AngularJs expression详解及简单示例
2016/09/01 Javascript
vue.js利用Object.defineProperty实现双向绑定
2017/03/09 Javascript
jQuery插件HighCharts绘制简单2D折线图效果示例【附demo源码】
2017/03/21 jQuery
JS中的BOM应用
2018/02/02 Javascript
详解Vue+axios+Node+express实现文件上传(用户头像上传)
2018/08/10 Javascript
vue-cli基础配置及webpack配置修改的完整步骤
2019/10/20 Javascript
js实现文章目录索引导航(table of content)
2020/05/10 Javascript
理解JavaScript中的Proxy 与 Reflection API
2020/09/21 Javascript
python计算圆周长、面积、球体体积并画出圆
2014/04/08 Python
关于Python的一些学习总结
2018/05/25 Python
Python subprocess模块常见用法分析
2018/06/12 Python
解决python3运行selenium下HTMLTestRunner报错的问题
2018/12/27 Python
Django 实现Admin自动填充当前用户的示例代码
2019/11/18 Python
wxPython之wx.DC绘制形状
2019/11/19 Python
Python爬虫抓取论坛关键字过程解析
2020/10/19 Python
工作交流会欢迎词
2014/01/12 职场文书
写求职信有什么意义
2014/02/17 职场文书
水利局群众路线专题民主生活会发言材料
2014/09/21 职场文书
python requests模块的使用示例
2021/04/07 Python
Jupyter notebook 输出部分显示不全的解决方案
2021/04/24 Python
Go缓冲channel和非缓冲channel的区别说明
2021/04/25 Golang
html5中sharedWorker实现多页面通信的示例代码
2021/05/07 Javascript