模块热替换

本指南继续沿用 开发环境 指南中的代码示例。

模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。本页面重点介绍其实现,而 概念 页面提供了更多关于它的工作原理以及为什么它有用的细节。

HMR 不适用于生产环境,这意味着它应当用于开发环境。更多详细信息,请查看 生产环境 指南。

启用 HMR

此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置,然后使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js 的入口起点,因为现在已经在 index.js 模块中引用了它。

如果你在技术选型中使用了 webpack-dev-middleware 而没有使用 webpack-dev-server,请使用 webpack-hot-middleware package,以在你的自定义 server 或应用程序上启用 HMR。

webpack.config.js

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin');
  3. const CleanWebpackPlugin = require('clean-webpack-plugin');
  4. + const webpack = require('webpack');
  5. module.exports = {
  6. entry: {
  7. - app: './src/index.js',
  8. - print: './src/print.js'
  9. + app: './src/index.js'
  10. },
  11. devtool: 'inline-source-map',
  12. devServer: {
  13. contentBase: './dist',
  14. + hot: true
  15. },
  16. plugins: [
  17. new CleanWebpackPlugin(['dist']),
  18. new HtmlWebpackPlugin({
  19. title: '模块热替换'
  20. }),
  21. + new webpack.HotModuleReplacementPlugin()
  22. ],
  23. output: {
  24. filename: '[name].bundle.js',
  25. path: path.resolve(__dirname, 'dist')
  26. }
  27. };

可以通过命令来修改 webpack-dev-server 的配置:webpack-dev-server --hotOnly

现在,修改 index.js 文件,以便在 print.js 内部发生变更时,告诉 webpack 接受 updated module。

index.js

  1. import _ from 'lodash';
  2. import printMe from './print.js';
  3. function component() {
  4. var element = document.createElement('div');
  5. var btn = document.createElement('button');
  6. element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  7. btn.innerHTML = 'Click me and check the console!';
  8. btn.onclick = printMe;
  9. element.appendChild(btn);
  10. return element;
  11. }
  12. document.body.appendChild(component());
  13. +
  14. + if (module.hot) {
  15. + module.hot.accept('./print.js', function() {
  16. + console.log('Accepting the updated printMe module!');
  17. + printMe();
  18. + })
  19. + }

修改 print.jsconsole.log 语句,你将会在浏览器中看到如下的输出(暂时不要担心 button.onclick = printMe() 输出,我们稍后也会更新这部分)。

print.js

  1. export default function printMe() {
  2. - console.log('I get called from print.js!');
  3. + console.log('Updating print.js...')
  4. }

console

  1. [HMR] Waiting for update signal from WDS...
  2. main.js:4395 [WDS] Hot Module Replacement enabled.
  3. + 2main.js:4395 [WDS] App updated. Recompiling...
  4. + main.js:4395 [WDS] App hot update...
  5. + main.js:4330 [HMR] Checking for updates on the server...
  6. + main.js:10024 Accepting the updated printMe module!
  7. + 0.4b8ee77….hot-update.js:10 Updating print.js...
  8. + main.js:4330 [HMR] Updated modules:
  9. + main.js:4330 [HMR] - 20

通过 Node.js API

在 Node.js API 中使用 webpack dev server 时,不要将 dev server 选项放在 webpack 配置对象(webpack config object)中。而是,在创建时,将其作为第二个参数传递。例如:

new WebpackDevServer(compiler, options)

想要启用 HMR,还需要修改 webpack 配置对象,使其包含 HMR 入口起点。webpack-dev-server package 中具有一个叫做 addDevServerEntrypoints 的方法,你可以通过使用这个方法来实现。这是关于如何使用的一个基本示例:

dev-server.js

  1. const webpackDevServer = require('webpack-dev-server');
  2. const webpack = require('webpack');
  3. const config = require('./webpack.config.js');
  4. const options = {
  5. contentBase: './dist',
  6. hot: true,
  7. host: 'localhost'
  8. };
  9. webpackDevServer.addDevServerEntrypoints(config, options);
  10. const compiler = webpack(config);
  11. const server = new webpackDevServer(compiler, options);
  12. server.listen(5000, 'localhost', () => {
  13. console.log('dev server listening on port 5000');
  14. });

如果你正在使用 webpack-dev-middleware,可以通过 webpack-hot-middleware package,在自定义 dev server 中启用 HMR。

问题

模块热替换可能比较难以掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮,你会发现控制台仍在打印旧的 printMe 函数。

这是因为按钮的 onclick 事件处理函数仍然绑定在旧的 printMe 函数上。

为了让 HMR 正常工作,我们需要更新代码,使用 module.hot.accept 将其绑定到新的 printMe 函数上:

index.js

  1. import _ from 'lodash';
  2. import printMe from './print.js';
  3. function component() {
  4. var element = document.createElement('div');
  5. var btn = document.createElement('button');
  6. element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  7. btn.innerHTML = 'Click me and check the console!';
  8. btn.onclick = printMe; // onclick 事件绑定原始的 printMe 函数上
  9. element.appendChild(btn);
  10. return element;
  11. }
  12. - document.body.appendChild(component());
  13. + let element = component(); // 存储 element,以在 print.js 修改时重新渲染
  14. + document.body.appendChild(element);
  15. if (module.hot) {
  16. module.hot.accept('./print.js', function() {
  17. console.log('Accepting the updated printMe module!');
  18. - printMe();
  19. + document.body.removeChild(element);
  20. + element = component(); // Re-render the "component" to update the click handler
  21. + element = component(); // 重新渲染 "component",以便更新 click 事件处理函数
  22. + document.body.appendChild(element);
  23. })
  24. }

这仅仅是一个示例,还有很多让人易于犯错的情况。幸运的是,有很多 loader(下面会提到一些)可以使得模块热替换变得更加容易。

HMR 加载样式

借助于 style-loader,使用模块热替换来加载 CSS 实际上极其简单。此 loader 在幕后使用了 module.hot.accept,在 CSS 依赖模块更新之后,会将其 patch(修补) 到 <style> 标签中。

首先使用以下命令安装两个 loader :

  1. npm install --save-dev style-loader css-loader

然后更新配置文件,使用这两个 loader。

webpack.config.js

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin');
  3. const CleanWebpackPlugin = require('clean-webpack-plugin');
  4. const webpack = require('webpack');
  5. module.exports = {
  6. entry: {
  7. app: './src/index.js'
  8. },
  9. devtool: 'inline-source-map',
  10. devServer: {
  11. contentBase: './dist',
  12. hot: true
  13. },
  14. + module: {
  15. + rules: [
  16. + {
  17. + test: /\.css$/,
  18. + use: ['style-loader', 'css-loader']
  19. + }
  20. + ]
  21. + },
  22. plugins: [
  23. new CleanWebpackPlugin(['dist']),
  24. new HtmlWebpackPlugin({
  25. title: '模块热替换'
  26. }),
  27. new webpack.HotModuleReplacementPlugin()
  28. ],
  29. output: {
  30. filename: '[name].bundle.js',
  31. path: path.resolve(__dirname, 'dist')
  32. }
  33. };

如同 import 模块,热加载样式表同样很简单:

project

  1. webpack-demo
  2. | - package.json
  3. | - webpack.config.js
  4. | - /dist
  5. | - bundle.js
  6. | - /src
  7. | - index.js
  8. | - print.js
  9. + | - styles.css

styles.css

  1. body {
  2. background: blue;
  3. }

index.js

  1. import _ from 'lodash';
  2. import printMe from './print.js';
  3. + import './styles.css';
  4. function component() {
  5. var element = document.createElement('div');
  6. var btn = document.createElement('button');
  7. element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  8. btn.innerHTML = 'Click me and check the console!';
  9. btn.onclick = printMe; // onclick event is bind to the original printMe function
  10. element.appendChild(btn);
  11. return element;
  12. }
  13. let element = component();
  14. document.body.appendChild(element);
  15. if (module.hot) {
  16. module.hot.accept('./print.js', function() {
  17. console.log('Accepting the updated printMe module!');
  18. document.body.removeChild(element);
  19. element = component(); // Re-render the "component" to update the click handler
  20. document.body.appendChild(element);
  21. })
  22. }

body 的 style 改为 background: red;,你应该可以立即看到页面的背景颜色随之更改,而无需完全刷新。

styles.css

  1. body {
  2. - background: blue;
  3. + background: red;
  4. }

其他代码和框架

社区还提供许多其他 loader 和示例,可以使 HMR 与各种框架和库平滑地进行交互……

  • React Hot Loader:实时调整 react 组件。
  • Vue Loader:此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
  • Elm Hot Loader:支持 Elm 编程语言的 HMR。
  • Angular HMR:没有必要使用 loader!直接修改 NgModule 主文件就够了,它可以完全控制 HMR API。

如果你知道任何其他 loader 或 plugin,能够有助于或增强模块热替换(hot module replacement),请提交一个 pull request 以添加到此列表中!


进一步阅读


贡献人员

EugeneHlushko EugeneHlushko aiduryagin aiduryagin aviyacohen aviyacohen bdwain bdwain caryli caryli drpicox drpicox gdi2290 gdi2290 jhnns jhnns jmreidy jmreidy joshsantos joshsantos rohannair rohannair sararubin sararubin sbaidon sbaidon skipjack skipjack xgirma xgirma