要想实现热刷新必须明确一下几点:
- webpack要监听文件修改
- 获取最新的hash值发给浏览器
- 浏览器使用hash值发起请求获取最新的文件
- 服务端拦截浏览器的请求,并从内存获取内容返回给浏览器
- 用动态script的方式渲染拉取的文件
服务端要做的事:
- 获取hash值并发送给客户端
- 拦截客户端请求,从内存中获取资源返回
let { compiler } = this;
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
console.log("stats.hash", stats.hash);
this.currentHash = stats.hash;
//每当新一个编译完成后都会向客户端发送消息
this.clientSocketList.forEach(socket => {
// 发送最新的hash
socket.emit("update", this.currentHash);
});
});
通过监听done钩子事件,获取stats,stats中包括当前的hash值
let { compiler } = this;
let config = compiler.options;
// 以watch模式进行编译,会监控文件的变化
compiler.watch({}, () => {
console.log("Compiled successfully!");
});
//设置文件系统为内存文件系统
let fs = new MemoryFileSystem();
this.fs = compiler.outputFileSystem = fs;
// express中间件,将编译的文件返回
const middleware = (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()) {
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.app.use(middleware(config.output.path));
通过拦截客户端的请求从内存中获取相对应的资源
浏览器要做的事:
- 监听服务端发送的hash事件
- 根据最新的hash值发起请求获取要更新的模块
- 用动态script的方式将要更新的文件拉取
首先监听服务端的发送hash事件
// 连接服务器
const URL = "/";
const socket = io(URL);
const onSocketMessage = {
update(hash) {
console.log("hash", hash);
// 如果支持的话发射webpackHotUpdate事件
currentHash = hash;
if (!lastHash) {// 说明是第一次请求
return lastHash = currentHash
}
hotCheck();
},
connect() {
console.log("client connect successful");
}
};
// 添加监听回调
Object.keys(onSocketMessage).forEach(eventName => {
let handler = onSocketMessage[eventName];
socket.on(eventName, handler);
});
根据hash值获取要更新的json文件该文件中包含了下一个文件更新的hash值和更新范围
let hotCheck = async () => {
const res = await hotDownloadManifest()
let chunkIdList = Object.keys(res.c);
// 循环更新的chunk,拉取新代码
chunkIdList.forEach(chunkID => {
hotDownloadUpdateChunk(chunkID);
});
lastHash = currentHash;
}
// 向 server 端发送 Ajax 请求,包含了所有要更新的模块的 hash 值和chunk名
let hotDownloadManifest = () => {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
let hotUpdatePath = `${lastHash}.hot-update.json`// xxxlasthash.hot-update.json
xhr.open("get", hotUpdatePath);
xhr.onload = () => {
let hotUpdate = JSON.parse(xhr.responseText);
resolve(hotUpdate);
};
xhr.onerror = (error) => {
reject(error);
}
xhr.send();
})
}
用动态script的方式将要更新的文件拉取
// 拉取更新的代码
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);
}
可以看出返回的文件是一个webpackHotUpdate函数,该函数是全局的。所以要在window上挂载该函数。
let hotCreateModule = () => {
let hot = {
accept(deps = [], callback) {
deps.forEach(dep => {
hot._acceptedDependencies[dep] = callback || function () { };
})
},
check: hotCheck
}
return hot;
}
// 补丁JS取回来后会调用webpackHotUpdate方法
window.webpackHotUpdate = (chunkID, moreModules) => {
//循环新拉来的模块
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回调
newModule.parents && newModule.parents.forEach(parentID => {
let parentModule = __webpack_require__.c[parentID];
parentModule.hot._acceptedDependencies[moduleID] && parentModule.hot._acceptedDependencies[moduleID]()
});
})
}