Hot Module Replacement是指当你对代码修改并保存后,webpack将会对代码进行新打爆,并将新的模块发送到浏览器端。
相对于live reload 刷新页面的方案, HMR的有点在于保存应用的状态,提高开发效率。
1、需要 cnpm i webpack webpack-cli webpack-dev-server html-webpack-plugin express socket.io events -S
// package.json
{
"name": "15.hmr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"express": "^4.17.1"
},
"dependencies": {
"express": "^4.17.1",
"html-webpack-plugin": "^4.4.1",
"mime": "^2.4.6",
"socket.io": "^2.3.0",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
// webpack.config
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.join(__dirname, "dist"),
},
devServer: {
hot: true,
contentBase: path.join(__dirname, "dist"),
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
new webpack.HotModuleReplacementPlugin()
],
};
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack热更新title>
head>
<body>
<div id="root">div>
<script src="/socket.io/socket.io.js">script>
body>
html>
// 入口文件:
// src/title.js
module.exports = 'title7';
// src/index.js // 入口文件
import '../webpackHotDevClient';
let root = document.getElementById("root");
function render() {
let title = require("./title");
root.innerHTML = title;
}
render(); // 渲染页面用
if(module.hot){
module.hot.accept(['./title'],()=>{
render();
});
}
1、创建webpack实例
2、定义个server类 实例化时传入webpack的complier
3、手写个简易版webpack-dev-middleware中间件, 做静态服务器, 用来提供编译后产出的文件的静态文件服务
4、使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些socket
消息进行不同的操作
当然服务端传递的最主要信息还是新模块的hash
值,后面的步骤根据这一hash
值来进行模块热替换
启动webpack-dev-server服务器
5、compiler.watch监听编译
6、 每次文件编译后触发webpack-dev-server事件,执行回调的socket.emit事件, 发送编译完后把hash发给客户端
const path = require("path");
const fs = require("fs");
const express = require("express");
const mime = require("mime");
const webpack = require("webpack");
let config = require("./webpack.config");//配置对象
let compiler = webpack(config); // 使用webpack 配置 返回一个complier编译器 里面可以执行webpack操作
//1. 创建webpack实例
//2. 启动webpack-dev-server服务器
class Server {
constructor(compiler) {
//4. 添加webpack的`done`事件回调,在编译完成后会向浏览器发送消息
let lastHash;
let sockets = [];
// 插件
compiler.hooks.done.tap("webpack-dev-server", (stats) => {
lastHash = stats.hash;
sockets.forEach((socket) => {
socket.emit("hash", stats.hash);//编译成功后先把hash值发给客户端,再发送ok事件
socket.emit("ok");
});
});
let app = new express();
//webpack开始以监听模式进行编译
compiler.watch({
}, (err) => {
console.log("编译成功");
});
//3. 添加webpack-dev-middleware中间件
//用来提供编译后产出的文件的静态文件服务
const webpackDevMiddleware = (req, res, next) => {
if (req.url === "/favicon.ico") {
return res.sendStatus(404);
} else if (req.url === "/") {
return res.sendFile(path.join(config.output.path,'index.html'));
}
// 比如请求/main.js 就拿到filename = dist/main.js
let filename = path.join(config.output.path, req.url.slice(1));//main.js
try {
let stats = fs.statSync(filename);
// 判断是文件就读取内容 返回给html
if (stats.isFile()) {
let content = fs.readFileSync(filename);
res.header("Content-Type", mime.getType(filename));
res.send(content);
} else {
next();
}
} catch (error) {
return res.sendStatus(404);
}
};
app.use(webpackDevMiddleware);
this.server = require("http").createServer(app);
//4. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接
//将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些`socket`消息进行不同的操作
//当然服务端传递的最主要信息还是新模块的`hash`值,后面的步骤根据这一`hash`值来进行模块热替换
let io = require("socket.io")(this.server);
//启动一个websocket服务器
io.on("connection", (socket) => {
sockets.push(socket);
if (lastHash) {
//5.发送hash值
socket.emit("hash", lastHash);
socket.emit("ok");
}
});
}
//9. 创建http服务器并启动服务
listen(port) {
this.server.listen(port, () => {
console.log(port + "服务启动成功!");
});
}
}
//3. 创建Server服务器
let server = new Server(compiler);
server.listen(8080);
1、scoket在客户端连接后,就能on ‘hash’ 和 ‘ok’ 事件了
2、currentHash 记录当前的hash, lastHash记录上次文件的hash
3、客户端监听到ok后,执行hotCheck(),即拉取 ("/" + lastHash + “.hot-update.json”)的数据 里面是一个对象,如下
用lastHash是为了告诉服务器 我目前版本是这个 你给当前到最新版本的补丁hash我
h:当前hash值 留到下次才用到更新 每次请求描述文件都是传上一个的hash值
c:里面的main代表修改的模块
4、通过script标签加载 “/” + chunkId + “.” + lastHash+ “.hot-update.js”
即 /main.f147xxxxxx.hot-update.js js内容如下,调用webpackHotUpdate 传入 main 和 修改的js对象
调用下面的方法
//11. 补丁JS取回来后会调用`webpackHotUpdate`方法
window.webpackHotUpdate = (chunkId, moreModules) => {
for (let moduleId in moreModules) {
let oldModule = __webpack_require__.c[moduleId];//获取老模块
let {
parents } = oldModule;//父亲们 儿子们
// 新的改变后的模块初始化
var module = (__webpack_require__.c[moduleId] = {
i: moduleId,
exports: {
},
parents,
children,
hot: window.hotCreateModule(),
});
// 执行模块操作
moreModules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
parents.forEach((parent) => {
let parentModule = __webpack_require__.c[parent];
parentModule.hot &&
parentModule.hot._acceptedDependencies[moduleId] &&
parentModule.hot._acceptedDependencies[moduleId]();
});
lastHash = currentHash;
}
};
5、webpackHotUpdate 调用时
5.1、会创建新的模块对象 module 然后调用该模块方法
5.2、循环父亲(即哪个js依赖了他,就是他的父亲)比如改了title.js 父亲就是index.js 然后执行父亲的hot对象的_acceptedDependencies里面对应模块的callback方法
_acceptedDependencies用来装他的依赖模块的回调,哪个依赖改动了就执行该依赖回调
5.3 执行完 页面就拿到新的title之后 重新渲染 , 思路就是如此。
window.hotCreateModule = () => {
var hot = {
_acceptedDependencies: {
}, //接收的依赖
accept: function (dep, callback) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback;
//hot._acceptedDependencies['./title']=callback
}
},
};
return hot;
}
// index.js 调用了module.hot.accept 等于依赖了title.js title改变后 会触发render();
if(module.hot){
module.hot.accept(['./title'],()=>{
render();
});
}
6、webpackHotDevClient代码介绍
// webpackHotDevClient.js
/*
* @description:
* @author: steve.deng
* @Date: 2020-09-07 11:50:33
* @LastEditors: steve.deng
* @LastEditTime: 2020-09-15 10:14:26
*/
let socket = io("/");//先通过socket.io连接服务器
let currentHash;//当前的hash
let lastHash;//上一次的hash
const onConnected = () => {
console.log("客户端已经连接");
//6. 客户端会监听到此hash消息
socket.on("hash", (hash) => {
currentHash = hash;
});
//7. 客户端收到`ok`的消息
socket.on("ok", () => {
hotCheck();
});
socket.on("disconnect", () => {
lastHash = currentHash = null;
});
};
//8.执行hotCheck方法进行更新
function hotCheck() {
if (!lastHash || lastHash === currentHash) {
return (lastHash = currentHash);
}
//9.向 server 端发送 Ajax 请求,服务端返回一个hot-update.json文件,该文件包含了所有要更新的模块的 `hash` 值和chunk名
hotDownloadManifest().then((update) => {
let chunkIds = Object.keys(update.c);//['main']
chunkIds.forEach((chunkId) => {
//10. 通过JSONP请求获取到最新的模块代码
hotDownloadUpdateChunk(chunkId);
});
});
}
function hotDownloadUpdateChunk(chunkId) {
var script = document.createElement("script");
script.charset = "utf-8";
script.src = "/" + chunkId + "." + lastHash+ ".hot-update.js";
document.head.appendChild(script);
}
function hotDownloadManifest() {
var url = "/" + lastHash + ".hot-update.json";
return fetch(url).then(res => res.json()).catch(error=>{
console.log(error);});
}
//11. 补丁JS取回来后会调用`webpackHotUpdate`方法
window.webpackHotUpdate = (chunkId, moreModules) => {
for (let moduleId in moreModules) {
let oldModule = __webpack_require__.c[moduleId];//获取老模块
let {
parents } = oldModule;//父亲们 儿子们
var module = (__webpack_require__.c[moduleId] = {
i: moduleId,
exports: {
},
parents,
children,
hot: window.hotCreateModule(),
});
moreModules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
parents.forEach((parent) => {
let parentModule = __webpack_require__.c[parent];
parentModule.hot &&
parentModule.hot._acceptedDependencies[moduleId] &&
parentModule.hot._acceptedDependencies[moduleId]();
});
lastHash = currentHash;
}
};
socket.on("connect", onConnected);
window.hotCreateModule = () => {
var hot = {
_acceptedDependencies: {
}, //接收的依赖
accept: function (dep, callback) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback;
//hot._acceptedDependencies['./title']=callback
}
},
};
return hot;
}
其实热更新本质就是服务器和客户端通过socket.io通信,然后代码修改后:
1、本地开发服务器就会emit hash和ok事件,传递文件当前hash值给currentHash
2、浏览器端监听ok事件后就会用上一次的lastHash去获取对应改变的模块的描述文件,根据描述文件里面的依赖模块chunkId和lastHash,jsonp去获取新的模块js(xxx.hot-update.js)
3、新的模块获取后会执行webpackHotUpdate, 初始化改变的模块和修改模块的父模块的_acceptedDependencies里面的对应callback方法。
4、父模块相当于更新了数据。
5、记录下lastHash = currentHash; 下次更新又用lastHash去获取描述文件和更新后的文件,即可。
Socket.IO 由两部分组成:
一个服务端用于集成 (或挂载) 到 Node.JS HTTP 服务器: socket.io
一个加载到浏览器中的客户端: socket.io-client
开发环境下, socket.io 会自动提供客户端。正如我们所见,到目前为止,我们只需要安装一个模块:
npm install --save socket.io
这会安装模块并添加依赖到 package.json。在 index.js 文件中添加该模块:
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
io.on('connection', function(socket){
console.log('a user connected');
});
http.listen(3000, function(){
console.log('listening on *:3000');
});
我们通过传入 http (HTTP 服务器) 对象初始化了 socket.io 的一个实例。 然后监听 connection 事件来接收 sockets, 并将连接信息打印到控制台。
在 index.html 的 标签中添加如下内容:
// require('socket.io')(http)后就会有提供个js /socket.io/socket.io.js加载socket.io-client, 客户端就能监听消息了
<script src="/socket.io/socket.io.js">script>
<script>
var socket = io();
script>
这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
重新加载服务器和网站,你将看到控制台打印出 “a user connected”。
尝试打开多个标签页,可以看到多条信息:
socket.io 快速入门教程——聊天应用4
每个 socket 还会触发一个特殊的 disconnect 事件:
io.on('connection', function(socket){
console.log('a user connected');
socket.on('disconnect', function(){
console.log('user disconnected');
});
});
你可以多次刷新标签页来查看效果:
socket.io 快速入门教程——聊天应用5
触发事件
Socket.IO 的核心理念就是允许发送、接收任意事件和任意数据。任意能被编码为 JSON 的对象都可以用于传输。二进制数据 也是支持的。
这里的实现方案是,当用户输入消息时,服务器接收一个 chat message 事件。index.html 文件中的 script 部分现在应该内容如下:
<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
$(function () {
var socket = io();
$('form').submit(function(){
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
});
</script>
在 index.js 中打印出 chat message 事件:
io.on('connection', function(socket){
socket.on('chat message', function(msg){
console.log('message: ' + msg);
});
});