构建工具03 Webpack模块热重载(HMR)

使用webpack-dev-server 实现的Hot Moudle Replacement(HMR)让我们在开发时修改代码并保存后,不必手动刷新浏览器,而是让浏览器通过新的模块替换老的模块。这样可以让我们在保证当前页面状态的前提下,让新的代码生效,就如同在Chrome的控制台修改CSS样式一样。

使用

安装webpack-dev-server

npm install webpack-dev-server --save-dev

webpack.config.js中进行配置

devServer: {
  contentBase: path.resolve(__dirname, 'dist'),
  host: 'localhost',
  compress: true,
  port: 8080
}

其中:

  • contentBase:服务器基本运行路径
  • host:服务器运行地址
  • compress:服务器压缩式,一般为true
  • port:服务运行端口

package.json中定义相关命令:

"scripts": {
  "dev": "webpack-dev-server --hot --open",
},

然后执行npm run dev就可以开启webpack的服务,并且实现模块热重载,并且自动打开浏览器。

增加--open属性可以自动打开浏览器。

原理解析

原来只是在各种Cli工具中使用了模块热重载,知道是利用了Webpack的HMR特性,但是它是怎么实现的却不了解。今天在清理收藏夹攒的知识时看到了饿了么前端专栏的这篇文章Webpack HMR 原理解析,写的非常好,简单易懂,把道理也说的很明白。

image

上图展示了从修改代码到模块热更新完成的一个周期:

第一步:Webpack在watch模式下打包更改的文件到内存中(对应图中的①②③)

Webpack-dev-middleware调用Webpack的API对文件系统watch,监听到文件变化时,根据配置文件对模块重新编译打包,将打包后的代码以JavaScript对象的形式保存在内存中。

// webpack-dev-middleware/lib/Shared.js
if (!options.lazy) {
  var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
  context.watching = watching;
}

Webpack会将打包的文件保存在内存中,而不是打包到output.path目录下,是因为访问内存中的代码比访问文件系统中的代码更快,也减少了写入文件的开销。这个过程利用了memory-fs这个库,它提供了一个简单的基于内存的文件系统,所有数据都保存在JavaScript对象中。

图中的第③步也是对文件变化的监控,只不过这一步监听的不是代码,而是在配置文件制定的静态文件目录下的静态文件的变化(当配置文件中配置了devServer.watchContentBasetrue的时候),当静态文件发生变化时通知浏览器对应用进行刷新(注意是浏览器刷新,而非HRM)

第二步:webpack-dev-Server通知浏览器端文件发生变化(对应④)

浏览器端和服务端之间是通过Websocket长连接进行通信的,利用的是sockjs建立的。通过Websocket长连接,webpack-dev-Server将编译打包的各个阶段状态告知浏览器(包括第③步中监听的静态文件的变化)。

同时webpack-dev-Server调用Webpack的API监听complie的done事件,在编译完成后,webpack-dev-Server通过_sendStatus方法将编译打包后的新模块的hash值发送给浏览器,后面的步骤都会利用这个hash值来进行模块热替换。

// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
  // stats.hash 是最新打包文件的 hash 值,发送给浏览器
  this._sendStats(this.sockets, stats.toJson(clientStats));
  this._stats = stats;
});
// ...
Server.prototype._sendStats = function (sockets, stats, force) {
  if (!force && stats &&
  (!stats.errors || stats.errors.length === 0) && stats.assets &&
  stats.assets.every(asset => !asset.emitted)
  ) { 
    return this.sockWrite(sockets, 'still-ok'); 
  }
  // 调用 sockWrite 方法将 hash 值通过 websocket 发送到浏览器端
  this.sockWrite(sockets, 'hash', stats.hash);
  if (stats.errors.length > 0) { 
    this.sockWrite(sockets, 'errors', stats.errors); 
  } 
  else if (stats.warnings.length > 0) { 
    this.sockWrite(sockets, 'warnings', stats.warnings); 
  } else { 
    this.sockWrite(sockets, 'ok'); 
  }
};

第三步:webpack-dev-server/client接收到服务端消息做出响应(对应⑤⑪)

webpack-dev-server/client端并不能够请求更新的代码,也不会执行热更模块操作,而是在接收到通过长连接收到的服务端的消息后,对信息进行处理,而具体的更新操作又交回给了Webpack。

webpack/hot/dev-server的工作就是根据webpack-dev-server/client传给它的信息以及dev-server的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。

我们并没有在业务代码里添加Websocket客户端的代码,也没有在webpack.config.js中的entry属性中添加新的入口文件,那么bundle.js中的接受Websocket信息的代码是从哪来的呢?答案是webpack-dev-server会自动修改Webpack配置中的entry属性,在里面添加了webpck-dev-client的代码。

具体来看,webpack-dev-server/client接收到typehash的消息后会将hash保存起来,接收到typeok的消息后会执行relooad操作,在reload操作中会根据hot的配置是刷新浏览器还是执行热更新(HMR):

// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
    currentHash = hash;
},
ok: function msgOk() {
    // ...
    reloadApp();
},
// ...
function reloadApp() {
  // ...
  if (hot) {
    log.info('[WDS] App hot update...');
    const hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
    // ...
  } else {
    log.info('[WDS] App updated. Reloading...');
    self.location.reload();
  }
}

在上面的代码中,webpack-dev-server/client首先将接收到的hash值存储到currentHash变量中,当接收到ok消息后调用reloadApp方法,在其内部根据hot配置,决定是调用webpack/hot/emitter将最新的hash值发送给Webpack执行热更新,还是直接调用location.reload刷新页面。

第四步:Webpack接收新的hash值并请求模块代码(对应⑥⑦⑧⑨)

首先webpack/hot/dev-server监听上一步webpack-dev-server/client发送的webpackHotUpdate消息,然后调用webpack/lib/HotModuleReplacement.runtime(简称HMR runtime),HMR runtime是客户端HMR的中枢,它首先通过JsonpMainTemplate.runtime调用hotDownloadManifest方法向server端发送JSONP请求,检查是否有更新的文件,如果有的话服务端返回一个JSON响应,包含了所有要更新的模块的hash值。

获取到更新列表后,该模块通过hotDownloadUpdateChunk再次发送JSONP请求,获取到最新的模块代码,并返回给HMR runtime。

上面为了获取最新的Hash值和最新的代码,HMR runtime向服务端发送了两次Ajax请求,为什么不在第三步的Websocket长连接中发送给浏览器呢?可能的原因:

(1)包括了功能模块的解耦,webpack-dev-server/client只负责消息的传递而不负责新模块的拉取,HRM runtime来负责获取新代码

(2)可以使用webpack-hot-middleware来代替webpack-dev-server实现HMR,webpack-hot-middleware没有使用Websocket,而是使用EventSource来实现客户端与服务端通信。

第五步:HMR runtime对模块进行热更新(对应⑩)

HMR runtime会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。

这一切都发生在HMR runtime的hotApply方法中:

// webpack/lib/HotModuleReplacement.runtime
function hotApply() {
  // ...
  var idx;
  var queue = outdatedModules.slice();
  while (queue.length > 0) {
    moduleId = queue.pop();
    module = installedModules[moduleId];
    // ...
    
    // remove module from cache
    delete installedModules[moduleId];
    // when disposing there is no need to call dispose handler
    delete outdatedDependencies[moduleId];
    // remove "parents" references from all children
    
    for (j = 0; j < module.children.length; j++) {
      var child = installedModules[module.children[j]];
      if (!child) continue;
      idx = child.parents.indexOf(moduleId);
      if (idx >= 0) {
        child.parents.splice(idx, 1);
      }
    }
  }
  // ...
  // insert new code
  for (moduleId in appliedUpdate) {
    if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
      modules[moduleId] = appliedUpdate[moduleId];
    }
  }
  // ...
}

hotApply方法主要分为了三个阶段:

  1. 找出陈旧的模块outdatedModules和依赖outdatedDependencies
  2. 从缓存中删除过期的模块和依赖
  3. 将新的模块和依赖添加到moudles中,当下次调用_webpack_require方法时就获取到新的代码

如果HMR失败后,回退到live reload操作,也就是进行浏览器刷新来获取最新打包代码,相关的代码在dev-server中:

module.hot.check(true).then(function(updatedModules) {
  if (!updatedModules) {
    return window.location.reload();
  }
  // ...
}).
catch (function(err) {
  var status = module.hot.status();
  if (["abort", "fail"].indexOf(status) >= 0) {
    window.location.reload();
  }
});

第六步:业务代码改造

当新的模块代替老的模块后,旧的业务代码并不能知道代码发生变化,所以需要在业务代码的入口调用HMR的accept方法,添加模块更新后的处理函数:

// index.js
if (module.hot) {
  module.hot.accept('./hello.js', function() {
    // 更新后的处理函数
  })
}

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 原文首发于:Webpack 3,从入门到放弃 Update (2017.8.27) : 关于 output.pub...
    昵称都被用完了衰阅读 1,882评论 4 19
  • Hot Module Replacement(简称 HMR) 包含以下内容: 热更新图 热更新步骤讲解 第一步:w...
    zhongmeizhi阅读 9,040评论 1 5
  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,428评论 1 32
  • 1.早上送孩子上学回来8点多,困得眼镜睁不开,就睡了,一觉醒来11点半。太不可思议了,我怎么这么瞌睡。 感受是:睡...
    Sunflower语阅读 153评论 0 0
  • 第一次看电影迟到30分钟,但接下来的90分钟,依然让自己感动。 《绿皮书》改编自真人真事,讲述了意裔美国人保镖托尼...
    lovexuxu_阅读 1,151评论 0 0