这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战
本文讲述webpack更高级的系列,如模块热替换(Hot Module Replacement)、Tree Shaking等。
模块热替换Hot Module Replacement
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
为什么需要HMP?
在上节我们讲述了 webpack-dev-server后极大提高了我们的开发效率,只需要编写代码,webpack会自动的帮我们重新加载页面,但这也导致了我们一个问题就是,当我们页面中有多个模块时,页面更改了某个模块的状态,此时修改另一个模块代码后保存,页面会刷新,之前模块的状态会丢失,我们又的重新操作,是不是很麻烦呢,有没有一个功能是我改了哪个模块的代码编译后只更新这个模块而不影响其他的呢?当然可以,就是我们今天说的模块热替换Hot Module Replacement。可以举个例子看看,下面会围绕这个例子展开。
// count.js
let count = 0
function countFunc() {
const element = document.createElement('div');
// lodash(目前通过一个 script 引入)对于执行这一行是必需的
element.innerHTML = count;
element.classList.add('count');
element.onclick = function () {
count++
element.innerHTML = count;
}
return element;
}
export default countFunc;
// index.js
import _ from 'lodash';
import countFunc from './count'
import './index.css'
function component() {
const element = document.createElement('div');
// lodash(目前通过一个 script 引入)对于执行这一行是必需的
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
return element;
}
document.body.appendChild(component());
document.body.appendChild(countFunc());
页面渲染如下,我们点击了几次count选然后内容,加到了4(最开始是0)
这个时候我们如果改变了component函数渲染的内容,比如:
function component() {
const element = document.createElement('div');
// lodash(目前通过一个 script 引入)对于执行这一行是必需的
// - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.innerHTML = _.join(['Hello', 'webpack', 'Hot Module Replacement'], ' ');
element.classList.add('hello');
return element;
}
然后我们保存后页面会重新渲染,会变成如下结果
我们的数字会重新渲染成0,如果我们还想它变成4的话我们又得重新点击,很麻烦,我们希望改变了component函数后,countFunc这个模块渲染得数字是我们操作之后的,怎么办呢?就用到了模块热替换这个功能了。
如何使用HMP?
我们修改一下webpack的配置让他支持HMR,如下:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require("webpack");
// 删除了其他的配置
module.exports = {
devServer: {
static: './dist',
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
};
然后修改一下用例
import printMe from './print.js';
function component() {
const element = document.createElement('div');
const 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;
}
// './print.js'
export default function printMe() {
console.log('I get called from print.js!');
}
渲染结果如下:
此时我想改变一下按钮点击时间的输出,按之前分析页面会刷新,对吧,但是我们将函数printMe修改如下后在看。
export default function printMe() {
// console.log('I get called from print.js!');
console.log('Updating print.js...');
}
会发现页面数字3不会变成0,因为我们启用了HMR,没有刷新页面,只更新了部分模块。但问题是我们点击按钮后,输出还是之前的I get called from print.js!这是为什么呢?因为我们button没有重新渲染呀,绑定的事件还是之前的,这里我们需要手动通知他,当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。在index.js中加入以下代码
if (module.hot) {
module.hot.accept('./print.js', function () {
// 对更新过的 print 模块做些事情...
document.body.removeChild(element);
element = component(); // 重新渲染 "component",以便更新 click 事件处理函数
document.body.appendChild(element);
})
}
然后执行上述操作,一切都会和我们预期一样,这就是我对HMR介绍,还有更多配置可以查看webpack官网详细的API介绍:HMR API
Tree Shaking
Tree Shaking通常用于描述移除JavaScript上下文中未使用的代码,他依赖于ES2015模块语法的静态结构的特性,例如 import 和 export,使用require、module.exports是不行的。
新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯正 ES2015 模块)",由此可以安全地删除文件中未使用的部分。
为什么需要Tree Shaking?
举个例子,我们定义了两个函数用于数学计算,而在index.js中只引用了一个函数,而打包后会将两个函数的代码都打包进去,如下。
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// index.js
import { add } from './math'
const result = add(1, 2);
console.log(result);
我们未使用sub函数却打包了,也会发布到线上,有影响吗?对代码执行肯定没影响,但是是不是增大了代码体积了,在进行资源下载的时候下载了无用的资源。竟然这个函数的代码不会影响我们的功能,是不是可以考虑在打包的时候进行删除呢?当然可以,就是我们今天说的Tree Shaking
如何使用Tree Shaking?
在webpack.config.js中添加配置如下,需要将 mode 配置设置成development,以确定 bundle 不会被压缩:
mode: 'development',
optimization: {
usedExports: true,
},
添加配置后进行打包,结果如下:
/***/ "./src/math.js":
/*!*********************!*\
!*** ./src/math.js ***!
*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "add": () => (/* binding */ add)
/* harmony export */ });
/* unused harmony export sub */
const add = (a, b) => a + b;
const sub = (a, b) => a - b;
/***/ })
我们会发现sub函数还是被打包了,怎么回事呢?因为我们是在development环境,还需要进行调试、source-map等,不会去除我们的代码,会造成错误信息行数不对的等问题,但是会标识哪些代码是未使用的/* unused harmony export sub */。
除此之外我们还需要在 package.json 中添加 "sideEffects" 属性,用来提示 webpack compiler 哪些代码是无副作用的。如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。
{
"name": "webpack01",
"sideEffects": false,
}
sideEffects的值除了false外,还可以是一个数组,如果是数组的话的意思是告诉webpack这几个文件有副作用不要进行Tree Shaking。
最后,如果我们想要删除无用代码,只需要把mode模式变成production就好了,生产环境会默认开启压缩代码与 tree shaking和其他优化项。
本节内容详见:Tree Shaking
生产环境
development(开发环境) 和 production(生产环境) 这两个环境下的构建目标存在着巨大差异。在开发环境中,我们需要:强大的 source map 和一个有着 live reloading(实时重新加载) 或 hot module replacement(热模块替换) 能力的 localhost server。而生产环境目标则转移至其他方面,关注点在于压缩 bundle、更轻量的 source map、资源优化等,通过这些优化方式改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
虽然,以上我们将 生产环境 和 开发环境 做了细微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个 "common(通用)" 配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。此工具会引用 "common" 配置,因此我们不必再在环境特定(environment-specific)的配置中编写重复代码。将webpack.config.js替换为webpack.common.js、webpack.dev.js、webpack.prod.js,在webpack.common.js中编写公共代码,在webpack.dev.js、webpack.prod.js中编写不同环境的代码,根据不同的关注点进行配置,引入webpack.common.js公共配置,使用webpack-merge插件将配置合并后进行导出。
在package.json中修改脚本命令
"start": "webpack serve --open --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
在生产环境中,webpack会自动的开启各种最佳实践,如代码压缩、Tree Shaking、Code Splitting等方式来优化我们的代码,当然我们也可以自定义来优化我们的配置。