热模块替换
热模块替换(HMR)是webpack提供的最有用的功能之一,它让各种模块可以在运行时更新而无需刷新,本篇主要注重于实现。
ps:HMR是为开发模式设计的,也只能用于开发模式。
启用HRM
启用HRM只需要更新webpack-dev-server的配置,然后使用webpack的内置插件,同时要删掉print.js入口。如果你使用的是webpack-dev-middleware,请使用webpack-hot-middleware包去启用HRM。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');
module.exports = {
entry: {
- app: './src/index.js',
- print: './src/print.js'
+ app: './src/index.js' }, devtool: 'inline-source-map', devServer: { contentBase: './dist', + hot: true }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Hot Module Replacement' }), + new webpack.NamedModulesPlugin(), + new webpack.HotModuleReplacementPlugin() ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
你也可以使用脚手架的webpack-dev-server --hotOnly来更新webpack-dev-server的这个配置。这里还添加了NamedModulesPlugin以便更容易查看要修补(patch)的依赖。在起步阶段,我们将通过在命令行中运行 npm start
来启动并运行 dev server。
更新一下index.js使得当print.js文件有任何变化就通知webpack去更新模块。
index.js
import _ from 'lodash';
import printMe from './print.js';
function component() {
var element = document.createElement('div');
var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
+
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe(); + }) + }
如果现在还没把项目跑起来的话就npm start一下,然后修改print.js中的内容,你就会在浏览器的控制台看到如下输出:
print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ console.log('Updating print.js...')
}
console
[HMR] Waiting for update signal from WDS...
main.js:4395 [WDS] Hot Module Replacement enabled.
+ 2main.js:4395 [WDS] App updated. Recompiling...
+ main.js:4395 [WDS] App hot update...
+ main.js:4330 [HMR] Checking for updates on the server...
+ main.js:10024 Accepting the updated printMe module! + 0.4b8ee77….hot-update.js:10 Updating print.js... + main.js:4330 [HMR] Updated modules: + main.js:4330 [HMR] - 20 + main.js:4330 [HMR] Consider using the NamedModulesPlugin for module names.
通过Node.js API:
当使用dev server和Node.js API时,不要把dev server的选项放在webpack的配置对象中,而应该在创建选项时把它作为第二个参数传递:
new WebpackDevServer(compiler, options)
启用HRM还需要修改webpack的配置对象以包含HRM入口,webpack-dev-server包提供一个addDevSeverEntryPoints方法来做这件事:
dev-server.js
const webpackDevServer = require('webpack-dev-server'); const webpack = require('webpack'); const config = require('./webpack.config.js'); const options = { contentBase: './dist', hot: true, host: 'localhost' }; webpackDevServer.addDevServerEntrypoints(config, options); const compiler = webpack(config); const server = new webpackDevServer(compiler, options); server.listen(5000, 'localhost', () => { console.log('dev server listening on port 5000'); });
ps:如果你使用的是webpack-dev-middleware可以通过webpack-hot-middleware包来启用HRM。
问题:
HRM是比较难掌握的,如果你点击一下页面上的按钮会发现控制台打印的还是旧的那一句输出。这是因为按钮的事件还是绑定在原来的printMe方法上。我们需要使用module.hot.accept来更新这个绑定关系:
index.js
import _ from 'lodash';
import printMe from './print.js';
function component() {
var element = document.createElement('div');
var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function
element.appendChild(btn);
return element;
}
- document.body.appendChild(component());
+ let element = component(); // Store the element to re-render on print.js changes
+ document.body.appendChild(element);
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
- printMe(); + document.body.removeChild(element); + element = component(); // Re-render the "component" to update the click handler + document.body.appendChild(element); }) }
这只是一个例子,还有很多地方能让人轻易犯错,幸运的是有很多loader(下面会提到一些)可以让HRM变得平易近人一些。
HRM修改样式表:
借助于style-loader,css的HRM变得非常简单,当css模块更新时style-loader会在后台使用module.hot.accept来patch<style>标签。
如果没安装的话这里要安装一下并且更新配置:
npm install --save-dev style-loader css-loader
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: {
app: './src/index.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
hot: true
},
+ module: {
+ rules: [
+ {
+ test: /\.css$/, + use: ['style-loader', 'css-loader'] + } + ] + }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Hot Module Replacement' }), new webpack.HotModuleReplacementPlugin() ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') } };
现在热加载样式表会和引入模块一样简单:
project
webpack-demo
| - package.json
| - webpack.config.js
| - /dist
| - bundle.js
| - /src
| - index.js
| - print.js
+ | - styles.css
styles.css
body {
background: blue; }
index.js
import _ from 'lodash';
import printMe from './print.js';
+ import './styles.css';
function component() {
var element = document.createElement('div');
var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe; // onclick event is bind to the original printMe function
element.appendChild(btn);
return element;
}
let element = component();
document.body.appendChild(element);
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
document.body.removeChild(element);
element = component(); // Re-render the "component" to update the click handler
document.body.appendChild(element);
})
}
再把背景改为红色你会发现浏览器会立刻更改而无需刷新:
styles.css
body {
- background: blue;
+ background: red;
}
其它代码和框架:
社区还有许多其他 loader 和示例,可以使 HMR 与各种框架和库(library)平滑地进行交互……
- React Hot Loader:实时调整 react 组件。
- Vue Loader:此 loader 支持用于 vue 组件的 HMR,提供开箱即用体验。
- Elm Hot Loader:支持用于 Elm 程序语言的 HMR。
- Redux HRM:无需 loader 或插件!只需对 main store 文件进行简单的修改。
- Angular HRM:No loader necessary! A simple change to your main NgModule file is all that's required to have full control over the HMR APIs.没有必要使用 loader!只需对主要的 NgModule 文件进行简单的修改,由 HMR API 完全控制。