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