issue-blog icon indicating copy to clipboard operation
issue-blog copied to clipboard

webpack HMR 折腾记

Open SunShinewyf opened this issue 4 years ago • 4 comments

前言

什么是 HMR:全称 Hot Module Replacement,当你在更改并保存代码时,webpack 将会重新进行打包,并将新的包模块发送到浏览器端,浏览器用新的包模块替换旧的,从而可以在不刷新浏览器的前提下就可以到修改的功能。

HMR 是提升开发体验的一个关键点,最近接触的项目中没有配置这个,导致在开发过程中,每次都要手动刷新,体验不太好,所以折腾一下,发现居然还有这么多门道...

首先介绍一下热重载热更新的区别:

  • 热重载(live reload): 修改文件之后,webpack 自动编译,并强制刷新浏览器,属于全局(整个应用)刷新,相当于 window.location.reload(),
  • 热更新(HMR): 修改文件之后,webpack 自动编译,但是刷新时可以记住应用的状态,从而做到局部刷新。


举个🌰,比如你在某个页面打开了一个弹窗,并且修改了弹窗的内容,如果是热重载(live reload),那么整个页面重新刷新了,弹窗消失,你需要重新触发打开弹窗的操作。而热更新(HMR),刷新弹窗内容的同时,弹窗不关闭,直接可以看到更新后的效果。是不是明显后者开发体验更好?

回到我的踩坑过程,我前后试了三种方式,下面一一介绍一下配置和三者优劣:

live reload

这其实就是上面说的热重载(live reload) 的实现方式了,虽然有点简单粗暴,但是比"刀耕火种"时期的完全需要手动刷新要好点吧。

配置方式

使用的是 webpack-livereload-plugin

  • 安装依赖
npm install --save-dev webpack-livereload-plugin
  • 在 webpack.config.js 中添加 plugin 配置
// webpack.config.js

var LiveReloadPlugin = require('webpack-livereload-plugin');

module.exports = {
  plugins: [
    new LiveReloadPlugin({
    	port: "12345" // 配置 live-reload 的端口号
    })
  ]
}
  • 在页面文件中加入 live-reload 生成的 js 文件,如果使用了 HtmlWebpackPlguin,可以直接在模板中加入这行。
// 端口号为上面配置的
<script src="http://localhost:12345/livereload.js"></script>

这种方式有一定的代码侵入性,而且因为是热重载(live reload) 的方式,所以开发体验欠佳。

react-hot-loader

使用的是 react-hot-loader,这种方式使用的是热更新(HMR),会检测你的改动点并局部刷新,而且支持 React Hooks。比上面热重载的体验会好一点。

配置方式

  • 安装依赖
npm install --save-dev react-hot-loader
npm install --save-dev @hot-loader/react-dom // 如果要支持 React Hooks 需要安装这个
  • 设置 webpack
// webpack.config.js
module.exports = {
  // 1. 在入口添加 react-hot-loader/patch, 保证 react-hot-loader 在 react 和 react-dom 之前加载
  entry: ['react-hot-loader/patch', './src'],
  ...
  // 2. 设置 ts/tsx 的编译方式,使用 babel-loader 时在 options 中添加 plugins
   {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            cacheDirectory: true,
            babelrc: false,
            presets: [
              [
                "@babel/preset-env",
              ],
              "@babel/preset-typescript",
              "@babel/preset-react",
            ],
            plugins: [
              // plugin-proposal-decorators is only needed if you're using experimental decorators in TypeScript
              ["@babel/plugin-proposal-decorators", { legacy: true }],
              ["@babel/plugin-proposal-class-properties", { loose: true }],
              "react-hot-loader/babel", // 添加 react-hot-loader 插件
            ],
          },
        },
   }
   // ... other configuration options
   resolve: {
  	// 3. 设置 @hot-loader/react-dom,支持 React Hooks
    alias: {
      "react-dom": "@hot-loader/react-dom",
    },
  },
};
  • 入口组件使用 hot 包裹
// App.js
import { hot } from 'react-hot-loader/root';
const App = () => <div>Hello World!</div>;
export default hot(App);

这种方式虽然实现了我们的终极目标热更新(HMR),但是代码侵入性较大,需要包使用 hot 包裹入口组件,当是 multiple entries 的场景,就需要改动较多的业务代码。其次,如果使用 ts-loader(没有使用 babel-loader) 去编译 ts 文件的话,使用 react-hot-loader 会不成功,因为 react-hot-loader 依赖于 babel-loader,所以对于使用 ts-loader 的项目来说其实不太友好。

Fast ReFresh

使用的是 React Refresh Webpack Plugin,该插件是 React 官方提供的,将热重载(live reload) 和 HMR 进行了整合,而且相比于 react-hot-loader,容错能力更高。

配置方式

  • 安装依赖
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
  • 修改 webpack 配置
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const webpack = require('webpack');
// ... your other imports

const isDevelopment = process.env.NODE_ENV !== 'production';

module.exports = {
  mode: isDevelopment ? 'development' : 'production',
  module: {
    rules: [
      // ... other rules
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: [
          // ... other loaders
          {
            loader: require.resolve('babel-loader'),
            options: {
              // ... other options
              plugins: [
                // ... other plugins
                isDevelopment && require.resolve('react-refresh/babel'),
              ].filter(Boolean),
            },
          },
        ],
      },
    ],
  },
  plugins: [
    // ... other plugins
    isDevelopment && new webpack.HotModuleReplacementPlugin(),
    isDevelopment && new ReactRefreshWebpackPlugin(),
  ].filter(Boolean),
  // ... other configuration options
};

从配置可以看出,只需要改动 webpack 相关配置即可,对业务代码没有侵入性。为什么说它是将热重载(live reload) 和 HMR 进行了整合,主要是它在处理 HMR 时分为了三种情况:

  • 如果所编辑的模块仅导出了 React 组件,Fast Refresh 就只更新该模块的代码,并重新渲染对应的组件。此时该文件的所有修改都能生效,包括样式、渲染逻辑、事件处理、甚至一些副作用
  • 如果所编辑的模块导出的东西不只是 React 组件,Fast Refresh 将重新执行该模块以及所有依赖它的模块
  • 如果所编辑的文件被 React(组件)树之外的模块引用了,Fast Refresh 会降级成整个刷新(Live Reloading)

总结

三种方式对比如下:

方式 体验 侵入性 配置难度
live reload ✅✅✅
react-hot-reload ✅✅✅ ✅✅✅✅ ✅✅✅
fast refresh ✅✅✅

由上表可以得出,fast refresh 无疑是最优选择。而且 fast refresh 被视为 react-hot-loader 下一代的解决方案,除此之外它还与平台无关,既支持 React Native,也支持 Web,所以最终选择了它。不过它也有一些局限性,比如当设置了 externals,HMR 会失效,所以开发环境可先关闭 externals 配置。

参考资料

SunShinewyf avatar Sep 11 '20 12:09 SunShinewyf

有错别字,代码’倾入‘性

xuedafei avatar Sep 12 '20 15:09 xuedafei

@xuedafei 感谢指出,已修正

SunShinewyf avatar Sep 14 '20 01:09 SunShinewyf

👍

Natumsol avatar Sep 28 '20 12:09 Natumsol

👍

jacinyan avatar Jan 25 '22 08:01 jacinyan