前言
此前讲解 react-router、redux、react-redux、redux-saga 所涉及的内容较多,篇幅也较长。终于可以介绍 HMR 模块热更新。
其实此前已经介绍过了,但今天就结合 React 搭建更友好的热更新效果。
正文
一、HMR 实现
此前是怎么做的呢?
当然这种方式是没有针对使用 React 做优化处理的。
// webpack.config.js modules.exports = { mode: 'develop', devServer: { // 需要注意的是,要完全启用 HMR,需要 webpack.HotModuleReplacementPlugin hot: true }, optimization: { // 告知 webpack 使用可读取模块标识符,来帮助更好地调试。开发模式默认开启。简单来说,开启时你看到的是一个具体的模块名称,而不是一个数字 id。 namedModules: true }, plugins: [ // 通过它启用 HMR,那么它的接口将被暴露在 module.hot 属性下面。 new webpack.HotModuleReplacementPlugin() ], module: { rules: [ { // 样式热更新,借助于 style-loader,其实幕后使用了 module.hot.accept test: /\.css/, use: ['style-loader', 'css-loader'] } ] } }
我们还需在入口文件添加 module.hot.accpet()
方法。
// index.js import React from 'react' import { render } from 'react-dom' import '../styles/style.css' import Root from './Root' // 最简单的 React 示例 const rootElem = document.getElementById('app') render(<Root />, rootElem) // 通常,先检查 HotModuleReplacementPlugin 暴露的接口是否可访问,然后再开始使用它。 if (module.hot) { // accept 方法接受给定的依赖模块的更新,并触发一个回调函数来对这些更新做出响应。 module.hot.accept('./Root', () => { import('./Root.js').then(module => { const NextRoot = module.default render(<NextRoot/>, rootElem) }) }) }
需要注意的是:
原先的
在 webpack 4 中已废弃,取代它的是new webpack.NameModulesPlugin()
optimization.nameModules
。开发模式下默认开启,生产模式下,默认关闭。关于开启与禁用,最直观的区别如下图。
尝试随便修改一下 Home 组件,看到控制台的输出(如下图),两者区别不同。开启时,能看到具体涉及更新的模块有哪些,而关闭状态则是只能看到一个数字 id。
关闭状态
开启状态
二、HMR 搭配 React 的不足
上面的方式,如果搭配 React 使用的话,其实还不够友好。
用一个最简单的例子说明问题
// 一个非常简单的有状态组件 import React, { Component } from 'react' class HMRDemo extends Component { constructor(props) { super(props) this.state = { count: 0 } } render() { return ( <div> <h3>HMR Demo Component!</h3> <h5>计数器:{this.state.count}</h5> <button onClick={() => { this.setState({ count: ++this.state.count }) }}>add</button> </div> ) } } export default HMRDemo
这时候我们点击按钮,然后组件状态 count
自然变成了 1
,这个没问题。接着,我们随意增加一个标签元素,然后页面自然会热更新,但是我们看到,count
又变成了 0
,组件状态丢失了,如下图:
我们享受着 HMR 给我们开发带来的便利,但同时我们又不想丢失 Component State 组件状态,怎么做呢?
为了解决这个问题,React Hot Loader 出现了。
三、React Hot Loader
先看一下官方指南的一段原话:
React-Hot-Loader is expected to be replaced by React Fast Refresh. Please remove React-Hot-Loader if Fast Refresh is currently supported on your environment.
大概的意思是,react-hot-loader 将会被 React Fast Refresh 取代。如果您当前的环境支持的话,请移除 react-hot-loader。
*至于 React Fast Refresh 是什么?如何使用?这里不展开述说,可以看一下其他人写的一篇文章。
继续介绍 react-hot-loader,还需要在上面的基础上,添加以下配置。
- 安装依赖包
$ yarn add --dev react-hot-loader@4.12.19 # 下面步骤用到 $ yarn add --dev @hot-loader@react-dom@16.12.0
@hot-loader/react-dom
是在react-dom
相同版本的基础上,添加了一些支持热更新的补丁。所以需要安装与react-dom
一致的版本。
- 添加
react-hot-loader/babel
到.babelrc
配置中。
// .babelrc { "plugins": [ "react-hot-loader/babel" ] }
- 将根组件标记为热导出(hot-exported)
import React from 'react' import { Provider } from 'react-redux' import { hot } from 'react-hot-loader/root' import App from './pages/App' import store from './store' const Root = () => { return ( <Provider store={store}> <App /> </Provider> ) } export default hot(Root) // 温馨提示 // 关于新 API 👉 hot 是位于 '/root' 下面的,但并不是所有的打包工具都支持该新 API。比如 parcel 就不支持。 // 此时 react-hot-loader 就会抛出错误,并要求你使用旧的 API,方法如下: // // import { hot } from 'react-hot-loader' // export default hot(module)(App)
- 确保在
react
和react-dom
之前导入react-hot-loader
,两种方式可选择:
- 在入口文件导入
import react-hot-loader
(要在 React 之前) - 在 webpack 配置文件的
entry
配置react-hot-loader/patch
。
// webpack.config.js { entry: [ 'react-hot-loader/patch', './src/js/index.js' ] }
需要注意的是:
react-hot-loader/patch
一定要写在entry
的最前面。如果有babel-polyfill
就写在babel-polyfill
的后面。
- 使用 React Hooks 需要用到
@hot-loader/react-dom
。同时,就以上配置重新启动,此时控制台会打印一个 WARNING,如下:
React-Hot-Loader: react-🔥-dom patch is not detected. React 16.6+ features may not work. (Issue #1227)
解决方案之一,另一种可以看下 ISSUE #1227。
// webpack.config.js { resolve: { alias: { 'react-dom': '@hot-loader/react-dom' } } }
测试一下,分别两次点击 add 后,再添加一个节点元素,发现 Component State 是在我们预期之内的,并没有像之前一样因为热更新而丢失状态。
四、至此
关于 Hot Module Replacement 模块热替换(热更新)基本就介绍完了。
前面我们提到一个 React Fast Refresh 概念,它由官方维护,稳定性与性能有保障,对 React Hooks 有更完善的支持。官方实现是 react-refresh
。
后面有时间的话,应该会写一篇关于它的文章。但是,它最低支持版本是 react-dom@16.9+
。
The end.