Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack HMR 折腾记 #52

Open
SunShinewyf opened this issue Sep 11, 2020 · 4 comments
Open

webpack HMR 折腾记 #52

SunShinewyf opened this issue Sep 11, 2020 · 4 comments

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Sep 11, 2020

前言

什么是 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 配置。

参考资料

@xuedafei
Copy link

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

@SunShinewyf
Copy link
Owner Author

@xuedafei 感谢指出,已修正

@Natumsol
Copy link

👍

1 similar comment
@jacinyan
Copy link

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants