tree-shaking是一个在前端领域比较熟知的东西了。在没有深入了解前,一直以为他在项目中发挥了很大的作用。但是在看了许多文章说tree-shaking并没有什么卵用后,想自己深入了解一下,所以搜了许多博文,自己也在项目中试验了一下。基本了解了大致的流程。所以这篇博文主要是记录一下学习的成果。
tree-shaking是干啥的:
// app.js
export function A(a, b) {
return a + b
}
export function B(a, b) {
return a + b
}
// index.js
import {A} from '/app.js'
A(1, 2)
当index.js引用了app.js的A函数,如果tree-shaking起了作用,B函数是不会被打包进最后的bundle的。
但是
世界上有很多但是,而且往往但是后面的内容更加重要。
relies on the static structure of ES2015 module syntax, i.e. import and export.
在webpack官网当中有这样一句话,翻译成人话就是tree-shaking依赖es6的模块引入或输出语法。如果你的模块引入方式是require等等等乱七八糟的东西。tree-shaking将不会起到任何作用。
babel, webpack打包, uglifyJs
这三项东西东西是在我们开发中几乎绕不过去东西。而tree-shaking的关键点就在第一步,babel
虽然我不太了解webpack内部的运行机制(看过运行顺序的相关文章,但一直是懵比状态),但是看过这么多的文章后,上面三项的基本运行顺序还是理解的:
就是babel-loader先去处理js文件,处理过后,webpack进行打包处理,最后uglifyjs进行代码压缩。而关键就是babel怎么去处理js文件
babel的配置文件中有一个preset配置项:
{
"presets": [
["env", {
"modules": false //关键点
}],
"stage-2",
"react"
]
}
其中presets里面的env的options中有一个 modules: false,这是指示babel如何去处理import和exports等关键子,默认处理成require形式。如果加上此option,那么babel就不会吧import形式,转变成require形式。为webpack进行tree-shaking创造了条件。
在看过这些篇博文后,我本人对于tree-shaking有了一个基本的认识,那就是
babel首先处理js文件,真正进行tree-shaking识别和记录的是webpack本身。删除多于代码是在uglify中执行的
注:webpack在认定某块代码无用后,会再处理过程中写下一段注释。uglifyjs会根据这点注释去进行删除代码。
注释的大体内容(博文很久了,还是在webpack2.0时代,具体内容可能已经变化,但原理应该是不变的。)
function(module, exports, __webpack_require__) {
/* harmony export */ exports["foo"] = foo;
/* unused harmony export bar */;
function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}
tree-shaking,实战代码:
背景:在学习tree-shaking的过程中,如何支持class的tree-shaking是我一直关注的,而且大部分的文章还只停留在理论方面。所以最近自己写了一个demo,支持class的tree-shaking
1.首先使用loader去处理,实验阶段代码,编译成标准es代码。这样webpack内部的编译器才能正确识别代码。
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['babel-preset-stage-2', 'babel-preset-react']
}
},
'eslint-loader'
],
exclude: /node_modules/
}
]
}
2.然后通过webpack打包,并对代码进行tree-shaking.在打包完最后的bundle之后,和输出文件之前,对最后的bundle进行兼容性处理。
plugins: [
new UglifyJSPlugin(), // uglify要在babelPugin的前面
new BabelPlugin({ //在这个插件内部进行最后bundle的兼容性处理
test: /\.js$/,
babelOptions: {
presets: [env]
}
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new HtmlWebpackPlugin({
template: 'index.html'
}),
]
最后总结步骤:
先编译实验性质代码为标准代码,会涉及到babel-preset-stage-x插件
webpack打包代码并进行tree-shaking识别。
uglifyjs进行代码压缩,并根据webpack标识删除多余代码
4.对最后的代码进行兼容性处理涉及到babel-preset-env插件。
第三方类库的tree-shaking
在研究了许多第三方类库后,基本得出了一个结论:tree-shaking本质上是不能对大部分的第三方类库进行tree-shaking的.上面的实战代码,对于自己写的代码还有点用,但是只要涉及到第三方类库,基本就是歇菜。
ramda的输出文件:
大部分的react ui组件,以及函数工具类库。基本都是这样来进行模块输出,和引用的。
export { default as F } from './F';
export { default as T } from './T';
export { default as __ } from './__';
export { default as add } from './add';
export { default as addIndex } from './addIndex';
export { default as adjust } from './adjust';
export { default as all } from './all';
export { default as allPass } from './allPass';
export { default as always } from './always';
export { default as and } from './and';
export { default as any } from './any';
export { default as anyPass } from './anyPass';
export { default as ap } from './ap';
export { default as aperture } from './aperture';
export { default as append } from './append';
export { default as apply } from './apply';
export { default as applySpec } from './applySpec';
...
这样的文件结构是无法进行tree-shaking的
// 只要是你在代码中引用了一个方法,那么你肯定将所有的代码都引入了进来
import {path} from 'ramda'
唯一的解决方法就是直接到具体的文件夹去引用,而不是在根index.js里面去引用。
import path from 'ramda/src/path'
但是如果每一次引用都是这样去写,开发的效率就无法保证,所以基本上有点追求的技术团队,基本上会再类库的基础上,开发一个babel的插件以支持代码的tree-shaking。
像著名的antd,以及ramda等都开发了相应的插件。
babel-plugin-ramda:此插件会默认将你写的代码转化为tree-shaking的代码
from:
import {path} from 'ramda'
to
import path from 'ramda/src/path'
而本人也在了解了以上东西后,为本公司的ui组件开发了一个插件:
babel-plugin-b-rc