来源:https://juejin.im/post/5df36ffd518825124d6c1765
本文以剖析webpack-dev-server源码,从零开始实现一个webpack热更新HMR,深入了解webpack-dev-server、webpack-dev-middleware、webpack-hot-middleware的实现机制,彻底搞懂他们的原理,在面试过程中这个知识点能答的非常出彩,在搭建脚手架过程中这块能得心应手。知其然并知其所以然,更上一层楼。
温馨提示❤️~篇幅较长,建议收藏到电脑端食用更佳。
源码链接
原理图链接
Hot Module Replacement是指当我们对代码修改并保存后,webpack将会对代码进行得新打包,并将新的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,以实现在不刷新浏览器的前提下更新页面。
相对于live reload
刷新页面的方案,HMR的优点在于可以保存应用的状态,提高了开发效率
./src/index.js
// 创建一个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
let content = "hello world"
console.log("welcome");
export default content;
复制代码
cd 项目根目录
npm run dev
当我们在输入框中输入了123,这个时候更新content.js中的代码,会发现hello world!!!!变成了hello world,但是 输入框的值 还保留着,这正是HMR的意义,页面刷新期间保留状态
chunk 就是若干 module 打成的包,一个 chunk 应该包括多个 module,一般来说最终会形成一个 file。而 js 以外的资源,webpack 会通过各种 loader 转化成一个 module,这个模块会被打包到某个 chunk 中,并不会形成一个单独的 chunk。
Webpack watch:使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,每次编译都会产生一个唯一的hash值,
生成两个补丁文件
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只是调用了这些函数
下面这段代码就是使用的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;
}
复制代码
如果要实现热更新,下面这段代码是必不可少的,accept传入的回调函数就是局部刷新逻辑,当./content.js模块改变时执行
if (module.hot) {
module.hot.accept(["./content.js"], render);
}
复制代码
为什么我们只有写了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搜集的回调)
// 再看下面这段代码是不是有点明白了
if (module.hot) {
module.hot.accept(["./content.js"], render);
// 等价于module.hot._acceptedDependencies["./content.js"] = render
// 没错,他就是将模块改变时,要做的事进行了搜集,搜集到_acceptedDependencies中
// 以便当content.js模块改变时,他的父模块index.js通过_acceptedDependencies知道要干什么
}
复制代码
websocket
建立起 浏览器端 和 服务器端 之间的通信通过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服务:建立本地服务和浏览器的双向通信;每当有新的编译,立马告知浏览器执行热更新逻辑
创建一个 websocket客户端 连接 websocket服务端,websocket客户端监听 hash
和 ok
事件
主要的热更新客户端实现逻辑,浏览器会接收服务器端推送的消息,如果需要热更新,浏览器发起http请求去服务器端获取新的模块资源解析并局部刷新页面(这本是HotModuleReplacementPlugin帮我们做了,他将HMR 运行时代码注入到chunk中了,但是我会带大家实现这个 HMR runtime
)
.
├── 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配置文件
复制代码
// /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代码
]
}
复制代码
"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",
}
复制代码
/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
// /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/`);
})
复制代码
// /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;
复制代码
在进行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'
],
},
}
复制代码
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");
});
});
}
复制代码
setupApp() {
this.app = new express();
}
复制代码
webpack-dev-server核心是做准备工作(更改entry、监听webpack done事件等)、创建webserver服务器和websocket服务器让浏览器和服务端建立通信
编译和编译文件相关的操作都抽离到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使用
}
复制代码
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导出的中间件
}
复制代码
让浏览器可以请求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);
}
复制代码
使用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');
});
}
复制代码
// /src/lib/server/Server.js
listen(port, host = "localhost", cb = new Function()) {
this.server.listen(port, host, cb);
}
复制代码
/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实例
// /src/lib/client/emiitter.js
const { EventEmitter } = require("events");
module.exports = new EventEmitter();
// 使用events 发布订阅的模式,主要还是为了解耦
复制代码
// /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();
}
}
复制代码
我们说了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等代码
架起一座桥梁
和源码的出入:源码中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】热更新的重点代码实现
}
复制代码
和源码的出入:源码中调用的是check方法,在check方法里调用module.hot.check方法——也就是hotCheck方法,check里面还会进行一些日志输出。这里直接写check里面的核心hotCheck方法
hotEmitter.on("webpackHotUpdate", (hash) => {
currentHash = hash;
if (!lastHash) {// 说明是第一次请求
return lastHash = currentHash
}
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、向 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();
})
}
复制代码
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);
}
复制代码
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;
}
复制代码
回顾下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 => {
// 1、通过__webpack_require__.c 模块缓存可以找到旧模块
let oldModule = __webpack_require__.c[moduleID];
// 2、更新__webpack_require__.c,利用moduleID将新的拉来的模块覆盖原来的模块
let newModule = __webpack_require__.c[moduleID] = {
i: moduleID,
l: false,
exports: {},
hot: hotCreateModule(moduleID),
parents: oldModule.parents,
children: oldModule.children
};
// 3、执行最新编译生成的模块代码
moreModules[moduleID].call(newModule.exports, newModule, newModule.exports, __webpack_require__);
newModule.l = true;
// 这块请回顾下accept的原理
// 4、让父模块中存储的_acceptedDependencies执行
newModule.parents && newModule.parents.forEach(parentID => {
let parentModule = __webpack_require__.c[parentID];
parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
});
})
}
复制代码
[利用webpack-dev-middleware、webpack-hot-middleware、express实现HMR Demo](
让webpack以watch模式编译;
并将文件系统改为内存文件系统,不会把打包后的资源写入磁盘而是在内存中处理;
中间件负责将编译的文件返回;
提供浏览器和 Webpack 服务器之间的通信机制、且在浏览器端订阅并接收 Webpack 服务器端的更新变化,然后使用webpack的HMR API执行这些更改
服务端监听compiler.hooks.done事件;
通过SSE,服务端编译完成向客户端发送building、built、sync事件;
webpack-dev-middleware是通过EventSource
也叫作server-sent-event(SSE)
来实现服务器发客户端单向推送消息。通过心跳检测,来检测客户端是否还活着,这个????就是SSE心跳检测,在服务端设置了一个 setInterval 每个10s向客户端发送一次
同样客户端代码需要添加到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运行时代码进行更新;
其实我们在实现webpack-dev-server热更新的时候,已经把webpack-hot-middleware的功能都实现了。
他们的最大区别就是浏览器和服务器之间的通信方式,webpack-dev-server
使用的是websocket
,webpack-hot-middleware
使用的是eventSource
;以及通信过程的事件名不一样了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(构建中,不会触发热更新)和sync(判断是否开始热更新流程)
Webpack-Dev-Server 就是内置了 Webpack-dev-middleware 和 Express 服务器,以及利用websocket
替代eventSource
实现webpack-hot-middleware的逻辑
Q: 为什么有了webpack-dev-server
,还有有webpack-dev-middleware
搭配webpack-hot-middleware
的方式呢?
A: webpack-dev-server
是封装好的,除了webpack.config
和命令行参数之外,很难定制型开发。在搭建脚手架时,利用 webpack-dev-middleware
和webpack-hot-middleware
,以及后端服务,让开发更灵活。
步骤 | 功能 | 源码链接 |
---|---|---|
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 |
步骤 | 功能 | 源码链接 |
---|---|---|
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 |
这是剖析webpack-dev-server源码的流程图
终于讲完啦,坚持到这,你是最棒的,为你点赞????(可能的多啃几下哦~)
不知道是否全面,如有不足,欢迎指正。
第一篇文章,如果对你有帮助和启发,还望给个小小的赞哟❤️~给我充充电????
欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!
欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!
在这里,瓶子君不仅介绍算法,还将算法与前端各个领域进行结合,包括浏览器、HTTP、V8、React、Vue源码等。
在这里(算法群),你可以每天学习一道大厂算法编程题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!
另外,每周还有手写源码题,瓶子君也会解答哟!
》》面试官也在看的算法资料《《
“在看和转发”就是最大的支持