最近原创文章回顾😊:
- 《1.2w字 | 初中级前端 JavaScript 自测清单 - 1》
- 《了不起的 Webpack HMR 学习指南(含源码分析)》
- 《了不起的 Webpack 构建流程学习指南》
- 《你不知道的 WeakMap》番外篇
- 《你不知道的 Blob》番外篇
- 《了不起的 tsconfig.json 指南》
- 《200行JS代码,带你实现代码编译器》
学习章节:《Webpack HMR 原理解析》
一、HMR 介绍
Hot Module Replacement(以下简称:HMR 模块热替换)是 Webpack 提供的一个非常有用的功能,它允许在 JavaScript 运行时更新各种模块,而无需完全刷新。
Hot Module Replacement (or HMR) is one of the most useful features offered by webpack. It allows all kinds of modules to be updated at runtime without the need for a full refresh. --《Hot Module Replacement》
当我们修改代码并保存后,Webpack 将对代码重新打包,HMR 会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
HMR 主要通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面时丢失的应用程序状态;
- 只更新变更内容,以节省宝贵的开发时间;
- 调整样式更加快速 - 几乎相当于在浏览器调试器中更改样式。
需要注意:HMR 不适用于生产环境,这意味着它应当只在开发环境使用。
二、HMR 使用方式
在 Webpack 中启用 HMR 功能比较简单:
1. 方式一:使用 devServer
1.1 设置 devServer 选项
只需要在 webpack.config.js
中添加 devServer
选项,并设置 hot
值为 true
,并使用HotModuleReplacementPlugin
和 NamedModulesPlugin
(可选)两个 Plugins :
// webpack.config.js const path = require('path') const webpack = require('webpack') module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.join(__dirname, '/') }, + devServer: { + hot: true, // 启动模块热更新 HMR + open: true, // 开启自动打开浏览器页面 + }, plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.HotModuleReplacementPlugin() ] }
1.2 添加 scripts
然后在 package.json
中为 scripts
命令即可:
// package.json { // ... "scripts": { + "start": "webpack-dev-server" }, // ... }
2. 方式二、使用命令行参数
另一种是通过添加 --hot
参数来实现。添加 --hot
参数后,devServer 会告诉 Webpack 自动引入 HotModuleReplacementPlugin
,而不需要我们手动引入。
另外常常也搭配 --open
来自动打开浏览器到页面。
这里移除掉前面添加的两个 Plugins :
// webpack.config.js const path = require('path') const webpack = require('webpack') module.exports = { // ... - plugins: [ - new webpack.NamedModulesPlugin(), - new webpack.HotModuleReplacementPlugin() - ] }
然后修改 package.json
文件中的 scripts
配置:
// package.json { // ... "scripts": { - "start": "webpack-dev-server" + "start": "webpack-dev-server --hot --open" }, // ... }
3. 简单示例
基于上述配置,我们简单实现一个场景: index.js
文件中导入 hello.js
模块,当 hello.js
模块发生变化时, index.js
将更新模块。
模块代码如下实现:
// hello.js export default () => 'hi leo!'; // index.js import hello from './hello.js' const div = document.createElement('div'); div.innerHTML = hello(); document.body.appendChild(div);
然后在 index.html
中导入打包后的 JS 文件,并执行 npm start
运行项目:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <div>了不起的 Webpack HMR 学习指南</div> <script src="bundle.js"></script> </body> </html>
4. 实现监听更新
当我们通过 HotModuleReplacementPlugin
插件启用了 HMR,则它的接口将被暴露在全局 module.hot
属性下面。通常,可以先检查这个接口是否可访问,然后再开始使用它。
举个例子,你可以这样 accept
一个更新的模块:
if (module.hot) { module.hot.accept('./library.js', function() { // 使用更新过的 library 模块执行某些操作... }) }
关于 module.hot
更多 API ,可以查看官方文档《Hot Module Replacement API》 。
回到上面示例,我们测试更新模块的功能。
这时我们修改 index.js
代码,来监听 hello.js
模块中的更新:
import hello from './hello.js'; const div = document.createElement('div'); div.innerHTML = hello(); document.body.appendChild(div); + if (module.hot) { + module.hot.accept('./hello.js', function() { + console.log('现在在更新 hello 模块了~'); + div.innerHTML = hello(); + }) + }
然后修改 hello.js
文件内容,测试效果:
- export default () => 'hi leo!'; + export default () => 'hi leo! hello world';
当我们保存代码时,控制台输出 "现在在更新 hello模块了~"
,并且页面中 "hi leo!"
也更新为 "hi leo! hello world"
,证明我们监听到文件更新了。
简单 Webpack HMR 使用方式就介绍到这,更多介绍,还请阅读官方文档《Hot Module Replacement》。
5. devServer 常用配置和技巧
5.1 常用配置
根据目录结构的不同,contentBase
、openPage
参数要配置合适的值,否则运行时应该不会立刻访问到你的首页。 同时要注意你的 publicPath
,静态资源打包后生成的路径是一个需要思考的点,取决于你的目录结构。
devServer: { contentBase: path.join(__dirname, 'static'), // 告诉服务器从哪里提供内容(默认当前工作目录) openPage: 'views/index.html', // 指定默认启动浏览器时打开的页面 index: 'views/index.html', // 指定首页位置 watchContentBase: true, // contentBase下文件变动将reload页面(默认false) host: 'localhost', // 默认localhost,想外部可访问用'0.0.0.0' port: 8080, // 默认8080 inline: true, // 可以监控js变化 hot: true, // 热启动 open: true, // 启动时自动打开浏览器(指定打开chrome,open: 'Google Chrome') compress: true, // 一切服务都启用gzip 压缩 disableHostCheck: true, // true:不进行host检查 quiet: false, https: false, clientLogLevel: 'none', stats: { // 设置控制台的提示信息 chunks: false, children: false, modules: false, entrypoints: false, // 是否输出入口信息 warnings: false, performance: false, // 是否输出webpack建议(如文件体积大小) }, historyApiFallback: { disableDotRule: true, }, watchOptions: { ignored: /node_modules/, // 略过node_modules目录 }, proxy: { // 接口代理(这段配置更推荐:写到package.json,再引入到这里) "/api-dev": { "target": "http://api.test.xxx.com", "secure": false, "changeOrigin": true, "pathRewrite": { // 将url上的某段重写(例如此处是将 api-dev 替换成了空) "^/api-dev": "" } } }, before(app) { }, }
5.2 技巧1:文件形式输出 dev-server 代码
dev-server 输出的代码通常在内存中,但也可以写入硬盘,产出实体文件:
devServer:{ writeToDisk: true, }
通常可以用于代理映射文件调试,编译时会产出许多带 hash 的 js 文件,不带 hash 的文件同样也是实时编译的。
5.3 技巧2:默认使用本地 IP 启动服务
有的时候,启动服务时,想要默认使用本地的 ip 地址打开:
devServer:{ disableHostCheck: true, // true:不进行host检查 // useLocalIp: true, // 建议不在这里配置 // host: '0.0.0.0', // 建议不在这里配置 }
同时还需要将 host 配置为 0.0.0.0
,这个配置建议在 scripts 命令中追加,而非在配置中写死,否则将来不想要这种方式往回改折腾,取巧一点,配个新命令:
"dev-ip": "yarn run dev --host 0.0.0.0 --useLocalIp"
5.4 技巧3:指定启动的调试域名
有时启动的时候希望是指定的调试域名,例如:local.test.baidu.com
:
devServer:{ open: true, public: 'local.test.baidu.com:8080', // 需要带上端口 port: 8080, }
同时需要将 127.0.0.1
修改为指定的 host,可以借助 iHost 等工具去修改,各个工具大同小异,格式如下:
127.0.0.1 local.test.baidu.com
服务启动后将自动打开 local.test.baidu.com:8080
访问
5.5 技巧4:启动 gzip 压缩
devServer:{ compress: true, }
三、HMR 基本原理介绍
从前面介绍中,我们知道:HMR 主要功能是会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
那么,Webpack 编译源码所产生的文件变化在编译时,替换模块实现在运行时,两者如何联系起来?
带着这两个问题,我们先简单看下 HMR 核心工作流程(简化版):
HMR 工作流程图.png
接下来开始 HMR 工作流程分析:
- 当 Webpack(Watchman) 监听到项目中的文件/模块代码发生变化后,将变化通知 Webpack 中的构建工具(Packager)即 HMR Plugin;
- 然后经过 HMR Plugin 处理后,将结果发送到应用程序(Application)的运行时框架(HMR Runtime);
- 最后由 HMR Runtime 将这些发生变化的文件/模块更新(新增/删除或替换)到模块系统中。
其中,HMR Runtime 是构建工具在编译时注入的,通过统一的 Module ID 将编译时的文件与运行时的模块对应起来,并且对外提供一系列 API 供应用层框架(如 React)调用。
💖注意💖:建议先理解上面这张图的大致流程,在进行后续阅读。放心,我等着大家~😃