热加载 HMR
什么是 HMR
HMR 是指 Hot Module Replacement。对于你需要更新的模块,进行一个"热"替换,所谓的热替换是指在不需要刷新页面的情况下,对某个改动进行无缝更新。如果你没有配置 HMR,那么你每次改动,都需要刷新页面,才能看到改动之后的结果,对于调试来说,非常麻烦,而且效率不高,最关键的是,你在界面上修改的数据,随着刷新页面会丢失,而如果有类似 Webpack 热更新的机制存在,那么,则是修改了代码,不会导致刷新,而是保留现有的数据状态,只将模块进行更新替换。也就是说,既保留了现有的数据状态,又能看到代码修改后的变化。
总结:
- 加载页面时保存应用程序状态
- 只更新改变的内容,节省调试时间
- 修改样式更快,几乎等同于在浏览器中更改样式
基础配置
安装依赖
假设现在已经有了 webpack 的基础依赖包,想要让 HMR 生效还需要安装以下包:
- webpack-dev-middleware
- webpack-hot-middleware (@webapp-suite/webpack-hot-middleware)
- npm
- Yarn
- pnpm
npm install -D webpack-dev-middleware @webapp-suite/webpack-hot-middleware @pmmmwh/react-refresh-webpack-plugin react-refresh
yarn add --dev webpack-dev-middleware @webapp-suite/webpack-hot-middleware @pmmmwh/react-refresh-webpack-plugin react-refresh
pnpm add -D webpack-dev-middleware @webapp-suite/webpack-hot-middleware @pmmmwh/react-refresh-webpack-plugin react-refresh
配置 Webpack
webpack.dev.config.js
module.exports = {
mode: 'development',
entry: {
app: [
resolve(__dirname, '../src/index'),
// 必须这么写,这将连接到服务器,以便在包重新构建时接收通知,然后相应地更新客户端
'@webapp-suite/webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000'
]
},
plugins: [new webpack.HotModuleReplacementPlugin()]
};
配置 Node Server
server.js
const webpackHotMiddleware = require('@webapp-suite/webpack-hot-middleware');
if (isDevelopment) {
const urls = prepareUrls('http', 'localhost');
const openUrl = urls.localUrlForBrowser;
const compiler = webpack({...WebpackDevConfig, mode: 'development'});
const webpackDevInstance = webpackDevMiddleware(compiler, {
stats: false
});
const webpackHotInstance = webpackHotMiddleware(compiler, {
log: false, // https://github.com/geowarin/friendly-errors-webpack-plugin#turn-off-errors
path: '/__webpack_hmr',
heartbeat: 10 * 1000
});
server.use(webpackDevInstance);
server.use(webpackHotInstance);
webpackDevInstance.waitUntilValid(() => {
openBrowser(openUrl);
});
}
查看
效果已经出来,我们发现控制台已经提示热更新日志输出:
[HMR] connected
这已经很明白地告诉我们热更新已经连接上了,当我们修改源码文件后,浏览器会自动的刷新。
问题
比如弹框组件,当我点击 button,state 也会随之增加。但是这个时候如果我修改了某一个文件内容,可以看到我浏览器的确刷新了。但是!state 却重置到了 1,这并不是我们想要的。
热更新保留组件状态
React
- @pmmmwh/react-refresh-webpack-plugin (React App)
- react-refresh (React App)
- npm
- Yarn
- pnpm
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
yarn add --dev @pmmmwh/react-refresh-webpack-plugin react-refresh
pnpm add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack.dev.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
app: [
resolve(__dirname, '../src/index'),
'@webapp-suite/webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000'
]
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
use: [
{
loader: 'babel-loader',
options: {
plugins: ['react-refresh/babel']
}
}
]
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin()
]
};
至此,我们就已经实现了,修改源码后 ,浏览器自动刷新的效果了,并且还保留了刷新前的 state 状态。
使用 TypeScript
- @babel/core
- @babel/plugin-syntax-decorators
- @babel/plugin-syntax-jsx
- @babel/plugin-syntax-typescript
- babel-loader
- npm
- Yarn
- pnpm
npm install -D @babel/core @babel/plugin-syntax-decorators @babel/plugin-syntax-jsx @babel/plugin-syntax-typescript babel-loader
yarn add --dev @babel/core @babel/plugin-syntax-decorators @babel/plugin-syntax-jsx @babel/plugin-syntax-typescript babel-loader
pnpm add -D @babel/core @babel/plugin-syntax-decorators @babel/plugin-syntax-jsx @babel/plugin-syntax-typescript babel-loader
webpack.dev.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
app: [
resolve(__dirname, '../src/index'),
'@webapp-suite/webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000'
]
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
use: [
{
loader: 'thread-loader'
},
{
loader: 'babel-loader',
options: {
plugins: [
'@babel/plugin-syntax-typescript',
['@babel/plugin-syntax-decorators', {legacy: true}],
'@babel/plugin-syntax-jsx',
'react-refresh/babel'
]
}
},
{
loader: 'ts-loader',
options: {
transpileOnly: true,
happyPackMode: true
}
}
]
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin()
]
};
虽然直接在 ts-loader 后面加一层 babel-loader 会带来一定的性能损耗,但实际开发过程中还是可以接受的,如果直接用 babel-loader 代替 ts-loader,各种语法兼容需要装插件,还可能还会有各种错,比较折腾,不如这样省事。
参考资料
- pmmmwh/react-refresh-webpack-plugin README
- Webpack: Hot Module Replacement
- Webpack: 模块热替换
- webpack 学习之路(四)webpack-hot-middleware 实现热更新, by daly42872
- Webpack 自动刷新和 HMR, by patrick_kibo
- Webpack 模块热替换(hot module replacement) API
- stackoverflow: webpack-dev-server: how to preserve state with module.hot.data?
- Facebook/create-react-app webpackHotDevClient.js
- react 配置 HMR(热替换 保留页面数据状态), by 哈娄
- react-hot-loader
- react-hot-loader: Performance oriented .babelrc for TS users, by RomanHotsiy
- 使用 webpack 实现 react 的热更新, by Nealyang
- 基于 webpack 的热重载 live reload 和热更新 HMR, by 快狗打车前端团队
- Webpack 如何配置热更新,By 发声的沉默者
- 看完这篇,面试再也不怕被问 Webpack 热更新, by 米亚
- How should we set up apps for HMR now that Fast Refresh replaces react-hot-loader?
- 使用 webpack 实现 react 的热更新, By isNealyang
- Webpack Guidebook: 模块热更新, by tsejx