热模块更换

热模块更换(HMR)建立在 webpack-dev-server(WDS) 之上。它启用了一个可以实时交换模块的接口。例如,style-loader 可以在不强制刷新的情况下更新 CSS。为样式实现 HMR 是比较不错的,因为 CSS 是无状态的。

HMR 也可以用于 JavaScript,但由于应用程序具备状态,替换起来会比较困难。react-hot-loadervue-hot-reload-api 是两个很好的范例。

鉴于 HMR 实现起来可能很复杂,一个很好的折衷方案是将应用程序状态存储到 localStorage 中,然后在刷新后基于 localStorage 将状态复原。但是这样做会将问题推向应用程序端。

启用 HMR

需要启用以下步骤才能使 HMR 正常工作:

  1. WDS 必须在热模式下运行才能将热模块替换接口暴露给客户端。
  2. Webpack 必须为服务器提供热更新,并且可以使用 webpack.HotModuleReplacementPlugin
  3. 客户端必须运行 WDS 提供的特定脚本。这些脚本会被自动注入到入口中,但我们可以手动配置入口以显式启用它。
  4. 客户端必须通过 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 节点,同时保留应用程序的其余部分。

下图显示了可能的输出:

通过HMR成功修补模块

上面的替换和样式、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 程序已经为做了这一工作。

用户头像
登录后发表评论