【webpack】hmr学习笔记

前言

  • 以前并不是特别理解这个,只知道好像是通过websocket搞得,今天彻底搞懂它。

基本使用

  • 首先进行安装
cnpm i [email protected] [email protected] [email protected] mime html-webpack-plugin express socket.io -S
  • webpack.config.js
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: {
		contentBase: path.join(__dirname, "dist"),
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: "./src/index.html",
			filename: "index.html",
		}),
	],
};

index.js

var root = document.getElementById("root");
function render() {
	let title = require("./title").default;
	root.innerHTML = title;
}

render();

title.js

export default "hello";

index.html


<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Documenttitle>
	head>
	<body>
		<div id="root">div>
	body>
html>
  • 这样打开webpack dev server发现hello在页面上就ok。
  • 此时我们将title的字符串进行修改,会发现网页重新进行了刷新。
  • 下面启用热更新,热更新有以下几步:
  • 在devServer处加hot,
  • 加插件: new webpack.HotModuleReplacementPlugin()
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(),
	],
};
  • index.js加上这个:
var root = document.getElementById("root");
function render() {
	let title = require("./title").default;
	root.innerHTML = title;
}
render();
if(module.hot){
    module.hot.accept(['./title'],render)
}
  • 重启webpack server 此时 热更新就能生效了。

Socket.io

  • 专门去官网看了下,这东西很神奇,官网特意说了,这个必须要区别于标准的websocket服务器,它可能不是用websocket与客户端进行通信,可能会用长连接等方式进行。所以websocket客户端无法连接socket.io服务器,而socket.io服务器无法给websocket客户端通信。
  • 客户端方面,ie9以上内置socket.io直接使用就可以了。
  • 服务端方面,node一般自带,直接用。
  • 官网:https://socket.io/

手动实现

  • 这里webpack实现是用websocket 还是用socket.io还是用SSE都无所谓,只关心其实现原理。
  • 梳理下手动实现的过程:
  • 首先利用webpack的tabable勾入done编译完成阶段拿到完成的代码块hash,使用socketio发送给客户端,同时,输出文件使用内存文件系统存起来,这样文件改动后有新的hash会推送给客户端,客户端进行比较后会去ajax请求hot-update.json来获取改变的模块,然后通过再去使用jsonp方式请求chunkid.hash.hot-update.js来获得更改后的代码,更改后的代码会执行webpackHotUpdate,然后更新__webpack_require__c的模块,再让其重新执行,如果有子元素,把子元素跟着一起执行,最后把hash进行更新。

client.js

let socket = io("/");
class Emitter {
	constructor() {
		this.listeners = {};
	}
	on(type, listener) {
		this.listeners[type] = listener;
	}
	emit(type) {
		this.listeners[type] && this.listeners[type]();
	}
}
let hotEmitter = new Emitter();
const onConnected = () => {
	console.log("客户端连接成功");
};
socket.on("connect", onConnected);
let hotCurrentHash; //lastHash 上一次 hash值
let currentHash; //这一次的hash值
socket.on("hash", (hash) => {
	console.log(hash);
	currentHash = hash;
});
socket.on("ok", () => {
	reloadApp(true);
});
hotEmitter.on("webpackHotUpdate", () => {
	if (!hotCurrentHash || hotCurrentHash == currentHash) {
		return (hotCurrentHash = currentHash);
	}
	hotCheck();
});
function hotCheck() {
	hotDownloadManifest().then((update) => {
		let chunkIds = Object.keys(update.c);
		chunkIds.forEach((chunkId) => {
			hotDownloadUpdateChunk(chunkId);
		});
	});
}
function hotDownloadUpdateChunk(chunkId) {
	let script = document.createElement("script");
	script.charset = "utf-8";
	// /main.xxxx.hot-update.js
	script.src = "/" + chunkId + "." + hotCurrentHash + ".hot-update.js";
	document.head.appendChild(script);
}
//此方法用来去询问服务器到底这一次编译相对于上一次编译改变了哪些chunk?哪些模块?
function hotDownloadManifest() {
	return new Promise(function (resolve) {
		let request = new XMLHttpRequest();
		//hot-update.json文件里存放着从上一次编译到这一次编译
		let requestPath = "/" + hotCurrentHash + ".hot-update.json";
		request.open("GET", requestPath, true);
		request.onreadystatechange = function () {
			if (request.readyState === 4) {
				let update = JSON.parse(request.responseText);
				resolve(update);
			}
		};
		request.send();
	});
}
//当收到ok事件后,会重新刷新app
function reloadApp(hot) {
	if (hot) {
		//如果hot为true 走热更新的逻辑
		hotEmitter.emit("webpackHotUpdate");
	} else {
		//如果不支持热更新,则直接重新加载
		window.location.reload();
	}
}
window.hotCreateModule = function () {
	let hot = {
		_acceptedDependencies: {},
		dispose() {
			//销毁老的元素
		},
		accept: function (deps, callback) {
			for (let i = 0; i < deps.length; i++) {
				//hot._acceptedDependencies={'./title',render}
				hot._acceptedDependencies[deps[i]] = callback;
			}
		},
	};
	return hot;
};
//当客户端把最新的代码拉到浏览之后
window.webpackHotUpdate = function (chunkId, moreModules) {
	//循环新拉来的模块
	for (let moduleId in moreModules) {
		//从模块缓存中取到老的模块定义
		let oldModule = __webpack_require__.c[moduleId];
		//parents哪些模块引用这个模块 children这个模块引用了哪些模块
		//parents=['./src/index.js']
		let { parents, children } = oldModule;
		//更新缓存为最新代码 缓存进行更新
		let module = (__webpack_require__.c[moduleId] = {
			i: moduleId,
			l: false,
			exports: {},
			parents,
			children,
			hot: window.hotCreateModule(moduleId),
		});
		moreModules[moduleId].call(
			module.exports,
			module,
			module.exports,
			__webpack_require__
		);
		module.l = true; //状态变为加载就是给module.exports 赋值了
		parents.forEach((parent) => {
			let parentModule = __webpack_require__.c[parent];
			//_acceptedDependencies={'./src/title.js',render}
			parentModule &&
				parentModule.hot &&
				parentModule.hot._acceptedDependencies[moduleId] &&
				parentModule.hot._acceptedDependencies[moduleId]();
		});
		hotCurrentHash = currentHash;
	}
};

服务端

const path = require("path");
const express = require("express");
const mime = require("mime");
const webpack = require("webpack");
//1. 启动webpack-dev-server服务器
//2. 创建webpack实例
let config = require("./webpack.config");
let compiler = webpack(config);
class Server {
	constructor(compiler) {
		this.compiler = compiler;
		//4. 添加webpack的`done`事件回调,在编译完成后会向浏览器发送消息
		let lastHash;
		let sockets = [];
		compiler.hooks.done.tap("webpack-dev-server", (stats) => {
			lastHash = stats.hash;
			console.log(lastHash);
			sockets.forEach((socket) => {
				socket.emit("hash", stats.hash);
				socket.emit("ok");
			});
		});
		//5. 创建express应用app
		let app = new express();

		//6. 使用监控模式开始启动webpack编译,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
		compiler.watch(config.watchOptions || {}, (err) => {
			console.log("编译成功");
		});
		//7. 设置文件系统为内存文件系统
		const MemoryFileSystem = require("memory-fs");
		const fs = new MemoryFileSystem();
		compiler.outputFileSystem = fs;
		//8. 添加webpack-dev-middleware中间件
		const devMiddleware = (req, res, next) => {
			if (req.url === "/favicon.ico") {
				return res.sendStatus(404);
			}
			let filename = path.join(config.output.path, req.url.slice(1));
			console.error(filename);
			if (fs.statSync(filename).isFile()) {
				let content = fs.readFileSync(filename);
				res.header("Content-Type", mime.getType(filename));
				res.send(content);
			} else {
				next();
			}
		};
		app.use(devMiddleware);
		//8. 创建http服务器并启动服务
		this.server = require("http").createServer(app);
		// 10. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,浏览器端根据这些`socket`消息进行不同的操作。当然服务端传递的最主要信息还是新模块的`hash`值,后面的步骤根据这一`hash`值来进行模块热替换
		let io = require("socket.io")(this.server);
		io.on("connection", (socket) => {
			sockets.push(socket);
			if (lastHash) {
				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(8000);

你可能感兴趣的:(webpack,websocket)