一、前言

本文是 从零到亿系统性的树立前端构建常识系统✨ 中的第九篇。

在现如今,热更新早已成为前端基建中不可或缺的一环,它可以在不改写整个页面的情况下更新页面中的部分内容,然后提高开发功率,优化开发体会。

然而,在实际面试的过程中,笔者发现 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:用于将打包后的 cssjs 等代码刺进到 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 功能:

  1. 设置devServer.hot属性为 true
  2. 在代码中调用module.hot.accept接口,声明模块改变时履行的回调函数

当咱们履行 yarn start 指令时,webpack-cli 就会运用 webpack-dev-server 以 watch 形式 来协助咱们发动编译。

作用:当文件保存后,并没有改写浏览器就自动更新了。

Webpack深度进阶:两张图完全批注白热更新原理!

三、结构中运用

上面咱们运用 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只需两步:

  1. babel.config.js 中装备相关插件:
module.exports = {
  presets: ["@babel/preset-react"],
+ plugins: ["react-refresh/babel"],
};
  1. webpack.config.js 中装备相关插件:
const HtmlWebpackPlugin = require("html-webpack-plugin");
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {
  //省掉其他
  plugins: [
    //省掉其他
  + new ReactRefreshWebpackPlugin(),
  ],
};

作用:可以看到,输入框中的值并没有被重置,阐明热更新收效。

四、中心思维

Webpack深度进阶:两张图完全批注白热更新原理!

咱们先来看看热更新的中心包 webpack-dev-server 做了哪些事:

当咱们运转 webpack serve 后,webpack-dev-server 会先往客户端代码中添加了两个文件,这两个文件的意图:

  1. websocket 相关的代码,用来跟服务端通信
  2. 接收到最新代码后,更新客户端代码

接着会帮咱们发动两个服务:

  1. 一个本地 HTTP 服务:这个本地服务会给咱们提供编译之后的成果,之后浏览器经过端口恳求时,就会恳求本地服务中编译之后的内容,恳求完之后浏览器再去解析,默认端口号 8080。
  2. 一个 websocket 双向通信服务器:如果有新的模块发生改变,编译成功会以消息的方法告诉客户端,让客户端来恳求最新代码,并进行客户端的热更新。

然后会以 watch 形式 开始编译,每次编译完毕后会生成一个仅有的 hash 值。

watch 形式:运用监控形式开始发动 webpack 编译,在 webpack 的 watch 形式下,文件系统中某一个文件发生修正,webpack 监听到文件改变,依据装备文件对模块从头编译打包,每次编译都会发生一个仅有的 hash 值

以上便是 webpack-dev-server 的大致的思路,接下来咱们对照真实事例来看看。


先简略说一下代码块(chunk)模块(module)的概念:

chunk 便是若干 module 打成的包,一个 chunk 包括多个 module,一般来说最终会构成一个 file。而 module 便是一个个代码模块。

拿本项目举比如:src/index.jssrc/name.js 他们就组成了一个代码块(chunk),因为他们来自于同一个进口文件(entry: "src/index.js"),或者说他们被同一个进口文件所依靠,因此他们最终会被打包进一个代码块中。

而同样的, src/index.jssrc/name.js 他们自己也是单独的模块(module)。

在初次编译完结(发动项目)后,webpack 内部会生成一个 hash = h1,并将 hash = h1 经过 websocket 的方法告诉给客户端,客户端上有两个变量:lastHashcurrentHash

  • lastHash:上一次接收到的 hash
  • currentHash:这一次接收到的 hash

在接收到服务端告诉过来的 hash 时,客户端会进行保存:

lastHash= "之前的hash值"
currentHash = hash

如果是第一次接收到 hash 值,代表是第一次连接,则:

lastHash = currentHash = hash

Webpack深度进阶:两张图完全批注白热更新原理!

此刻,当源代码发生改变(name="不要秃头啊123"),webpack 对源文件从头进行编译,但是打包之后的成果并不会进行输出,而是将打包后的文件保留在内存中(事实上 webpack-dev-server 运用了一个库叫 memfs,它是 Webpack 官方自己写的),以此来提高功能。

在编译完结后生成 hash = h2 ,并将 hash = h2 发送给客户端,客户端接收到消息后,修正本身的变量:

//客户端代码
lastHash = h1
currentHash = h2

接着客户端经过 h1 向服务端恳求 json 数据main.hash1.json),意图是为了获得 改动的代码块(chunk)

Webpack深度进阶:两张图完全批注白热更新原理!

服务端接收到恳求后,将传过来的 h1 和 本身最新的 hash = h2 进行比照,找出 改动的代码块(chunk:main) 后回来给客户端。

客户端在收到呼应后,知道了哪些代码块(chunk:main)发生了改变,接着客户端会持续经过 h1 去恳求 改动代码块(main)中的变化模块(src/name.js) 代码(main.hash1.js):

Webpack深度进阶:两张图完全批注白热更新原理!

服务端接收到 js 恳求后,将传过来的 h1 和 本身最新的 hash = h2 再次进行比照,找出详细 改动的模块代码(src/name.js) 后回来给客户端。

最终,客户端拿到了改动模块的代码,从头去履行依靠该模块的模块(比方 src/name.js 被修正了,src/index.js 依靠 src/name.js,那就要从头履行 src/index.js 这个模块),到达更新的意图。

Webpack深度进阶:两张图完全批注白热更新原理!

这儿或许有同学要问了:为什么客户端会有两个 hash 值?

  • lastHash:上一次接收到的 hash
  • currentHash:这一次接收到的 hash

这么规划的用意:服务端不知道现在客户端的 hash 是多少,如果此刻又连接一个客户端(多窗口的场景)怎么办?

所以这儿需求客户端将上一次的 hash 回来给服务端,服务端经过比较后才回来改动的代码块。

五、总结

读完本文,你会发现热更新的中心便是经过 websocket 服务 进行客户端和服务端的同步改动,并没有咱们幻想中那么杂乱。

虽然这篇文章并没有对 Webpack HMR 进行源码等级的解析,很多细节方面也没过多讨论,但它真实起到的是一个抛砖引玉的作用,会大大减少你对 HMR 的生疏感。

如果对 webpack 感兴趣,想了解 Webpack HMR 更多的底层细节,相信阅览 webpack 源码将是一个不错的选择,也期望这篇文章可以对你阅览源码有所协助,这才是我真实的写作意图。

引荐阅览

  1. 从零到亿系统性的树立前端构建常识系统✨
  2. 我是怎么带领团队从零到一树立前端规范的?
  3. 线上崩了?一招教你快速定位问题!
  4. 【中级/高档前端】为什么我主张你一定要读一读 Tapable 源码?
  5. 前端工程化基石 — AST(笼统语法树)以及AST的广泛应用
  6. 二十张图片完全批注白Webpack规划理念,以看懂为意图
  7. 【万字长文|趣味图解】完全弄懂Webpack中的Loader机制
  8. 学会这些自定义hooks,让你摸鱼时间再翻一倍
  9. 浅析前端反常及降级处理
  10. 前端从头部署后,领导跟我说页面崩溃了…