一、前言
本文是 从零到亿系统性的树立前端构建常识系统✨ 中的第九篇。
在现如今,热更新早已成为前端基建中不可或缺的一环,它可以在不改写整个页面的情况下更新页面中的部分内容,然后提高开发功率,优化开发体会。
然而,在实际面试的过程中,笔者发现 80% 的人并不清楚这其中的规划原理,只有很少一部分人可以表达清楚,原因我以为或许有以下几点:
- 工作中不是必要:由于热更新通常是经过运用东西或结构来完结的,以为热更新的原理并不重要,只需求运用即可
- 学习成本高:热更新的原理涉及到较高档的技术常识,原理过于杂乱
- ……
总归,热更新对咱们来说,就像是一块难啃的骨头。但在前端基建岗位中,它又是必备的常识系统之一。
因此,本文将一改之前的文风,全文不会手写任何原理相关的代码,尽量经过图文的方法去讲解整个运作流程,旨在协助大家了解其中的规划思维,以看懂为意图。
回到正文:
在 HMR 之前,应用的加载、更新都是一种页面等级的操作,即使仅仅单个代码文件发生改动,都需求改写整个页面,才能将最新代码映射到浏览器上,这会丢掉之前在页面履行过的所有交互与状况,例如:
- 关于杂乱表单场景,这意味着你或许需求从头填充十分多字段信息
- 弹框消失,你必须从头履行交互动作才会从头弹出
再小的改动,例如更新字体大小,改动补白信息都会需求整个页面从头加载履行,全体开发功率偏低。
而引入 HMR 后,虽然无法掩盖所有场景,但大多数小改动都可以经过模块热替换方法更新到页面上,然后确保接连、顺畅的开发调试体会,极大提高开发功率。
文中所涉及到的代码均放到个人 github 库房中。
二、基本运用
在正式讲原理之前,先简略过一下热更新的运用方法,照顾一下不太熟悉的同学。满足熟悉的同学可直接定位到第四节 —— 中心思维。
初始化项目:
npm init //初始化一个项目
yarn add webpack webpack-cli webpack-dev-server html-webpack-plugin//装置项目依靠
简略说一下这几个依靠:
- webpack:这个不用说,中心库
-
webpack-cli:首要用来处理指令行中的参数,并发动
webpack
编译 - webpack-dev-server:提供开发服务器,用来支撑热更新
-
html-webpack-plugin:用于将打包后的
css
、js
等代码刺进到html
模版中
装置完依靠后,依据以下目录结构来添加对应的目录和文件:
├── node_modules
├── package.json
├── index.html # html模版代码
├── webpack.config.js #装备文件
└── src # 源码目录
|── index.js
└── name.js
webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development", //开发形式
entry: "./src/index.js", //进口
devServer: {
hot: true, //敞开热更新,这个是要害!!!
port: 8000, //设置端口号
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html", //将打包后的代码刺进到html模版中
}),
],
};
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>hmr</title>
</head>
<body>
<div id="root"></div>
<!-- 可以在里面输入一些东西,便利咱们观察热更新的作用 -->
<input />
</body>
</html>
src/index.js
import name from "./name";
const render = () => {
const rootDom = document.getElementById("root");
rootDom.innerText = name;
};
render();
//要完结热更新,这段代码并不可少,描绘当模块被更新后做什么
if (module.hot) {
module.hot.accept("./name", function () {
console.log("name模块发生改变,处理热更新逻辑");
render();
});
}
src/name.js
const name = "不要秃头啊";
export default name;
package.json
"scripts": {
"start": "webpack serve"
},
在 Webpack 生态下,只需两步即可发动 HMR
功能:
- 设置
devServer.hot
属性为 true - 在代码中调用
module.hot.accept
接口,声明模块改变时履行的回调函数
当咱们履行 yarn start
指令时,webpack-cli
就会运用 webpack-dev-server
以 watch 形式 来协助咱们发动编译。
作用:当文件保存后,并没有改写浏览器就自动更新了。
三、结构中运用
上面咱们运用 HMR 有一个很大的痛点:在开发项目时,咱们经常需求手动去修正 module.hot.accpet
相关的函数,这就比较反人类了。
不过幸好,在社区中现已针对这些有很成熟的解决方案了:
- 比方 Vue 开发中,咱们运用 vue-loader,此 loader 支撑 Vue 组件的 HMR,提供开箱即用的体会
- 比方 React 开发中,有 React Hot Loader,实时调整 React 组件(目前 React 官方现已弃用了,改成运用 react-refresh)
这儿以 React 举例,先快速树立 React 运转环境:
3.1、树立React开发环境
装置 React 和相关依靠:
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@babel/core": "^7.20.5",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^9.1.0",
简略介绍一下这几个库:
- react:中心库,不用多说
- react-dom:中心库,不用多说
- @babel/core:Babel 编译器的中心
- @babel/preset-react:所有 React 插件的 Babel 预设
- babel-loader:在 Webpack 中转译 JavaScript 文件
在根目录下新建 babel.config.js 文件,并装备:
module.exports = {
presets: ["@babel/preset-react"],
};
修正 webpack.config.js 下装备:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {
//省掉其他
entry: "./src/index.jsx", //进口
+ module: {
+ rules: [
+ {
+ test: /.jsx?$/i,
+ exclude: /node_modules/,
+ use: "babel-loader",
+ },
+ ],
+ },
};
进口文件 src/index.jsx :
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./app.jsx";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
src/app.jsx :
import React from "react";
export default function App() {
return (
<div>
作者:不要秃头啊
<input />
</div>
);
}
3.2、React中启用HMR
装置热更新相关依靠:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
or
yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
react-refresh 是专门用来做 React 热更新的,Redux 作者 Dan 还曾专门讲解过怎么运用react-refresh
去替代之前的 React Hot Loader,有需求的自行查阅:github.com/facebook/re… 。
@pmmmwh/react-refresh-webpack-plugin 则是 react-refresh
在 Webpack 中的插件。
在React中启用HMR只需两步:
- babel.config.js 中装备相关插件:
module.exports = {
presets: ["@babel/preset-react"],
+ plugins: ["react-refresh/babel"],
};
- 在 webpack.config.js 中装备相关插件:
const HtmlWebpackPlugin = require("html-webpack-plugin");
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {
//省掉其他
plugins: [
//省掉其他
+ new ReactRefreshWebpackPlugin(),
],
};
作用:可以看到,输入框中的值并没有被重置,阐明热更新收效。
四、中心思维
咱们先来看看热更新的中心包 webpack-dev-server 做了哪些事:
当咱们运转 webpack serve 后,webpack-dev-server 会先往客户端代码中添加了两个文件,这两个文件的意图:
- websocket 相关的代码,用来跟服务端通信
- 接收到最新代码后,更新客户端代码
接着会帮咱们发动两个服务:
- 一个本地
HTTP 服务
:这个本地服务会给咱们提供编译之后的成果,之后浏览器经过端口恳求时,就会恳求本地服务中编译之后的内容,恳求完之后浏览器再去解析,默认端口号 8080。 - 一个
websocket 双向通信服务器
:如果有新的模块发生改变,编译成功会以消息的方法告诉客户端,让客户端来恳求最新代码,并进行客户端的热更新。
然后会以 watch 形式 开始编译,每次编译完毕后会生成一个仅有的 hash 值。
watch 形式:运用监控形式开始发动 webpack 编译,在 webpack 的 watch 形式下,文件系统中某一个文件发生修正,webpack 监听到文件改变,依据装备文件对模块从头编译打包,每次编译都会发生一个仅有的 hash 值
以上便是 webpack-dev-server 的大致的思路,接下来咱们对照真实事例来看看。
先简略说一下
代码块(chunk)
和模块(module)
的概念:
chunk
便是若干module
打成的包,一个chunk
包括多个module
,一般来说最终会构成一个 file。而module
便是一个个代码模块。拿本项目举比如:src/index.js 和 src/name.js 他们就组成了一个代码块(
chunk
),因为他们来自于同一个进口文件(entry: "src/index.js"
),或者说他们被同一个进口文件所依靠,因此他们最终会被打包进一个代码块中。而同样的, src/index.js、src/name.js 他们自己也是单独的模块(
module
)。
在初次编译完结(发动项目)后,webpack 内部会生成一个 hash = h1
,并将 hash = h1
经过 websocket 的方法告诉给客户端,客户端上有两个变量:lastHash
、currentHash
。
- lastHash:上一次接收到的 hash
- currentHash:这一次接收到的 hash
在接收到服务端告诉过来的 hash 时,客户端会进行保存:
lastHash= "之前的hash值"
currentHash = hash
如果是第一次接收到 hash 值,代表是第一次连接,则:
lastHash = currentHash = hash
此刻,当源代码发生改变(name="不要秃头啊123"
),webpack
对源文件从头进行编译,但是打包之后的成果并不会进行输出,而是将打包后的文件保留在内存中(事实上 webpack-dev-server 运用了一个库叫 memfs,它是 Webpack 官方自己写的),以此来提高功能。
在编译完结后生成 hash = h2
,并将 hash = h2
发送给客户端,客户端接收到消息后,修正本身的变量:
//客户端代码
lastHash = h1
currentHash = h2
接着客户端经过 h1
向服务端恳求 json 数据
(main.hash1.json),意图是为了获得 改动的代码块(chunk):
服务端接收到恳求后,将传过来的 h1
和 本身最新的 hash = h2
进行比照,找出 改动的代码块(chunk:main) 后回来给客户端。
客户端在收到呼应后,知道了哪些代码块(chunk:main)发生了改变,接着客户端会持续经过 h1
去恳求 改动代码块(main)中的变化模块(src/name.js) 代码(main.hash1.js):
服务端接收到 js 恳求后,将传过来的 h1
和 本身最新的 hash = h2
再次进行比照,找出详细 改动的模块代码(src/name.js) 后回来给客户端。
最终,客户端拿到了改动模块的代码,从头去履行依靠该模块的模块
(比方 src/name.js 被修正了,src/index.js 依靠 src/name.js,那就要从头履行 src/index.js 这个模块),到达更新的意图。
这儿或许有同学要问了:为什么客户端会有两个 hash 值?
- lastHash:上一次接收到的 hash
- currentHash:这一次接收到的 hash
这么规划的用意:服务端不知道现在客户端的 hash 是多少,如果此刻又连接一个客户端(多窗口的场景)怎么办?
所以这儿需求客户端将上一次的 hash 回来给服务端,服务端经过比较后才回来改动的代码块。
五、总结
读完本文,你会发现热更新
的中心便是经过 websocket 服务 进行客户端和服务端的同步改动,并没有咱们幻想中那么杂乱。
虽然这篇文章并没有对 Webpack HMR
进行源码等级的解析,很多细节方面也没过多讨论,但它真实起到的是一个抛砖引玉的作用,会大大减少你对 HMR 的生疏感。
如果对 webpack 感兴趣,想了解 Webpack HMR 更多的底层细节,相信阅览 webpack 源码将是一个不错的选择,也期望这篇文章可以对你阅览源码有所协助,这才是我真实的写作意图。
引荐阅览
- 从零到亿系统性的树立前端构建常识系统✨
- 我是怎么带领团队从零到一树立前端规范的?
- 线上崩了?一招教你快速定位问题!
- 【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?
- 前端工程化基石 — AST(笼统语法树)以及AST的广泛应用
- 二十张图片完全批注白Webpack规划理念,以看懂为意图
- 【万字长文|趣味图解】完全弄懂Webpack中的Loader机制
- 学会这些自定义hooks,让你摸鱼时间再翻一倍
- 浅析前端反常及降级处理
- 前端从头部署后,领导跟我说页面崩溃了…