正如在概念章节提到的,模块热替换(HMR)的作用是,在应用运行时,无需刷新页面,便能替换、增加、删除必要的模块。 HMR 对于那些由单一状态树构成的应用非常有用。因为这些应用的组件是 "dumb" (相对于 "smart") 的,所以在组件的代码更改后,组件的状态依然能够正确反映应用的最新状态。
下面的介绍是基于 Babel 和 React 的,但它们对于 HMR 并不是必需的。
T> 如果你想了解别的配置方式,可以告诉我们,或者更好的方式是,提一个 PR。
下面将会示范怎么在 Babel, React 和 PostCSS(使用 CSS 模块)的项目中配置 HMR。为此,你需要在你的 package.json
加上以下依赖,可以通过下面的命令来安装:
npm install --save-dev babel-core@6.13.2 babel-loader@6.2.4 babel-preset-es2015@6.13.2 babel-preset-react@6.11.1 babel-preset-stage-2@6.13.0 css-loader@0.23.1 postcss-loader@0.9.1 react-hot-loader@3.0.0-beta.6 style-loader@0.13.1 webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.0
另外,你也需要安装 React:
npm install --save react@15.3.0 react-dom@15.3.0
你的 .babelrc
配置文件或许会和下面的配置相差无几:
{
"presets": [
["es2015", {"modules": false}],
// webpack现在已经支持原生的import语句了, 并且将其运用在tree-shaking特性上
"stage-2",
// 规定JS运用的语言规范层级
// Stage 2 是 "草案", 4 是 "已完成", 0 is "稻草人(strawman)"。
// 详情查看 https://tc39.github.io/process-document/
"react"
// 转译React组件为JS代码
],
"plugins": [
"react-hot-loader/babel"
// 开启react代码的模块热替换(HMR)
]
}
当然,有很多方法来设置你的 webpack ── 例如通过 API 配置,由单个或多个配置文件来配置,等等。下面是一个基本的配置,可以供你作为参考:
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
entry: [
'react-hot-loader/patch',
// 开启react代码的模块热替换(HMR)
'webpack-dev-server/client?http://localhost:8080',
// 为webpack-dev-server的环境打包好运行代码
// 然后连接到指定服务器域名与端口
'webpack/hot/only-dev-server',
// 为热替换(HMR)打包好运行代码
// only- 意味着只有成功更新运行代码才会执行热替换(HMR)
'./index.js'
// 我们app的入口文件
],
output: {
filename: 'bundle.js',
// 输出的打包文件
path: resolve(__dirname, 'dist'),
publicPath: '/'
// 对于热替换(HMR)是必须的,让webpack知道在哪里载入热更新的模块(chunk)
},
context: resolve(__dirname, 'src'),
devtool: 'inline-source-map',
devServer: {
hot: true,
// 开启服务器的模块热替换(HMR)
contentBase: resolve(__dirname, 'dist'),
// 输出文件的路径
publicPath: '/'
// 和上文output的"publicPath"值保持一致
},
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader',
],
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader?modules',
'postcss-loader',
],
},
],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// 开启全局的模块热替换(HMR)
new webpack.NamedModulesPlugin(),
// 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
],
};
上面的内容涵盖了 webpack 配置的方方面面,并不是全部都和 HMR 相关。这个 webpack 开发服务器的完整的文档能够让你对它了解更多,这些在 webpack.js.org 上的文章也应该一读。
这里有一个基本假设,便是你的 JavaScript 入口在 ./src/index.js
,还有,你在使用 CSS 模块。
配置中的注释或许能够帮助你理解一二。有两个主要的部分值得一看: devServer
键和 entry
键。另外,HotModuleReplacementPlugin
是必须加到 plugins
数组中去的。
这里特别要提一下下面的两个模块:
在 entry
里的 react-hot-loader
,是 React 配置 HMR 必不可少的模块。
还有 NamedModulesPlugin
,它的用处在于,能让你知道热重载时是哪个模块作出了变动。
下面是和上面配置相关的代码:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
// AppContainer is a necessary wrapper component for HMR
import App from './components/App';
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component/>
</AppContainer>,
document.getElementById('root')
);
};
render(App);
// 模块热替换的 API
if (module.hot) {
module.hot.accept('./components/App', () => {
render(App)
});
}
// ./src/components/App.js
import React from 'react';
import styles from './App.css';
const App = () => (
<div className={styles.app}>
<h2>Hello, </h2>
</div>
);
export default App;
// ./src/components/App.css
.app {
text-size-adjust: none;
font-family: helvetica, arial, sans-serif;
line-height: 200%;
padding: 6px 20px 30px;
}
事实上,上面的代码中最重要是引用 module
的那一部分代码。
如果我们设置了 devServer: { hot: true }
,webpack 会暴露 module.hot
给我们的代码;
因此,我们可以使用 module.hot
钩子函数为特定资源启用 HMR(这里是App.js
)。这里最重要的 API 是 module.hot.accept
,它指定如何处理对特定依赖的更改。
注意,因为 webpack 2 对 ES2015 模块有内置的支持,你不需要在 module.hot.accept
中重新引入你的根组件。要完成这项工作,你需要更改 Babel ES2015 在 .babelrc
的预设值:
["es2015", {"modules": false}]
与我们在 Babel 配置文件 中所配置的是一样的。注意,不仅仅只有模块热替换的场景需要禁用Babel模块插件。如果你不将此插件禁用,你可能会遇到许多其他的问题(查看 从webpack v1 迁移到 v2 和 webpack-tree-shaking)。
注意,如果你在webpack 2 配置文件中启用了 ES2015 模块,并且按照上文#3 的配置,修改了你的.babelrc
文件,你需要使用require
命令,或者,创建两个.babelrc
文件(查看问题 这里):
"presets": ["es2015"]
src/
所以,在这种情景下,当 src/components/App.js
或者它的依赖文件被更改了, module.hot.accept
将会触发 render
方法,这意味着,因为 App.js
里面包含了对 App.css
的引用, 所以 render
方法同样会在 App.css
被修改的时候触发,
这个文件需要放置在你的项目根路径下的 dist
目录,不然 webpack-dev-server 将因为缺少这个文件而无法运行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example Index</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
最后,让我们启动 webpack-dev-server
来打包我们的代码,看看 HMR 的运行效果吧。我们可以使用下面的 package.json
进入:
{
"scripts" : {
"start" : "webpack-dev-server"
}
}
运行 npm start
。然后打开浏览器,在地址栏输入 localhost:8080
。这时候应该在你的 console.log 中会看到下面的输出:
dev-server.js:49[HMR] Waiting for update signal from WDS…
only-dev-server.js:74[HMR] Waiting for update signal from WDS…
client?c7c8:24 [WDS] Hot Module Replacement enabled.
接着你可以对 App.js 作出修改并保存。你的 console.log 应该会显示下面的信息:
[WDS] App updated. Recompiling…
client?c7c8:91 [WDS] App hot update…
dev-server.js:45 [HMR] Checking for updates on the server…
log-apply-result.js:20 [HMR] Updated modules:
log-apply-result.js:22 [HMR] - ./components/App.js
dev-server.js:27 [HMR] App is up to date.
我们可以看到,HMR 标记出了被修改文件的路径。这便是 NamedModules 插件所起的作用。