热模块更换(HMR)建立在 webpack-dev-server(WDS) 之上。它启用了一个可以实时交换模块的接口。例如,style-loader 可以在不强制刷新的情况下更新 CSS。为样式实现 HMR 是比较不错的,因为 CSS 是无状态的。
HMR 也可以用于 JavaScript,但由于应用程序具备状态,替换起来会比较困难。react-hot-loader 和 vue-hot-reload-api 是两个很好的范例。
鉴于 HMR 实现起来可能很复杂,一个很好的折衷方案是将应用程序状态存储到
localStorage
中,然后在刷新后基于localStorage
将状态复原。但是这样做会将问题推向应用程序端。
启用 HMR
需要启用以下步骤才能使 HMR 正常工作:
- WDS 必须在热模式下运行才能将热模块替换接口暴露给客户端。
- Webpack 必须为服务器提供热更新,并且可以使用
webpack.HotModuleReplacementPlugin
。 - 客户端必须运行 WDS 提供的特定脚本。这些脚本会被自动注入到入口中,但我们可以手动配置入口以显式启用它。
- 客户端必须通过
module.hot.accept
实现 HMR 接口。
使用 webpack-dev-server --hot
解决了前两个问题。现在,如果要更新 JavaScript 应用程序代码,则必须自己处理最后一个问题。跳过 --hot
标志并通过 Webpack 配置可提供更大的灵活性。
下面的配置基本上达到了前面的两个条件,你可以对此处代码进行调整以匹配您的配置:
{
devServer: {
// 热加载失败后不刷新,如果实现了
// 客户端接口,开启它就比较合适
hotOnly: true,
// 如果出现错误时,你仍然希望它刷新, 请设置:
// hot: true,
},
plugins: [
// 启用这个插件以使 webpack 将变化通知到 WDS
// 这个插件会自动启用 --hot 标志
new webpack.HotModuleReplacementPlugin(),
],
}
如果在不实现客户端接口的情况下使用上述配置,则很可能会出现错误:
该消息告诉我们 HMR 接口接收到了客户端需要热更新的部分代码,但没有做任何的热更换,这是下一步要解决的问题。
该设置假定您已启用
webpack.NamedModulesPlugin()
。默认情况下,如果以development
模式运行 Webpack,它将处于打开状态。
webpack-dev-server 可以在特定路径下运行。Webpack issue#675 更详细地讨论了该问题。
你不能在生产环境中启用 HMR。它可能有效,但此时没有必要这样做。
如果您正在使用 Babel,请将其配置为允许 Webpack 控制模块生成,否则 HMR 逻辑将无法正常工作!
实现 HMR 接口
Webpack 通过全局变量暴露 HMR 接口:module.hot
。它通过module.hot.accept(<path to watch>, <handler>)
函数进行热更新,您需要在那里替换应用程序代码。
请看下面的例子:
src/index.js
import component from "./component";
let demoComponent = component();
document.body.appendChild(demoComponent);
// HMR 接口
if (module.hot) {
// 捕获热更新
module.hot.accept("./component", () => {
const nextComponent = component();
// 用新的内容替换老的内容
document.body.replaceChild(nextComponent, demoComponent);
demoComponent = nextComponent;
});
}
刷新浏览器,尝试在此更改后修改 src/component.js,将文本更改为其他内容,您应该注意到浏览器根本不刷新。相反,它应该替换 DOM 节点,同时保留应用程序的其余部分。
下图显示了可能的输出:
上面的替换和样式、React、Redux 以及其他技术在思想上是想通的。有时您不必自己实现接口,有一些可用的工具为您处理。
要证明 HMR 保留应用程序状态,请在原始文件旁边添加一个基于复选框的组件。
module.hot.accept
可以捕获变更了的组件。
当对代码进行压缩时,
if(module.hot)
块将完全从生产构建中消除。压缩代码一章深入研究了这个话题。
手动设置 WDS 入口点
在上面的设置中,与 WDS 相关的代码是自动注入到入口中的。假设您正在通过 Node 使用 WDS,您必须自己设置它们,因为 Node API 不支持注入。以下示例说明了如何实现此目的:
entry: {
hmr: [
// 包含客户端代码,注意主机地址和端口
"webpack-dev-server/client?http://localhost:8080",
// 只有在编译成功时才进行热更新
"webpack/hot/only-dev-server",
// 编译失败时也刷新
// "webpack/hot/dev-server",
],
...
},
HMR 和动态加载
通过 require.context
和 HMR 进行动态加载需要额外的努力:
const req = require.context("./pages", true, /^(.*\.(jsx$))[^.]*$/g);
module.hot.accept(req.id, ...); // 像上面那样替换模块
总结
HMR 是 Webpack 吸引开发人员的众多功能点之一,而 Webpack 已经在这方面做了许多工作,要让 HMR 正常工作,需要客户端和服务器端同时支持。为此,webpack-dev-server 提供了这两者。通常你必须实现客户端接口,但是像 style-loader 这样的 loader 程序已经为做了这一工作。