原文地址:https://juejin.im/post/5df36ffd518825124d6c1765
前言
本文以剖析webpack-dev-server源码,从零开始实现一个webpack热更新HMR,深刻了解webpack-dev-server、webpack-dev-middleware、webpack-hot-middleware的实现机制,完全搞懂他们的原理,在面试过程当中这个知识点能答的很是出彩,在搭建脚手架过程当中这块能驾轻就熟。知其然并知其因此然,更上一层楼。javascript
一年前做为一个小白的我去面试,小姐姐问webpack热更新原理,我跟她说了一小时,而后我被挂了,这也是我人生中惟一一次面试没有经过,她跟我说:其实你不用说这么详细的html
舒适提示❤️~篇幅较长,建议收藏到电脑端食用更佳。前端
源码连接
原理图连接vue
零、什么是HMR
1. 概念
Hot Module Replacement是指当咱们对代码修改并保存后,webpack将会对代码进行得新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实如今不刷新浏览器的前提下更新页面。java
2. 优势
相对于live reload
刷新页面的方案,HMR的优势在于能够保存应用的状态,提升了开发效率node
3. 那就来用用吧
./src/index.js
webpack
// 建立一个input,能够在里面输入一些东西,方便咱们观察热更新的效果
let inputEl = document.createElement("input");
document.body.appendChild(inputEl);
let divEl = document.createElement("div")
document.body.appendChild(divEl);
let render = () => {
let content = require("./content").default;
divEl.innerText = content;
}
render();
// 要实现热更新,这段代码并不可少,描述当模块被更新后作什么
// 为何vue-cli中.vue不用写额外的逻辑,也能够实现热更新呢?那是由于有vue-loader帮咱们作了,不少loader都实现了热更新
if (module.hot) {
module.hot.accept(["./content.js"], render);
}
复制代码
./src/content.js
web
let content = "hello world"
console.log("welcome");
export default content;
复制代码
cd 项目根目录
面试
npm run dev
vue-cli
4. 效果看图
当咱们在输入框中输入了123,这个时候更新content.js中的代码,会发现hello world!!!!变成了hello world,可是 输入框的值 还保留着,这正是HMR的意义,页面刷新期间保留状态
5. 理解chunk和module的概念
chunk 就是若干 module 打成的包,一个 chunk 应该包括多个 module,通常来讲最终会造成一个 file。而 js 之外的资源,webpack 会经过各类 loader 转化成一个 module,这个模块会被打包到某个 chunk 中,并不会造成一个单独的 chunk。
1、webpack编译
Webpack watch:使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包,每次编译都会产生一个惟一的hash值,
1. HotModuleReplacementPlugin作了哪些事
生成两个补丁文件
manifest(JSON)
上一次编译生成的hash.hot-update.json
(如:b1f49e2fc76aae861d9f.hot-update.json)updated chunk (JavaScript)
chunk名字.上一次编译生成的hash.hot-update.js
(如main.b1f49e2fc76aae861d9f.hot-update.js)这里调用了一个全局的
webpackHotUpdate
函数,留心一下这个js的结构是的这两个文件不是webpack生成的,而是这个插件生成的,你可在配置文件把HotModuleReplacementPlugin去掉试一试
在chunk文件中注入HMR runtime运行时代码:咱们的热更新客户端主要逻辑(
拉取新模块代码
、执行新模块代码
、执行accept的回调实现局部更新
)都是这个插件 把函数 注入到咱们的chunk文件中,而非webpack-dev-server,webpack-dev-server只是调用了这些函数
2. 看懂打包文件
下面这段代码就是使用的HotModuleReplacementPlugin编译生成的chunk,注入了HMR runtime的代码,启动服务npm run dev,输入http://localhost:8000/main.js,截取主要的逻辑,细节处理省了(先细看,有个大概印象)
(function (modules) {
//(HMR runtime代码) module.hot属性就是hotCreateModule函数的执行结果,全部hot属性有accept、check等属性
function hotCreateModule() {
var hot = {
accept: function (dep, callback) {
for (var i = 0; i < dep.length; i++)
hot._acceptedDependencies[dep[i]] = callback;
},
check: hotCheck,//【在webpack/hot/dev-server.js中调用module.hot.accept就是hotCheck函数】
};
return hot;
}
//(HMR runtime代码) 如下几个方法是 拉取更新模块的代码
function hotCheck(apply) {}
function hotDownloadUpdateChunk(chunkId) {}
function hotDownloadManifest(requestTimeout) {}
//(HMR runtime代码) 如下几个方法是 执行新代码 并 执行accept回调
window["webpackHotUpdate"] = function webpackHotUpdateCallback(chunkId, moreModules) {
hotAddUpdateChunk(chunkId, moreModules);
};
function hotAddUpdateChunk(chunkId, moreModules) {hotUpdateDownloaded();}
function hotUpdateDownloaded() {hotApply()}
function hotApply(options) {}
//(HMR runtime代码) hotCreateRequire给模块parents、children赋值了
function hotCreateRequire(moduleId) {
var fn = function(request) {
return __webpack_require__(request);
};
return fn;
}
// 模块缓存对象
var installedModules = {};
// 实现了一个 require 方法
function __webpack_require__(moduleId) {
// 判断这个模块是否在 installedModules缓存 中
if (installedModules[moduleId]) {
// 在缓存中,直接返回 installedModules缓存 中该 模块的导出对象
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false, // 模块是否加载
exports: {}, // 模块的导出对象
hot: hotCreateModule(moduleId), // module.hot === hotCreateModule导出的对象
parents: [], // 这个模块 被 哪些模块引用了
children: [] // 这个模块 引用了 哪些模块
};
// (HMR runtime代码) 执行模块的代码,传入参数
modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
// 设置模块已加载
module.l = true;
// 返回模块的导出对象
return module.exports;
}
// 暴露 模块的缓存
__webpack_require__.c = installedModules;
// 加载入口模块 而且 返回导出对象
return hotCreateRequire(0)(__webpack_require__.s = 0);
})(
{
"./src/content.js":
(function (module, __webpack_exports__, __webpack_require__) {}),
"./src/index.js":
(function (module, exports, __webpack_require__) {}),// 在模块中使用的require都编译成了__webpack_require__
"./src/lib/client/emitter.js":
(function (module, exports, __webpack_require__) {}),
"./src/lib/client/hot/dev-server.js":
(function (module, exports, __webpack_require__) {}),
"./src/lib/client/index.js":
(function (module, exports, __webpack_require__) {}),
0:// 主入口
(function (module, exports, __webpack_require__) {
eval(`
__webpack_require__("./src/lib/client/index.js");
__webpack_require__("./src/lib/client/hot/dev-server.js");
module.exports = __webpack_require__("./src/index.js");
`);
})
}
);
复制代码
梳理下大概的流程:
hotCreateRequire(0)(__webpack_require__.s = 0)
主入口当浏览器执行这个chunk时,在执行每一个模块的时候,会给每一个模块传入一个module对象,结构以下,并把这个module对象放到缓存installedModules中;咱们能够经过
__webpack_require__.c拿到这个模块缓存对象
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
hot: hotCreateModule(moduleId),
parents: [],
children: []
};
复制代码hotCreateRequire会帮咱们给模块 module的parents、children赋值
接下来看看hot属性,hotCreateModule(moduleId)返回了啥?没错hot是一个对象有accept、check两个主要属性,接下来咱们就详细的解剖下module.hot和module.hot.accept
function hotCreateModule() {
var hot = {
accept: function (dep, callback) {
for (var i = 0; i < dep.length; i++)
hot._acceptedDependencies[dep[i]] = callback;
},
check: hotCheck,
};
return hot;
}
复制代码
3. 聊聊module.hot和module.hot.accept
1. accept使用
若是要实现热更新,下面这段代码是必不可少的,accept传入的回调函数就是局部刷新逻辑,当./content.js模块改变时执行
if (module.hot) {
module.hot.accept(["./content.js"], render);
}
复制代码
2. accept原理
为何咱们只有写了module.hot.accept(["./content.js"], render);
才能实现热更新,这得从accept这个函数的原理开始提及,咱们再来看看 module.hot 和 module.hot.accept
function hotCreateModule() {
var hot = {
accept: function (dep, callback) {
for (var i = 0; i < dep.length; i++)
hot._acceptedDependencies[dep[i]] = callback;
},
};
return hot;
}
var module = installedModules[moduleId] = {
// ...
hot: hotCreateModule(moduleId),
};
复制代码
没错accept就是往hot._acceptedDependencies
对象存入 局部更新回调函数,_acceptedDependencies何时会用到呢?(当模块文件改变的时候,咱们会调用acceptedDependencies搜集的回调)
3. 再看accept
// 再看下面这段代码是否是有点明白了
if (module.hot) {
module.hot.accept(["./content.js"], render);
// 等价于module.hot._acceptedDependencies["./content.js"] = render
// 没错,他就是将模块改变时,要作的事进行了搜集,搜集到_acceptedDependencies中
// 以便当content.js模块改变时,他的父模块index.js经过_acceptedDependencies知道要干什么
}
复制代码
2、整体流程
1. 整个流程分为客户端和服务端
2. 经过 websocket
创建起 浏览器端 和 服务器端 之间的通讯
3. 服务端主要分为四个关键点
经过webpack建立compiler实例,webpack在watch模式下编译
compiler实例:监听本地文件的变化、文件改变自动编译、编译输出
更改config中的entry属性:将lib/client/index.js、lib/client/hot/dev-server.js注入到打包输出的chunk文件中
往compiler.hooks.done钩子(webpack编译完成后触发)注册事件:里面会向客户端发射
hash
和ok
事件调用webpack-dev-middleware:启动编译、设置文件为内存文件系统、里面有一个中间件负责返回编译的文件
建立webserver静态服务器:让浏览器能够请求编译生成的静态资源
建立websocket服务:创建本地服务和浏览器的双向通讯;每当有新的编译,立马告知浏览器执行热更新逻辑
4. 客户端主要分为两个关键点
建立一个 websocket客户端 链接 websocket服务端,websocket客户端监听
hash
和ok
事件主要的热更新客户端实现逻辑,浏览器会接收服务器端推送的消息,若是须要热更新,浏览器发起http请求去服务器端获取新的模块资源解析并局部刷新页面(这本是HotModuleReplacementPlugin帮咱们作了,他将HMR 运行时代码注入到chunk中了,可是我会带你们实现这个
HMR runtime
)
5. 原理图
3、源码实现
1、结构
.
├── package-lock.json
├── package.json
├── src
│ ├── content.js 测试代码
│ ├── index.js 测试代码入口
│ ├── lib
│ │ ├── client 热更新客户端实现逻辑
│ │ │ ├── index.js 等价于源码中的webpack-dev-server/client/index.js
│ │ │ ├── emitter.js
│ │ │ └── hot
│ │ │ └── dev-server.js 等价于源码中的webpack/hot/dev-server.js 和 HMR runtime
│ │ └── server 热更新服务端实现逻辑
│ │ ├── Server.js
│ │ └── updateCompiler.js
│ └── myHMR-webpack-dev-server.js 热更新服务端主入口
└── webpack.config.js webpack配置文件
复制代码
2、看看webpack.config.js
// /webpack.config.js
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin")
let path = require("path");
module.exports = {
mode: "development",
entry:"./src/index.js",// 这里咱们尚未将客户端代码配置,而是经过updateCompiler方法更改entry属性
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist")
},
plugins: [
new HtmlWebpackPlugin(),// 输出一个html,并将打包的chunk引入
new webpack.HotModuleReplacementPlugin()// 注入HMR runtime代码
]
}
复制代码
3、依赖的模块
"dependencies": {
"express": "^4.17.1",
"mime": "^2.4.4",
"socket.io": "^2.3.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"memory-fs": "^0.5.0",
"html-webpack-plugin": "^3.2.0",
}
复制代码
4、服务端实现
/src/myHMR-webpack-dev-server.js 热更新服务端入口
/src/lib/server/Server.js Server类是热更新服务端的主要逻辑
/src/lib/server/updateCompiler.js 更改entry,增长/src/lib/client/index.js和/src/lib/client/hot/dev-server.js
1. myHMR-webpack-dev-server.js总体一览
// /src/myHMR-webpack-dev-server.js
const webpack = require("webpack");
const Server = require("./lib/server/Server");
const config = require("../../webpack.config");
// 【1】建立webpack实例
const compiler = webpack(config);
// 【2】建立Server类,这个类里面包含了webpack-dev-server服务端的主要逻辑(在 2.Server总体 中会梳理他的逻辑)
const server = new Server(compiler);
// 最后一步【10】启动webserver服务器
server.listen(8000, "localhost", () => {
console.log(`Project is running at http://localhost:8000/`);
})
复制代码
2. Server总体一览
// /src/lib/server/Server.js
const express = require("express");
const http = require("http");
const mime = require("mime");// 能够根据文件后缀,生成相应的Content-Type类型
const path = require("path");
const socket = require("socket.io");// 经过它和http实现websocket服务端
const MemoryFileSystem = require("memory-fs");// 内存文件系统,主要目的就是将编译后的文件打包到内存
const updateCompiler = require("./updateCompiler");
class Server {
constructor(compiler) {
this.compiler = compiler;// 将webpack实例挂载到this上
updateCompiler(compiler);// 【3】entry增长 websocket客户端的两个文件,让其一同打包到chunk中
this.currentHash;// 每次编译的hash
this.clientSocketList = [];// 全部的websocket客户端
this.fs;// 会指向内存文件系统
this.server;// webserver服务器
this.app;// express实例
this.middleware;// webpack-dev-middleware返回的express中间件,用于返回编译的文件
this.setupHooks();// 【4】添加webpack的done事件回调,编译完成时会触发;编译完成时向客户端发送消息,经过websocket向全部的websocket客户端发送两个事件,告知浏览器来拉取新的代码了
this.setupApp();//【5】建立express实例app
this.setupDevMiddleware();// 【6】里面就是webpack-dev-middlerware完成的工做,主要是本地文件的监听、启动webpack编译、设置文件系统为内存文件系统(让编译输出到内存中)、里面有一个中间件负责返回编译的文件
this.routes();// 【7】app中使用webpack-dev-middlerware返回的中间件
this.createServer();// 【8】建立webserver服务器,让浏览器能够访问编译的文件
this.createSocketServer();// 【9】建立websocket服务器,监听connection事件,将全部的websocket客户端存起来,同时经过发送hash事件,将最新一次的编译hash传给客户端
}
setupHooks() {}
setupApp() {}
setupDevMiddleware() {}
routes() {}
createServer() {}
createSocketServer() {}
listen() {}// 启动服务器
}
module.exports = Server;
复制代码
3. 更改webpack的entry属性,增长 websocket客户端文件,让其编译到chunk中
在进行webpack编译前,调用了updateCompiler(compiler)
方法,这个方法很关键,他会往咱们的chunk中偷偷塞入两个文件,lib/client/client.js
和lib/client/hot-dev-server.js
这两个文件是干什么的呢?咱们说利用websocket实现双向通讯的,咱们服务端会建立一个websocket服务器(第9步会讲),每当代码改动时会从新进行编译,生成新的编译文件,这时咱们websocket服务端将通知浏览器,你快来拉取新的代码啦
那么一个websocket客户端,实现和服务端通讯的逻辑,是否是也的有呢?因而webpack-dev-server给咱们提供了客户端的代码,也就是上面的两个文件,为咱们安插了一个间谍,悄悄地去拉新的代码、实现热更新
为啥要分红两个文件呢?固然是模块划分啦,balabala写在一坨总很差吧,在客户端实现部分我会细说这两个文件干了什么
// /src/lib/server/updateCompiler.js
const path = require("path");
let updateCompiler = (compiler) => {
const config = compiler.options;
config.entry = {
main: [
path.resolve(__dirname, "../client/index.js"),
path.resolve(__dirname, "../client/hot-dev-server.js"),
config.entry
]
}
compiler.hooks.entryOption.call(config.context, config.entry);
}
module.exports = updateCompiler;
复制代码
修改后的webpack
入口配置以下:
{
entry:{
main: [
'xxx/src/lib/client/index.js',
'xxx/src/lib/client/hot/dev-server.js',
'./src/index.js'
],
},
}
复制代码
4. 添加webpack的done
事件回调
咱们要在compiler编译完成的钩子上注册一个事件,这个事件主要干了一件事情,每当新一次编译完成后都会向全部的websocket客户端发送消息,发射两个事件,通知浏览器来拉代码啦
浏览器会监听这两个事件,浏览器会去拉取上次编译生成的hash.hot-update.json
,具体的逻辑咱们会在下面的客户端章节详细讲解
// /src/lib/server/Server.js
setupHooks() {
let { compiler } = this;
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
//每次编译都会产生一个惟一的hash值
this.currentHash = stats.hash;
//每当新一个编译完成后都会向全部的websocket客户端发送消息
this.clientSocketList.forEach(socket => {
//先向客户端发送最新的hash值
socket.emit("hash", this.currentHash);
//再向客户端发送一个ok
socket.emit("ok");
});
});
}
复制代码
5.建立express实例app
setupApp() {
this.app = new express();
}
复制代码
6. 添加webpack-dev-middleware中间件
1. 关于webpack-dev-server和webpack-dev-middleware
webpack-dev-server核心是作准备工做(更改entry、监听webpack done事件等)、建立webserver服务器和websocket服务器让浏览器和服务端创建通讯
编译和编译文件相关的操做都抽离到webpack-dev-middleware
2. Webpack-dev-middleware主要干了三件事(这里咱们本身实现他的逻辑)
本地文件的监听、启动webpack编译;使用监控模式开始启动webpack编译在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包;
设置文件系统为内存文件系统(让编译输出到内存中)
实现了一个express中间件,将编译的文件返回
// /src/lib/server/Server.js
setupDevMiddleware() {
let { compiler } = this;
// 会监控文件的变化,每当有文件改变(ctrl+s)的时候都会从新编译打包
// 在编译输出的过程当中,会生成两个补丁文件 hash.hot-update.json 和 chunk名.hash.hot-update.js
compiler.watch({}, () => {
console.log("Compiled successfully!");
});
//设置文件系统为内存文件系统,同时挂载到this上,以方便webserver中使用
let fs = new MemoryFileSystem();
this.fs = compiler.outputFileSystem = fs;
// express中间件,将编译的文件返回
// 为何不直接使用express的static中间件,由于咱们要读取的文件在内存中,因此本身实现一款简易版的static中间件
let staticMiddleWare = (fileDir) => {
return (req, res, next) => {
let { url } = req;
if (url === "/favicon.ico") {
return res.sendStatus(404);
}
url === "/" ? url = "/index.html" : null;
let filePath = path.join(fileDir, url);
try {
let statObj = this.fs.statSync(filePath);
if (statObj.isFile()) {// 判断是不是文件,不是文件直接返回404(简单粗暴)
// 路径和原来写到磁盘的同样,只是这是写到内存中了
let content = this.fs.readFileSync(filePath);
res.setHeader("Content-Type", mime.getType(filePath));
res.send(content);
} else {
res.sendStatus(404);
}
} catch (error) {
res.sendStatus(404);
}
}
}
this.middleware = staticMiddleWare;// 将中间件挂载在this实例上,以便app使用
}
复制代码
7. app中使用webpack-dev-middlerware返回的中间件
routes() {
let { compiler } = this;
let config = compiler.options;// 通过webpack(config),会将 webpack.config.js导出的对象 挂在compiler.options上
this.app.use(this.middleware(config.output.path));// 使用webpack-dev-middleware导出的中间件
}
复制代码
8. 建立webserver服务器
让浏览器能够请求webpack编译后的静态资源
这里使用了express和原生的http,你可能会有个疑问?为何不直接使用express和http中的任意一个?
不直接使用express,是由于咱们拿不到server,能够看下express的源码,为何要这个server,由于咱们要在socket中使用;
不直接使用http,想必你们也知道,原生http写逻辑简直伤不起;咱们这里只是写了一个简单的static处理逻辑,因此看不出什么,可是源码中还有不少的逻辑,这里只是将核心逻辑挑了出来实现
那既然二者都有缺陷,就结合一下呗,咱们用原生http建立一个服务,不就拿到了server嘛,这个server的请求逻辑,仍是交给express处理就行了呗,
this.server = http.createServer(app);
一行代码完美搞定
// /src/lib/server/Server.js
createServer() {
this.server = http.createServer(this.app);
}
复制代码
9. 建立websocket服务器
使用socket.js在浏览器端和服务端之间创建一个 websocket 长链接
// /src/lib/server/Server.js
createSocketServer() {
// socket.io+http服务 实现一个websocket
const io = socket(this.server);
io.on("connection", (socket) => {
console.log("a new client connect server");
// 把全部的websocket客户端存起来,以便编译完成后向这个websocket客户端发送消息(实现双向通讯的关键)
this.clientSocketList.push(socket);
// 每当有客户端断开时,移除这个websocket客户端
socket.on("disconnect", () => {
let num = this.clientSocketList.indexOf(socket);
this.clientSocketList = this.clientSocketList.splice(num, 1);
});
// 向客户端发送最新的一个编译hash
socket.emit('hash', this.currentHash);
// 再向客户端发送一个ok
socket.emit('ok');
});
}
复制代码
10. 启动webserver服务,开始监听
// /src/lib/server/Server.js
listen(port, host = "localhost", cb = new Function()) {
this.server.listen(port, host, cb);
}
复制代码
5、客户端实现
/src/lib/client/index.js负责websocket客户端hash和ok事件的监听,ok事件的回调只干了一件事
发射webpackHotUpdate事件
/src/lib/client/hot/dev-server.js负责监听
webpackHotUpdate
,调用hotCheck
开始拉取代码,实现局部更新他们经过/src/lib/client/emitter.js的共用了一个EventEmitter实例
0. emitter纽带
// /src/lib/client/emiitter.js
const { EventEmitter } = require("events");
module.exports = new EventEmitter();
// 使用events 发布订阅的模式,主要仍是为了解耦
复制代码
1. index.js实现
// /src/lib/client/index.js
const io = require("socket.io-client/dist/socket.io");// websocket客户端
const hotEmitter = require("./emitter");// 和hot/dev-server.js共用一个EventEmitter实例,这里用于发射事件
let currentHash;// 最新的编译hash
//【1】链接websocket服务器
const URL = "/";
const socket = io(URL);
//【2】websocket客户端监听事件
const onSocketMessage = {
//【2.1】注册hash事件回调,这个回调主要干了一件事,获取最新的编译hash值
hash(hash) {
console.log("hash",hash);
currentHash = hash;
},
//【2.2】注册ok事件回调,调用reloadApp进行热更新
ok() {
console.log("ok");
reloadApp();
},
connect() {
console.log("client connect successfully!");
}
};
// 将onSocketMessage进行循环,给websocket注册事件
Object.keys(onSocketMessage).forEach(eventName => {
let handler = onSocketMessage[eventName];
socket.on(eventName, handler);
});
//【3】reloadApp中 发射webpackHotUpdate事件
let reloadApp = () => {
let hot = true;
// 会进行判断,是否支持热更新;咱们自己就是为了实现热更新,因此简单粗暴设置为true
if (hot) {
// 事件通知:若是支持的话发射webpackHotUpdate事件
hotEmitter.emit("webpackHotUpdate", currentHash);
} else {
// 直接刷新:若是不支持则直接刷新浏览器
window.location.reload();
}
}
复制代码
2. 聊聊源码中的webpack/hot/dev-server.js
咱们说了webpack-dev-server.js会在updateCompiler(compiler)
更改entry配置,将webpack-dev-server/client/index.js?http://localhost:8080
和webpack/hot/dev-server.js
一块儿打包到chunk中,那咱们就来揭开源码中的hot/devserver.js的真面目吧,没错下面就是主要代码
// 源码中webpack/hot/dev-server.js
if (module.hot) {// 是否支持热更新
var check = function check() {
module.hot
.check(true)// 没错module.hot.check就是hotCheck函数,看是否是绕到了HRMPlugin在打包的chunk中注入的HMR runtime代码啦
.then( /*日志输出*/)
.catch( /*日志输出*/)
};
// 和client/index.js共用一个EventEmitter实例,这里用于监听事件
var hotEmitter = require("./emitter");
// 监听webpackHotUpdate事件
hotEmitter.on("webpackHotUpdate", function(currentHash) {
check();
});
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
}
复制代码
明白了吧,真正的客户端热更新的逻辑都是HotModuleReplacementPlugin.runtime运行时代码干的,经过module.hot.check=hotCheck把 webpack/hot/dev-server.js
和 HotModuleReplacementPlugin在chunk文件中注入的hotCheck等代码
架起一座桥梁
3. hot/dev-server.js总体概览
和源码的出入:源码中hot/dev-server.js很简单,就是调用了module.hot.check(即HMR runtime运行时的hotCheck)。HotModuleReplacementPlugin插入的代码是热更新客户端的核心
接下来看看咱们本身要实现的hot/dev-server.js的总体,咱们不使用HotModuleReplacementPlugin插入的运行时代码,而是在hot/dev-server.js咱们本身实现一遍
let hotEmitter = require("../emitter");// 和client.js公用一个EventEmitter实例
let currentHash;// 最新编译生成的hash
let lastHash;// 表示上一次编译生成的hash,源码中是hotCurrentHash,为了直接表达他的字面意思换了个名字
//【4】监听webpackHotUpdate事件,而后执行hotCheck()方法进行检查
hotEmitter.on("webpackHotUpdate", (hash) => {
hotCheck();
})
//【5】调用hotCheck拉取两个补丁文件
let hotCheck = () => {
hotDownloadManifest().then(hotUpdate => {
hotDownloadUpdateChunk(chunkID);
})
}
// 【6】拉取lashhash.hot-update.json,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名
let hotDownloadManifest = () => {}
// 【7】拉取更新的模块chunkName.lashhash.hot-update.json,经过JSONP请求获取到更新的模块代码
let hotDownloadUpdateChunk = (chunkID) => {}
// 【8.0】这个hotCreateModule很重要,module.hot的值 就是这个函数执行的结果
let hotCreateModule = (moduleID) => {
let hot = {
accept() {},
check: hotCheck
}
return hot;
}
//【8】补丁JS取回来后会调用webpackHotUpdate方法(请看update chunk的格式),里面会实现模块的热更新
window.webpackHotUpdate = (chunkID, moreModules) => {
//【9】热更新的重点代码实现
}
复制代码
4. 监听webpackHotUpdate事件
和源码的出入:源码中调用的是check方法,在check方法里调用module.hot.check方法——也就是hotCheck方法,check里面还会进行一些日志输出。这里直接写check里面的核心hotCheck方法
hotEmitter.on("webpackHotUpdate", (hash) => {
currentHash = hash;
if (!lastHash) {// 说明是第一次请求
return lastHash = currentHash
}
hotCheck();
})
复制代码
5. hotCheck
let hotCheck = () => {
//【6】hotDownloadManifest用来拉取lasthash.hot-update.json
hotDownloadManifest().then(hotUpdate => {// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}}
let chunkIdList = Object.keys(hotUpdate.c);
//【7】调用hotDownloadUpdateChunk方法经过JSONP请求获取到最新的模块代码
chunkIdList.forEach(chunkID => {
hotDownloadUpdateChunk(chunkID);
});
lastHash = currentHash;
}).catch(err => {
window.location.reload();
});
}
复制代码
6. 拉补丁代码——lasthash.hot-update.json
// 六、向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(xxxlasthash.hot-update.json),该 Manifest 包含了全部要更新的模块的 hash 值和chunk名
let hotDownloadManifest = () => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
let hotUpdatePath = `${lastHash}.hot-update.json`
xhr.open("get", hotUpdatePath);
xhr.onload = () => {
let hotUpdate = JSON.parse(xhr.responseText);
resolve(hotUpdate);// {"h":"58ddd9a7794ab6f4e750","c":{"main":true}}
};
xhr.onerror = (error) => {
reject(error);
}
xhr.send();
})
}
复制代码
7. 拉补丁代码——更新的模块代码lasthash.hot-update.json
hotDownloadUpdateChunk方法经过JSONP
请求获取到最新的模块代码
为何是JSONP?由于chunkName.lasthash.hot-update.js是一个js文件,咱们为了让他从服务端获取后能够立马执行js脚本
let hotDownloadUpdateChunk = (chunkID) => {
let script = document.createElement("script")
script.charset = "utf-8";
script.src = `${chunkID}.${lastHash}.hot-update.js`//chunkID.xxxlasthash.hot-update.js
document.head.appendChild(script);
}
复制代码
8.0 hotCreateModule
module.hot
module.hot.accept
module.hot.check
let hotCreateModule = (moduleID) => {
let hot = {// module.hot属性值
accept(deps = [], callback) {
deps.forEach(dep => {
// 调用accept将回调函数 保存在module.hot._acceptedDependencies中
hot._acceptedDependencies[dep] = callback || function () { };
})
},
check: hotCheck// module.hot.check === hotCheck
}
return hot;
}
复制代码
8. webpackHotUpdate实现热更新
回顾下hotDownloadUpdateChunk来取的代码长什么样
webpackHotUpdate("index", {
"./src/lib/content.js":
(function (module, __webpack_exports__, __webpack_require__) {
eval("");
})
})
复制代码
调用了一个webpackHotUpdate
方法,说明咱们得在全局上有一个webpackHotUpdate
方法
和源码的出入:源码webpackHotUpdate里面会调用hotAddUpdateChunk方法动态更新模块代码(用新的模块替换掉旧的模块),而后调用hotApply方法进行热更新,这里将这几个方法核心直接写在webpackHotUpdate中
window.webpackHotUpdate = (chunkID, moreModules) => {
// 【9】热更新
// 循环新拉来的模块
Object.keys(moreModules).forEach(moduleID => {
// 一、经过__webpack_require__.c 模块缓存能够找到旧模块
let oldModule = __webpack_require__.c[moduleID];
// 二、更新__webpack_require__.c,利用moduleID将新的拉来的模块覆盖原来的模块
let newModule = __webpack_require__.c[moduleID] = {
i: moduleID,
l: false,
exports: {},
hot: hotCreateModule(moduleID),
parents: oldModule.parents,
children: oldModule.children
};
// 三、执行最新编译生成的模块代码
moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__);
newModule.l = true;
// 这块请回顾下accept的原理
// 四、让父模块中存储的_acceptedDependencies执行
newModule.parents && newModule.parents.forEach(parentID => {
let parentModule = __webpack_require__.c[parentID];
parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
});
})
}
复制代码
6、webpack-dev-server,webpack-hot-middleware,webpack-dev-middleware
利用webpack-dev-middleware、webpack-hot-middleware、express实现HMR Demo
1.Webpack-dev-middleware
让webpack以watch模式编译;
并将文件系统改成内存文件系统,不会把打包后的资源写入磁盘而是在内存中处理;
中间件负责将编译的文件返回;
2. Webpack-hot-middleware:
提供浏览器和 Webpack 服务器之间的通讯机制、且在浏览器端订阅并接收 Webpack 服务器端的更新变化,而后使用webpack的HMR API执行这些更改
1. 服务端
服务端监听compiler.hooks.done事件;
经过SSE,服务端编译完成向客户端发送building、built、sync事件;
webpack-dev-middleware是经过
EventSource
也叫做server-sent-event(SSE)
来实现服务器发客户端单向推送消息。经过心跳检测,来检测客户端是否还活着,这个💓就是SSE心跳检测,在服务端设置了一个 setInterval 每一个10s向客户端发送一次
2. 客户端
一样客户端代码须要添加到config的entry属性中,
// /dev-hot-middleware demo/webpack.config.js
entry: {
index: [
// 主动引入client.js
"./node_modules/webpack-hot-middleware/client.js",
// 无需引入webpack/hot/dev-server,webpack/hot/dev-server 经过 require('./process-update') 已经集成到 client.js模块
"./src/index.js",
]
},
复制代码客户端 建立
EventSource
实例 请求 /__webpack_hmr,监听building、built、sync事件,回调函数会利用HotModuleReplacementPlugin运行时代码进行更新;
3. 总结
其实咱们在实现webpack-dev-server热更新的时候,已经把webpack-hot-middleware的功能都实现了。
他们的最大区别就是浏览器和服务器之间的通讯方式,
webpack-dev-server
使用的是websocket
,webpack-hot-middleware
使用的是eventSource
;以及通讯过程的事件名不同了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(构建中,不会触发热更新)和sync(判断是否开始热更新流程)
3. webpack-dev-server
Webpack-Dev-Server 就是内置了 Webpack-dev-middleware 和 Express 服务器,以及利用websocket
替代eventSource
实现webpack-hot-middleware的逻辑
4. 区别
Q: 为何有了webpack-dev-server
,还有有webpack-dev-middleware
搭配webpack-hot-middleware
的方式呢?
A: webpack-dev-server
是封装好的,除了webpack.config
和命令行参数以外,很难定制型开发。在搭建脚手架时,利用 webpack-dev-middleware
和webpack-hot-middleware
,以及后端服务,让开发更灵活。
7、源码位置
1. 服务端
步骤 | 功能 | 源码连接 |
---|---|---|
1 | 建立webpack实例 | webpack-dev-server |
2 | 建立Server实例 | webpack-dev-server |
3 | 更改config的entry属性 | Server updateCompiler |
entry添加dev-server/client/index.js | addEntries | |
entry添加webpack/hot/dev-server.js | addEntries | |
4 | 监听webpack的done事件 | Server |
编译完成向websocket客户端推送消息,最主要信息仍是新模块的hash 值,后面的步骤根据这一hash 值来进行模块热替换 |
Server | |
5 | 建立express实例app | Server |
6 | 使用webpack-dev-middlerware | Server |
以watch模式启动webpack编译,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块从新编译打包 | webpack-dev-middleware | |
设置文件系统为内存文件系统 | webpack-dev-middleware | |
返回一个中间件,负责返回生成的文件 | webpack-dev-middleware | |
7 | app中使用webpack-dev-middlerware返回的中间件 | Server |
8 | 建立webserver服务器并启动服务 | Server |
9 | 使用sockjs在浏览器端和服务端之间创建一个 websocket 长链接 | Server |
建立socket服务器并监听connection事件 | SockJSServer |
2. 客户端
步骤 | 功能 | 源码连接 |
---|---|---|
1 | 链接websocket服务器 | client/index.js |
2 | websocket客户端监听事件 | client/index.js |
监听hash事件,保存此hash值 | client/index.js | |
监听ok事件,执行reloadApp 方法进行更新 |
client/index.js | |
3 | 调用reloadApp,在reloadApp中会进行判断,是否支持热更新,若是支持的话发射webpackHotUpdate 事件,若是不支持则直接刷新浏览器 |
client/index.js |
reloadApp中发射webpackHotUpdate事件 | reloadApp | |
4 | 在webpack/hot/dev-server.js 会监听webpackHotUpdate事件, |
webpack/hot/dev-server.js |
而后执行check()方法进行检查 | webpack/hot/dev-server.js | |
在check方法里会调用module.hot.check 方法 |
webpack/hot/dev-server.js | |
5 | module.hot.check也就是hotCheck | HotModuleReplacement.runtime |
6 | 调用hotDownloadManifest`,向 server 端发送 Ajax 请求,服务端返回一个 Manifest文件(lasthash.hot-update.json),该 Manifest 包含了本次编译hash值 和 更新模块的chunk名 | HotModuleReplacement.runtime JsonpMainTemplate.runtime |
7 | 调用hotDownloadUpdateChunk``方法经过JSONP请求获取到最新的模块代码 | HotModuleReplacement.runtime HotModuleReplacement.runtime JsonpMainTemplate.runtime |
8 | 补丁JS取回来后会调用的webpackHotUpdate 方法,里面会调用hotAddUpdateChunk 方法,用新的模块替换掉旧的模块 |
JsonpMainTemplate.runtime |
9 | 调用hotAddUpdateChunk 方法动态更新模块代码 |
JsonpMainTemplate.runtime JsonpMainTemplate.runtime |
10 | 调用hotApply 方法进行热更新 |
HotModuleReplacement.runtime HotModuleReplacement.runtime |
从缓存中删除旧模块 | HotModuleReplacement.runtime | |
执行accept的回调 | HotModuleReplacement.runtime | |
执行新模块 | HotModuleReplacement.runtime |
8、流程图
这是剖析webpack-dev-server源码的流程图

本文分享自微信公众号 - 前端巅峰(Java-Script-)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。