引言

使用webpack有一段时间了,对其中的热更新的大概理解是:对某个模块做了修改,页面只做局部更新而不需要刷新整个页面来进行更新。这样就能节省因为整个页面刷新所产生开销的时间,模块热加载加快了开发的速度。

热加载的基础是模块热替换(HMR,Hot Module Replacement)。

具体的是:webpack可以监控文件的改动,在模块文件代码发生改动时,并发送 HMR 更新消息(HMR update)给HMR 运行时(HMR runtime)环境,它决定模块的替换,具体可以参考下图:

HMR实现的具体效果可以先看下下图的效果:

可是最近,亲自搭建一个webpack应用项目时,在实现开发环境的模块热更新时,遇到这样那样的问题。由于之前都是使用第三方插件来实现应用的热更新,它们都封装了实现热更新的一些细节,导致在不用第三方插件实现模块热更新时出现问题,其实还是理解的不够深入。于是在搞明白之后写下此文与大家分享。

Hot Module Replacement(HMR)

webpack的自带的HMR插件HotModuleReplacementPlugin是使用webpack热更新功能的基础。其他的第三方插件如webpack-hot-middlewarereact-hot-loaderbabel-plugin-dva-hmr等等都是要配合webpack自带的HotModuleReplacementPlugin插件提供的api来实现代码的热更新。例如下面在某个模块中使用HMR代码一个例子:

 if (module.hot) {
module.hot.accept('./containers/rootContainer.js', () => {
const NextRootContainer = require('./containers/rootContainer.js').default;
render(<NextRootContainer />, document.getElementById('react-root'));
}
}

当然HotModuleReplacementPlugin为可以使用HMR的模块提供了module.hot,它为一个对象,其含有很多api,具体可以参考这里。这样利用插件提供的这些api可以为模块实现自定义的热更新逻辑。

但是,在开发过程中,你们可能也发现了,我们并没有为项目中的每个模块提供这种多余的HMR代码,尽管所有代码都有可能变化。那么当这些代码没有HMR代码的模块发生变化时,他是如何实现热更新的呢?这就要说到webpack HMR更新的冒泡(bubble)机制。具体可以看下图所展示的冒泡机制:

从图中可以看出:

  • 模块C发生了变化,但是模块C没有用HMR代码捕获变化,则模块C的变化消息将冒泡到依赖C模块的其他模块A和B中。

  • 模块B由于使用了HMR代码进行捕获变化,那么应用的变化就按照代码进行了更新。并且不会再冒泡了。

  • 模块A由于同样没有HMR代码捕获变化,同样将变更消息冒泡到依赖A模块的模块entry中。

  • 入口entry模块没有HMR代码捕获变化的话:

    • 1、 若项目使用webpack-dev-server的webpack/hot/dev-server,则页面会刷新整个页面来加载变化;若使用webapck/hot/only-dev-server的话,不会刷新页面,会在控制台展示一些有用的信息供开发者参考。具体可以参考这里

    • 2、若为webpack-hot-middleware配置了reload:true,那么页面就会整个刷新来加载加载变化,这就变成liveroad模式;否则webpack就不知道如何加载变化模块,控制台也会有对应的提示。

例如,在本人的实例中,修改了searchForm.jsx模块,可以在控制台清晰的看到,它一直冒泡到入口模块index.js。如下图:

开发过程中遇到的问题

在用webpack构建的项目中,在开发阶段我们为了实现开发过程代码的热更新,如果对使用HMR不熟悉,可能会遇到这样或者那样的问题。下面就在本人开发过程中遇到过:

1、在cli中使用带--hot选项的webpack-dev-server命令时,不要在webpack的配置文件在配置HMR插件。

否则会报下面的错误,具体可参考这里

注意:

webpack-dev-server的node api模式下配置hot: true仍然需要在webpack配置文件中配置该插件

重要更新:

\(\color{#FF0000}{该规则已不是问题,目前的webpack4已做了处理,即若webpack的配置项配置过HMR插件就不做处理,没有配置则会主动帮我们添加。}\)

其中源码如下:

[].concat(config).forEach((config) => {
config.entry = prependEntry(config.entry || './src'); if (options.hot || options.hotOnly) {
config.plugins = config.plugins || [];
if (
!config.plugins.find(
(plugin) =>
plugin.constructor === webpack.HotModuleReplacementPlugin
)
) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
}
});

2、在不使用第三方HMR库,纯搭建自己的本地node server时,一定要在项目的入口模块添加module.hot.accept代码来接受更新消息以实现热更新。

在本人另一个项目中,使用dora插件系列的dora-plugin-webpack-hmr插件来实现热更新,由于没有在入口模块添加HMR代码来接受变更,导致模块一产生变化就刷新整个页面。

具体是因为:dora-plugin-webpack-hmr使用webpack-hot-middleware时,默认配置了其reload:true(参考这里),所以每次修改都会刷新整个页面。

第三方HMR插件/库的实现细节

前面说到,要想实现webpack的HMR功能,需要两点:webpack配置HMR入口文件添加HMR代码。二者缺一不可,否则模块热更新就会失败。

但是,在开发过程中,我们可能根本没有配置过上面所说的两点;这主要是因为我们在项目中使用第三方HMR插件或者库,它们自动替我们完成这些;要么是二者都会给配置掉,要么就配置其中之一。 比方在本人项目中使用过的dora-plugin-webpack-hmrbabel-plugin-dva-hmr,以及Gaearon大神的react-hot-loader;下面就来说说他们的他们为我们做了什么隐蔽的事。

dora-plugin-webpack-hmr

该插件是为dora系列的插件,主要用在基于dora的项目中。该插件是基于webpack-hot-middleware库来实现热加载的,它主要为我们做了两件事:

  • 代码更新没有捕获时会刷新整个页面来加载更新。 也就是为webpack-hot-middleware的reload属性默认配置true,可看源码1

  • 自动为webpack配置项添加HMR插件配置。具体看源码2这样,我们使用该插件就不需要在webpack中配置HMR,否则会遇到常见问题1中的情况。

所以:

使用dora-plugin-webpack-hmr插件还是需要在入口模块添加module.hot.accept来接受更新,否则达不到热更新效果。

babel-plugin-dva-hmr

该插件是与dva配套的,用在使用dva框架下的代码热更新插件。该插件自动替我们在入口模块添加HMR代码,具体可看源码3,开发环境下入口模块添加的代码如下图:

由此该插件只帮我们在入口模块添加HMR代码接受变更,但是它没有帮我们在webpack中配置HMR,这样HMR的api是不能用的。所以:

使用babel-plugin-dva-hmr插件还需要在webpack配置项中配置HMR。

react-hot-loader@❤️.0.0

该loader的目的是:保持组件状态的热更新。即不仅达到模块的热更新,还要保持各个模块的状态不会丢失,具体可参考Gaearon大神的Hot Reloading in React。它如何保持状态不在本文范围,可自行查询。

在该loader的3.0.0版本前,与babel-plugin-dva-hmr插件类似,它也是自动为我们在模块中注入接受更新的HMR代码而没有在webpack配置项自动添加HMR配置,具体可参考源码4。但是它与前者不同是:它为每个启用该loader的js文件都注入接受更新的HMR代码

例如,在webpack.config.js中为js文件配置该插件:

//这样src目录下的所有.js文件都将被自动添加HMR热更新代码
loaders: [{
test: /\.js$/,
loaders: ['react-hot', 'babel'],
include: path.join(__dirname, 'src')
}]

自动添加的有关HMR代码如下,只截取部分代码:

但是同样的,

我们需要在webpack配置项中添加HMR插件配置。

注意:

react-hot-loader在3.0.0版本之后就废弃掉该方式,不会自动添加HMR热更新代码,需要开发者在项目入口模块手动添加HMR代码,参考这里

搭建带HMR的本地开发node sever

之前,与webpack配合的webpack-dev-server服务,通过配置就可以实现代码热更新,但是隐藏了实现细节。下面我们手动搭建一个自带HMR功能的本地开发node sever。

1、使用webpack-dev-middleware搭建本地服务

webpack-dev-server就是基于webpack-dev-middleware来搭建内部node server。我们搭建自己的开发环境就用它来直接搭建。

2、使用webpack-hot-middleware来实现客户端与服务端的通信以接受更新

该模块只是负责客户端与服务器通信及接受变化,但是如何实现根据热加载来完成应用的无缝变化衔接就超出了该模块的范围,正如其官网所描述:

This module is only concerned with the mechanisms to connect a browser client to a webpack server & receive updates. It will subscribe to changes from the server and execute those changes using webpack's HMR API. Actually making your application capable of using hot reloading to make seamless changes is out of scope, and usually handled by another library.

这句话的意思是:

What this means in practice, is you either need to add some code which calls module.hot.accept(), or use a plugin which can automatically add this code to your modules - otherwise webpack doesn't know how to apply the hot update.

也就是, 要么你在模块中增加调用module.hot.accept()的代码,要么使用第三方插件自动的为你模块添加这些代码;否则webpack不知道怎么更新这些模块。具体可以参考这里

另外,要使用HMR功能,需要在webpack的配置项的每个入口项数组中添加webpack-hot-middleware/client,即:

 entry: {
index: ['./src/index','webpack-hot-middleware/client']
}

3、配置HMR

正如上文所描述的,它分为两步:

  • 首先,要在webpack的配置项plugins需要配置HMR插件即
plugins: [ new webpack.HotModuleReplacementPlugin()]
  • 其次,需要在项目的入口模块中添加HMR代码捕获变化以做热更新。例如下面:
if(module.hot){
module.hot.accpet() //接受模块更新的事件,同时阻止这个事件继续冒泡
}

若为每个模块添加HMR代码来热更新对应的模块机制是不可取的,这会产生大量冗余代码,极不推荐这种做法,除非像第三方插件那样自动帮我们完成。

一般在入口模块添加module.hot的相关api来更新具体变化,入口模块没有添加的话就不会达到热更新的效果,浏览器控制台也会出现如下警告(前提是webpack-hot-middleware的reload配置为false):

在浏览器控制台中出现这样一句提示:

This is usually because the modules which have changed (and their parents) do not know how to hot reload themselves.

正如提示所说的,修改某个子模块时,若不在模块本身或者顶级的入口模块添加热更新接受机制,那么产生变化的模块及其父模块不知道怎么加载他们。

最终,用户自定义的开发环境node server具体的核心开发代码如下:

//dev-server.js 文件
var webpackDevMiddleware = require('webpack-dev-middleware');
var webpackHotMiddleware = require('webpack-hot-middleware'); Object.keys(webpackConfig.entry).forEach(function(name){
webpackConfig.entry[name] = ['webpack-hot-middleware/client'].concat(webpackConfig.entry[name]);
})
var compiler = webpack(webpackConfig); var devMiddleware = webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
hot: true,
noInfo: true,
stats: {
colors: true
}
});
var hotMiddleware = webpackHotMiddleware(compiler);
app.use(devMiddleware);
app.use(hotMiddleware); app.listen(port, function(err){
if(err){
console.log(err);
}else {
var url = 'http://localhost:' + port;
console.log("listening on port %s", port);
}
})

另外,我们可能会想到,在使用redux的react项目中,这种热更新会导致应用的state丢失,为了防止state随热更新而丢失,一般需要在针对reducer的修改来实现进行state的保存,最常用的做法是在store模块下添加如下reducer热更新代码:

if(module.hot){
module.hot.accept('../reducers/index.js', ()=>{
const nextReducer = require('../reducers/index.js');
store.replaceReducer(nextReducer || nextReducer.default);
})
}

至此,一个带HMR代码热更新功能的本地开发node server就搭建成功了。

其他文件热更新的实现

上面的带HMR热更新功能的node server虽已搭建,但是就能满足我们的开发需求了么?我想答案是否定的。上面的热更新其实是针对js文件的热更新,也就是说对js文件的变更做热更新。在实际项目中,我们修改的可不仅仅是js文件,还有css文件html文件等等,这些都需要考虑热更新。

1、html文件的热更新

在项目中,我们使用html-webpack-plugin来生成webpack spa页面。由于该插件不支持HMR,为了支持html的HMR,我们需要利用webpack-hot-middleware提供对外接口来实现。具体需要三步:

  • 首先,在上面的dev-server.js中为html-webpack-plugin钩子html-webpack-plugin-after-emit增加回调,释放一个信号表示html页面已经构建完成。
// dev-server.js
compiler.plugin('compilation', function (compilation) {//webpack编译完成
//在这个插件合成出页面之后,添加一个回调,调用中间件emit一个action为reload的事件,对应另一边client订阅的事件,实现浏览器的刷新
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({action: 'reload'})
cb()
})
});
  • 其次,为html页面构建完成后添加回调,用于实现热更新逻辑
// 新建一个build/dev-client.js文件
var hotClient = require('webpack-hot-middleware/client');
// 添加一个订阅事件,当监听到 event.action === 'reload' 时执行页面刷新
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
  • 最后,修改webpack的entry,为其添加前缀,即第二步创建的文件build/dev-client
// 在webpack配置中设置
Object.keys(config.entry).forEach(function (name, i) {
config.entry[name] = ['./build/dev-client'].concat(config.entry[name])
})

至此,html文件的热更新就完成了,不过这里不是真正意义上的热更新,而是刷新整个页面。

2、css文件的热更新

一般情况下,webpack项目中的css处理都是通过 extract-text-webpack-plugin 插件把css抽离到单独css文件中,但令人遗憾的是该插件是不支持热加载的,具体可以参考issue

但是,可喜的是webpack的style-loader是支持css热加载的。 该插件通过js创建一个 style 标签,然后注入内联的css。

所以,按照上面描述,要想实现css的热加载,只需要: 开发环境不要用extract-text-webpack-plugin插件,而是用style-loader代替。 但是,这种做法被开发者狠狠的吐槽了,并且还列出的原因:

  • 用隔离的css文件能更好的调试

  • 开发和生产环境的尽可能的一致,可以保证尽可能少的bug

吐槽归吐槽,官方还是没有提供热加载支持,但是社区出现了extract-text-webpack-plugin支持热加载的各种实现方式,虽然有些是hack,但是能工作的很好啊,例如下面的列举的实现:

  • 类似于html文件热更新,采用事件通知机制来实现,可以参加这里

  • 将引入js模块中的css模块文件,如require('<path to css file>')这行代码抽取成一个单独的js文件,并在该js文件实现模块更新接收,可以参考这里

  • 基于webpack2热加载机制的事件实现(参考这里以及基于此为避免FOUC升级实现)

  • 用一个babel插件css-hot-loader来实现。

该插件的实现原理:

每次热加载都是一个 js 文件的修改,每个 css 文件在 webpack 中也是一个 js 模块,那么只需要在这个 css 文件对应的模块里面加一段代码就可以实现 css 文件的更新了(具体的是更新外链link的地址url,为其添加时间戳),它会自动在每个css文件中添加如下代码:

 if(module.hot) {
// ${Date.now()}
const cssReload = require(${loaderUtils.stringifyRequest(this, require.resolve('./hotModuleReplacement'))})(${JSON.stringify(options)});
module.hot.dispose(cssReload);
module.hot.accept(undefined, cssReload);
}

最终对应的CSS文件编译生成的代码可能是这样子:

// removed by extract-text-webpack-plugin
if(module.hot) {
// 1498744720173
const cssReload = require("../../../node_modules/css-hot-loader/hotModuleReplacement.js")({"fileMap":"{fileName}"});
module.hot.dispose(cssReload);
module.hot.accept(undefined, cssReload);
} /*****************
** WEBPACK FOOTER
** ./src/routes/main.less
** module id = 636
** module chunks = 1
**/

3、其他配置文件变动的更新

这里不说代码热更新,而是提供一种代码变动更新机制。

在项目中,我们可以很容易实现js、css和html文件的热更新;但是,我们有没有想到过,在我们项目中其他文件变更时也要加载变化后的文件,例如项目中package.json或者webpack.config.js配置文件发生了变化,我们也想浏览器有所反应而不是无动于衷,那么我们可以监控这些文件的变化来实现。具体:

  • 在上述的dev-server.js文件中用chokidar添加对指定文件的监控,比如webpack.config.js
//dev-server.js
var chokidar = require('chokidar');
chokidar.watch(path.resolve(process.cwd(), 'webpack.dev.conf.js')).on('change', function(){
process.send('restart'); //向父进程传递消息信号
})
  • 创建本地node server主入口文件,用于创建dev-server.js对应的子进程。
//dev-server-main.js
var cp = require('child_process');
function start(){
const p = cp.fork(__dirname + '/dev-server.js');
p.on('message', function(data){
if(data === 'restart'){
p.kill('SIGINT');
start();
}
})
}
if(!process.send){
start();
}
  • 最后用node dev-server-main.js开启服务

这样,就可以实现修改webpack.config.js达到重新加载配置的目的。不过它的做法是webpack重新对项目编译。

参考文献

搭建带热更新功能的本地开发node server的更多相关文章

  1. vue开发环境搭建及热更新

    写这篇博客的目的是让广大的学者在初入Vue项目的时候少走些弯路,虽然现在有很多博客也有差不多的内容,但是博主在里面添加了一些学习时碰到的小问题.在阅读这篇博客之前,我先给大家推荐一篇文章<入门W ...

  2. 手把手教你实现热更新功能,带你了解 Arthas 热更新背后的原理

    文章来源:https://studyidea.cn/java-hotswap 一.前言 一天下午正在摸鱼的时候,测试小姐姐走了过来求助,说是需要改动测试环境 mock 应用.但是这个应用一时半会又找不 ...

  3. 为我们的SSR程序添加热更新功能

    前沿 通过上一篇文章 通过vue-cli3构建一个SSR应用程序 我们知道了什么是SSR,以及如何通过vue-cli3构建一个SSR应用程序.但是最后遗留了一些问题没有处理,就是没有添加开发时的热更新 ...

  4. webpack 环境搭建+实现热更新

    让我们一起构建一个小的app 为了便于你更好的了解Webpack带来的好处,我们将会构建一个非常小的app并将资源文件打包.在这个教程中我推荐基于Node4或Node5和NPM3来进行开发,这样就避免 ...

  5. Elastic ik插件配置热更新功能

    ik github地址:https://github.com/medcl/elasticsearch-analysis-ik 官网说明: 热更新 IK 分词使用方法 目前该插件支持热更新 IK 分词, ...

  6. react-router-dom和本地服务本地开发 (node、webpack)

    场景 使用react 做开发,避免会使用react-router React Router 已经是V4的版本 React Router 目前已经被划分成了三个包:react-router,react- ...

  7. Vuejs开发环境搭建及热更新

    一.安装NPM 1.1最新稳定版本: npm install vue 二.命令行工具安装 国内速度慢,使用淘宝镜像: npm install -g cnpm --registry=https://re ...

  8. [Android教程] Cordova开发App入门(二)使用热更新插件

    前言 不知各位遇没遇到过,刚刚发布的应用,突然发现了一个隐藏极深的“碧油鸡(BUG)”,肿么办!肿么办!肿么办!如果被老板发现,一定会让程序员哥哥去“吃鸡”.但是想要修复这个“碧油鸡”,就必须要重新打 ...

  9. Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建

    前言 前面铺垫了这么久,终于要开始写热更新了. Unity游戏热更新包含两个方面,一个是资源的更新,一个是脚本的更新. 资源更新是Unity本来就支持的,在各大平台也都能用.而脚本的热更新在iOS平台 ...

随机推荐

  1. 我是这样发现ISP劫持HTTP请求的

    编者按:Fundebug的客户通过分析我们提供的报警信息,定位了一个非常棘手的问题—ISP劫持http请求.他的分析过程非常有意思,同时也提醒我们,应该及时支持HTTPS来保证站点安全. 原文: IS ...

  2. 遇见未知的CSS

    1.1 CSS中你可能会疑问的几个问题 1.1.1 在CSS中为什么要有层叠 在CSS中可能会有多个样式表同时影响同一个元素的某个属性,设计这个功能的主要原因有两个,解决模块化和作者.用户.用户代理样 ...

  3. hdu1507 Uncle Tom's Inherited Land* 二分匹配

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1507 将i+j为奇数的构成x集合中 将i+j为偶数的构成y集合中 然后就是构建二部图 关键就是构图 然 ...

  4. GreenDao

    前言 我相信,在平时的开发过程中,大家一定会或多或少地接触到 SQLite.然而在使用它时,我们往往需要做许多额外的工作,像编写 SQL 语句与解析查询结果等.所以,适用于 Android 的ORM ...

  5. web 直播&即时聊天------阿里云、融云

    随着直播越来越火,所在公司也打算制作自己的直播,所以去了解了这方面,使用后发现还是有些问题需要记录的. 经过分析,制作直播应该是分为两块来做,即直播与实时评论.这里先去制作实时评论,等直播ok后,也会 ...

  6. 2.Java 加解密技术系列之 MD5

    Java 加解密技术系列之 MD5 序 背景 正文 结束语 序 上一篇文章中,介绍了最基础的编码方式 — — BASE64,也简单的提了一下编码的原理.这篇文章继续加解密的系列,当然也是介绍比较基础的 ...

  7. 在服务器上用Fiddler抓取HTTPS流量

    转自:http://yoursunny.com/t/2011/FiddlerHTTPS/在服务器上用Fiddler抓取HTTPS流量 阳光男孩 发表于2011-03-19 开发互联网应用的过程中,常常 ...

  8. 微信小程序实战(商城)

    github地址(欢迎star):https://github.com/xiaobinwu/dj 版本:0.15.152900(暂未升级原因:升级后需要图片无法本地引用,必须使用image或是远程路径 ...

  9. C# MVC权限验证

    前言 之前一直没怎么接触过权限验证这块,刚好公司老平台改版,就有了这篇权限验证.此篇文章大致讲解下 精确到按钮级别的验证如何实现.以及权限验证设计的参考思路(菜鸟一枚,大神勿喷). 在开发大项目的时候 ...

  10. SmartCoder每日站立会议06

    站立会议内容 讨论了小程序的具体实现方式,主要会加入地图这一元素,使程序看起来更加的方便直观,同时也会使人感到新颖.在对各个点的评论对话功能也在考虑中. 1. 站立会议照片: 2.任务展板 3.燃尽图