предисловие
В этой статье используется исходный код WebPack-Dev-Server, реализуется HMR горячего обновления WebPack с нуля, глубоко понимается механизм реализации WebPack-Dev-Server, WebPack-Middleware и полностью понимается их принципы. интервью очень беглое, эту часть можно носить перед созданием процесса строительных лесов. Зная это, зная это, даже следующий уровень.
Теплое напоминание ❤️~ Длина большая, рекомендуется сохранить на компьютер для лучшего использования.
Ноль, что такое HMR
1. Концепция
Горячая замена модуля означает, что когда мы изменяем и сохраняем код, веб-пакет переупаковывает код и отправляет новый модуль в браузер.Браузер заменяет старый модуль новым модулем для достижения обновления страницы на основе обновления браузера.
2. Преимущества
относительноlive reload
Схема обновления страницы, преимущество HMR в том, что он может сохранять состояние приложения и повышать эффективность разработки
3. Тогда используйте его
./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
4. Эффект изображения
Когда мы введем 123 в поле ввода и обновим код в content.js в это время, мы обнаружим, что hello world!!!! становится hello world, но значение поля ввода все еще сохраняется, что и является значением HMR, сохранение состояния при обновлении страницы
5. Понимание концепций блока и модуля
Чанк — это пакет, состоящий из нескольких модулей.Чанк должен включать в себя несколько модулей.В общем случае он в конечном итоге образует файл. Для ресурсов, отличных от js, webpack преобразует его в модуль через различные загрузчики, этот модуль будет упакован в чанк и не будет образовывать отдельный чанк.
1. Компиляция веб-пакета
Webpack watch: Используйте режим мониторинга, чтобы начать компиляцию веб-пакета.Когда файл в файловой системе изменяется, веб-пакет отслеживает изменение файла, перекомпилирует и упаковывает модуль в соответствии с файлом конфигурации., каждая компиляция производитуникальное хэш-значение,
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 - Да, эти два файла генерируются не вебпаком, а этим плагином.Вы можете удалить HotModuleReplacementPlugin в файле конфигурации и попробовать
-
Внедрить код времени выполнения HMR в файл фрагмента: Основная логика нашего клиента горячего обновления (
拉取新模块代码
,执行新模块代码
,执行accept的回调实现局部更新
) все, что этот плагин вводит функции в наши файлы чанков, а не webpack-dev-server, webpack-dev-server просто вызывает эти функции
2. Разберитесь с файлом пакета
Следующий код представляет собой фрагмент, сгенерированный компиляцией HotModuleReplacementPlugin, внедряет код среды выполнения HMR, запускает службу 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)
главный вход -
Когда браузер выполняет этот фрагмент, при выполнении каждого модуляОбъект модуля будет передан для каждого модуля, структура следующая, иПоместите этот объект модуля в кешinstalledModulesв; мы можем пройти
__webpack_require__.c拿到这个模块缓存对象
var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {}, hot: hotCreateModule(moduleId), parents: [], children: [] };
-
hotCreateRequire поможет нам присвоить значения родителям и детям модуля модуля
-
Затем посмотрите на свойство 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, представляет собой локальную логику обновления, которая выполняется при изменении модуля ./content.js.
if (module.hot) {
module.hot.accept(["./content.js"], render);
}
2. Принцип принятия
Почему мы пишем толькоmodule.hot.accept(["./content.js"], render);
Чтобы добиться горячего обновления, мы должны начать с принципа функции принятия.Давайте посмотрим на 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),
};
Да, принятьhot._acceptedDependencies
Хранилище объектов Локальная функция обратного вызова обновления, Когда будет использоваться _acceptedDependencies? (Когда файл модуля изменяется, мы вызываем обратный вызов, собранный с помощью acceptDependencies.)
3. Посмотрите еще раз на accept
// 再看下面这段代码是不是有点明白了
if (module.hot) {
module.hot.accept(["./content.js"], render);
// 等价于module.hot._acceptedDependencies["./content.js"] = render
// 没错,他就是将模块改变时,要做的事进行了搜集,搜集到_acceptedDependencies中
// 以便当content.js模块改变时,他的父模块index.js通过_acceptedDependencies知道要干什么
}
Во-вторых, общий процесс
1. Весь процесс разделен на клиентский и серверный
2. Пройтиwebsocket
Установить связь между браузером и сервером
3. Сервер в основном разделен на четыре ключевые точки
- Создайте экземпляр компилятора через веб-пакет, и веб-пакет компилируется в режиме просмотра.
- Экземпляр компилятора: отслеживать изменения локальных файлов, автоматически компилировать изменения файлов, компилировать выходные данные
- Измените атрибут записи в конфигурации: добавьте lib/client/index.js, lib/client/hot/dev-server.js в файл фрагмента упакованного вывода.
- Зарегистрируйте событие в хуке компилятора.hooks.done (срабатывает после компиляции веб-пакета): оно будет передано клиенту.
hash
а такжеok
мероприятие
- Вызываем webpack-dev-middleware: запускаем компиляцию, устанавливаем файл в файловую систему памяти, а в нем есть middleware, который отвечает за отдачу скомпилированного файла
- Создайте статический сервер веб-сервера: разрешите браузеру запрашивать скомпилированные статические ресурсы.
- Создайте службу веб-сокетов: установите двустороннюю связь между локальной службой и браузером; всякий раз, когда происходит новая компиляция, немедленно сообщайте браузеру о выполнении логики горячего обновления.
4. Клиент в основном делится на два ключевых момента
- Создайте клиент веб-сокета для подключения к серверу веб-сокета, и клиент веб-сокета прослушивает
hash
а такжеok
мероприятие - Основная логика реализации клиента горячего обновления, браузер получит сообщение, отправленное сервером, если требуется горячее обновление, браузер инициирует http-запрос к серверу, чтобы получить анализ ресурсов нового модуля и локально обновить страницу (это что делает для нас HotModuleReplacementPlugin.Да, он внедрил в чанк исполняемый код HMR, но я покажу вам, как это реализовать
HMR runtime
)
5. Схема
В-третьих, реализация исходного кода
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",
}
В-четвертых, реализация сервера
- /src/myHMR-webpack-dev-server.js запись сервера горячего обновления
- Класс сервера /src/lib/server/Server.js — это основная логика сервера горячего обновления.
- /src/lib/server/updateCompiler.js Изменить запись, добавить /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. Общий список серверов
// /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. Измените атрибут записи веб-пакета, добавьте клиентский файл веб-сокета и дайте ему скомпилироваться в фрагмент.
Перед компиляцией веб-пакета вызовитеupdateCompiler(compiler)
метод, этот метод очень важен, он скрытно вставит в наш чанк два файла,lib/client/client.js
а такжеlib/client/hot-dev-server.js
Для чего эти два файла? Мы сказали, что используя websocket для достижения двусторонней связи, наш сервер создаст сервер websocket (на шаге 9), и каждый раз, когда код будет изменен, он будет перекомпилирован для создания нового скомпилированного файла, после чего наш сервер websocket уведомит браузер., ты приходишь и дергаешь новый код
Так есть ли клиент websocket, который реализует логику общения с сервером? Таким образом, webpack-dev-server предоставляет нам код на стороне клиента, то есть два вышеуказанных файла, и устанавливает для нас программу-шпион, чтобы мы могли незаметно получать новый код и добиваться горячих обновлений.
Зачем разбивать на два файла? Конечно это разделение модулей.Нехорошо писать балабалу одним куском.В клиентской части реализации я подробнее остановлюсь на том, что делают эти два файла.
// /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. Добавьте веб-пакетdone
обратный вызов события
Мы должны зарегистрировать событие на крючке, выполненном в компиляторе, и это событие в основном делает одно. Всякий раз, когда новая компиляция завершена, отправьте сообщение всем клиентам Websocket, передавать два события, уведомить браузер, чтобы вытащить код LA
Браузер будет прослушивать эти два события, а браузер потянет его上次编译生成的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. Создайте приложение экспресс-экземпляра
setupApp() {
this.app = new express();
}
6. Добавьте промежуточное ПО webpack-dev-middleware
1. О webpack-dev-server и webpack-dev-middleware
- Ядром webpack-dev-server является выполнение подготовительной работы (изменение записи, отслеживание событий webpack done и т. д.), создание сервера веб-сервера и сервера веб-сокетов, чтобы позволить браузеру и серверу установить связь.
- Операции, связанные с компиляцией и компиляцией файлов, извлекаются в webpack-dev-middleware
2. WebPack-Dev-Madeware в основном делает три вещи (здесь мы реализуем свою собственную логику)
- Следите за локальными файлами и запускайте компиляцию веб-пакета, используйте режим мониторинга для запуска компиляции веб-пакета В режиме наблюдения веб-пакета, когда файл в файловой системе изменяется, веб-пакет отслеживает изменения файла, перекомпилирует и упаковывает модуль в соответствии с файлом конфигурации;
- Установите файловую систему в файловую систему в памяти (пусть компиляция выводит в память)
- Реализовано экспресс промежуточное ПО, возвращающее скомпилированный файл
// /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. Используйте в приложении промежуточное ПО, возвращаемое 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. Создайте сервер веб-сервера
Разрешить браузеру запрашивать статические ресурсы, скомпилированные webpack
Здесь используется экспресс и нативный http, у вас могут возникнуть вопросы? Почему бы просто не использовать экспресс или http?
- Мы не используем экспресс напрямую, потому что мы не можем получить сервер Мы можем посмотреть исходный код экспресса Зачем нам нужен этот сервер, потому что мы хотим использовать его в сокете;
- Если вы не используете http напрямую, вы должны знать, что нативная логика написания http не повредит; мы просто написали здесь простую статическую логику обработки, так что мы ничего не видим, но в исходный код, вот только ядро Logic выбирает и реализует
- Так как у обоих есть недочеты, давайте их объединим.Сделаем сервис с нативным http и получим сервер.Логику запросов этого сервера должен обрабатывать экспресс.
this.server = http.createServer(app);
Одна строка кода идеальна
// /src/lib/server/Server.js
createServer() {
this.server = http.createServer(this.app);
}
9. Создайте сервер веб-сокетов
Используйте socket.js, чтобы установить длинное соединение через веб-сокет между браузером и сервером.
// /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);
}
-
发射webpackHotUpdate事件
-
webpackHotUpdate
hotCheck
// /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)
Измените конфигурацию входа наwebpack-dev-server/client/index.js?http://localhost:8080
а такжеwebpack/hot/dev-server.js
Соберите их в куски вместе, затем раскройте истинное лицо Hot / Deverserver.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 (то есть hotCheck, когда запущена среда выполнения HMR). Код, вставленный 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
Отличие с исходным кодом: в исходном коде вызывается метод проверки, а в методе проверки вызывается метод module.hot.check, то есть метод hotCheck, и в проверке также выполняется некоторый вывод лога. Здесь прямо пишем основной метод 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
// 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();
})
}
7. Pull patch code - обновленный код модуля 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
метод
Отличие от исходного кода: В исходном коде будет вызываться метод hotAddUpdateChunk webpackHotUpdate для динамического обновления кода модуля (замена старого модуля на новый модуль), а затем для горячего обновления будет вызываться метод 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]()
});
})
}
6. webpack-dev-server, webpack-hot-middleware, webpack-dev-middleware
Использование webpack-dev-middleware, webpack-hot-middleware, экспресс-доставка HMR Demo
1.Webpack-dev-middleware
-
Позвольте веб-пакету скомпилироваться в режиме просмотра;
-
Измените файловую систему на файловую систему в памяти, которая не будет записывать упакованные ресурсы на диск, а будет обрабатывать их в памяти;
-
Промежуточное ПО отвечает за возврат скомпилированного файла;
2. Webpack-горячее промежуточное ПО:
Обеспечивает механизм связи между браузером и сервером Webpack, подписывается и получает обновления от сервера Webpack на стороне браузера, а затем использует HMR API webpack для выполнения этих изменений.
1. Сервер
-
События компилятора.hooks.done монитора сервера;
-
Через SSE сервер компилирует и отправляет клиенту события сборки, построения и синхронизации;
webpack-dev-middleware через
EventSource
также называемыйserver-sent-event(SSE)
Для достижения односторонних push-сообщений от сервера к клиенту. Благодаря обнаружению сердцебиения, чтобы определить, жив ли клиент, это 💓 SSE.Обнаружение сердцебиения, установитеsetIntervalКаждые 10 с отправляет время клиенту
2. Клиент
-
Тот же код клиента необходимо добавить в атрибут записи config,
// /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, слушать события сборки, сборки, синхронизации, функция обратного вызова будет обновлена кодом времени выполнения HotModuleReplacementPlugin;
3. Резюме
- Фактически, когда мы внедрили горячее обновление webpack-dev-server, мы уже реализовали функции webpack-hot-middleware.
- Их самая большая разница заключается в средствах связи между браузером и сервером.
webpack-dev-server
используетwebsocket
,webpack-hot-middleware
используетeventSource
; и имена событий коммуникационного процесса отличаются, webpack-dev-server использует хэш и ok, webpack-dev-middleware находится в сборке (в разработке, горячее обновление не будет запускаться) и синхронизации (чтобы определить, следует ли запускать горячее процесс обновления)
3. webpack-dev-server
Webpack-Dev-Server — это встроенное ПО Webpack-dev-middleware и сервер Express, использующийwebsocket
заменятьeventSource
Реализовать логику webpack-hot-middleware
4. Разница
В: Почему у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 | Создать экземпляр сервера | webpack-dev-server |
3 | Изменить атрибут записи конфигурации |
Server updateCompiler |
записьДобавить dev-сервер/клиент/index.js | addEntries | |
записьДобавить webpack/hot/dev-server.js | addEntries | |
4 | Прослушивание события готовности веб-пакета | Server |
После завершения компиляции сообщение отправляется клиенту веб-сокета.Самая важная информация — новый модуль.hash значение, следующие шаги основаны на этомhash стоимость горячей замены модуля |
Server | |
5 | Создать приложение экспресс-экземпляра | Server |
6 | Используйте ПО webpack-dev-middlerware | Server |
Запустить компиляцию веб-пакета в режиме наблюдения, файл в файловой системе изменен, веб-пакет отслеживает изменение файла, перекомпилирует и упаковывает модуль в соответствии с файлом конфигурации. | webpack-dev-middleware | |
Установите файловую систему в файловую систему в памяти | webpack-dev-middleware | |
Возвращает промежуточное ПО для возврата сгенерированного файла | webpack-dev-middleware | |
7 | Промежуточное ПО, возвращаемое WebPack-dev-middlerware в приложении | Server |
8 | Создайте сервер веб-сервера и запустите службу | Server |
9 | Используйте sockjs для установки длинного соединения через веб-сокет между браузером и сервером. | Server |
Создайте сервер сокетов и прослушивайте события подключения | SockJSServer |
2. Клиент
шаг | Функция | Исходная ссылка |
---|---|---|
1 | Подключиться к веб-серверу | client/index.js |
2 | клиент websocket прослушивает события | client/index.js |
Прослушайте хэш-событие и сохраните хэш-значение | client/index.js | |
Прослушайте событие ok и выполнитеreloadApp способ обновления |
client/index.js | |
3 | Вызовите reloadApp, в reloadApp рассудят, поддерживается ли горячее обновление, если поддерживается, запускаемwebpackHotUpdate событие, если оно не поддерживается, обновить браузер напрямую |
client/index.js |
Запустить событие webpackHotUpdate в reloadApp | reloadApp | |
4 | существуетwebpack/hot/dev-server.js Будет прослушивать событие webpackHotUpdate, |
webpack/hot/dev-server.js |
Затем выполните метод check(), чтобы проверить | webpack/hot/dev-server.js | |
будет вызываться в методе проверкиmodule.hot.check метод |
webpack/hot/dev-server.js | |
5 | module.hot.check также является hotCheck | HotModuleReplacement.runtime |
6 | Вызов HotDownloadManifest` отправляет запрос AJAX на сторону сервера, сервер возвращает файл манифеста (Lasthash.hot-update.json), который содержит имя чанка этого скомпилированного значения хэша и модуль обновления. |
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.
пиши до конца
Наконец-то готово, дерзайте, вы лучшие, мне это нравится для вас 👍 (может быть, еще несколько кусочков~)Я не знаю, является ли он исчерпывающим, если есть какой-либо недостаток, пожалуйста, поправьте меня.
Первая статья, если она была для вас полезной и вдохновляющей, ставлю вам небольшой лайк ❤️~зарядите меня🔋