引言
本文主要介绍
- sourcemap的分析与使用
- 页面公共资源的提取
- Tree Shaking的原理与使用
1.sourcemap
Source map是一个信息文件,里面储存着位置信息。有了它,出错的时候,开发工具将直接显示原始代码,而不是转换后的代码。一般建议开发环境时开启(快速定位错误信息,提升开发效率),线上环境关闭(避免暴露业务代码)。 感兴趣的可以看看 阮一峰老师的Source Map详解 对于如此实用的soucemap,webpack也帮我们开放了自定义配置选项——devtool。
1.1 了解webapck的devtool
devtool是webpack的一个options。用于控制是否以及如何生成 source map。默认不开启。
const path = require('path');
module.exports = {
devtool: 'none', // SourceMap
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js', // 文件名
path: path.resolve(__dirname, 'dist') // 文件夹
}
}
2.2 devtool的可选值
这里直接截取 官方文档的表格说明:
表格中: `+++` 非常快速, `++` 快速, `+` 比较快, `o` 中等, `-` 比较慢, `--` 慢
其中一些值适用于开发环境,一些适用于生产环境。
看到表格的同志不要被这么多的可选值给吓唬住啦。其实这些选项大多都是由这几个关键字组合而成的,咱只需要记住关键字的意思就可以猜出个大概了~
| 关键字 | 代表含义 |
|---|---|
| eval | 使用eval包裹模块代码 |
| source-map | 产生.map文件 |
| cheap | 不包含列信息 |
| inline | 将.map作为DataUrl嵌入,不单独生成.map文件 |
| module | 包含loader的sourcemap |
2.3 devtool最佳实践
对于开发环境,通常希望更快速的 source map,需要添加到 bundle 中以增加体积为代价,但是对于生产环境,则希望更精准的 source map,需要从 bundle 中分离并独立存在。 一般情况下,推荐开发环境使用
devtool: 'cheap-module-eval-source-map',
生产环境使用
devtool: 'cheap-module-source-map',
2.页面公共资源的提取
2.1 externals的配置与使用
对于不常更新的第三方库,我们可以通过配置externals使其脱离webpack打包,而是采用CDN的方式引入。这样既可以加快打包速度,减少bundle体积,又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用(一般都以import方式引用使用)。这里以React,ReactDOM为例
方式一: 使用插件HtmlWebpackExternalsPlugin
npm i HtmlWebpackExternalsPlugin -D
在webpack的plugins中使用HtmlWebpackExternalsPlugin
const HtmlWebpackExternalsPlugin = require('HtmlWebpackExternalsPlugin');
module.exports = {
...
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://unpkg.com/react@16/umd/react.production.min.js',
global: 'React'
},
{
module: 'react-dom',
entry: 'https://unpkg.com/react-dom@16/umd/react-dom.production.min.js',
global: 'ReactDOM'
}
]
}),
new HtmlWebpackPlugin({
meta: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'},
favicon: './src/images/favicon.ico',
scriptLoading: 'blocking',
template: path.join(__dirname, './src/index.html'),
minify: true
})
]
}
执行打包命令,在浏览器中访问dist/index.html,可以看到index.html中动态添加了如下代码:
观察下使用插件前后两次打包文件体积大小变化
使用前:
使用后:
方式二:配置externals
首先在index.html页面引入静态资源
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
然后配置externals
const HtmlWebpackExternalsPlugin = require('HtmlWebpackExternalsPlugin');
module.exports = {
...
externals: {
'react': 'React',
'react-dom': 'ReactDOM'
}
}
执行打包命令,在浏览器中访问dist/index.html,正常访问。同上,React与ReactDOM没有被webpack打包。
2.2 分离基础包
当我们多个模块使用共同的资源时,为了减少打包体积,可以使用webpack4内置的 splitChunksPlugin 将公共资源提取出来。
const HtmlWebpackExternalsPlugin = require('HtmlWebpackExternalsPlugin');
module.exports = {
...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}
打包后可以看到,react与react-dom已经被分离出来了
更多配置项戳 split-chunks-plugin配置项文档
3. Tree Shaking的原理与使用
3.1 什么是tree shaking
tree shaking——摇树。把webapck形象成一棵树,入口文件就是树的主干,各个依赖模块就是树的枝杈。”摇树“顾名思义,就是把webpack这颗大树上的没有用的枝杈给摇掉。它的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)。
webpack2及以后的版本,内置了tree shaking的功能。
3.2 如何触发tree shaking
什么样的代码会被认定为是无用的不影响输出的代码并被webpack摇掉呢?
- 1 永远不会到达(被执行)的代码
- 2 只写不读的代码
- 3 执行的结果永远不会被用到的代码
例如:
// 永远不被执行的代码
if (false) {
console.log('我还在吗?');
}
// 只写不读的代码
let unusedVar = 'var 我还在吗'
// 执行的结果永远不被用到的代码
const Search = () => {
// 永远不被执行的代码
if (false) {
console.log('我还在吗?');
}
// 只写不读的代码
let unusedVar = 'var 我还在吗'
// 执行的结果永远不被用到的代码
const fn = () => {
return 'return 我还在吗'
}
useEffect(() => {
fn()
}, [])
console.log('我还在吗?')
const [comp, setComp] = useState();
const clicked = () => {
import('./test.js').then(text => {
setComp(text.default);
})
}
return <div>
<div className="search-text less" onClick={clicked}>Search Text</div>
{comp && comp}
</div>
}
用上面的代码,打包后看看webapck的输出文件:
以上无用代码都被shaking掉了~
3.3 tree shaking原理
tree shaking主要是利用ES6 模块的特点,在代码的预编译阶段,将无用的代码找出并标记,然后在uglify阶段将标记的无用代码擦除。因为需要再预编译阶段就确定哪些是无用代码,所以tree shaking只能在静态modules下工作。ECMAScript 6 模块加载是静态的,因此整个依赖树可以被静态地推导出解析语法树。
ES6的特点:
- 只能作为模块顶层的语句出现
- import的模块名只能是字符串常数,不能是变量
- import后不能修改
这些特点保证了模块是静态的。
注意:为了利用 tree shaking 的优势, 你必须使用 ES2015 模块语法(即 import 和 export)并且确保没有编译器将 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅文档)。