在上週末廣州舉辦的feday中,webpack的核心開發者Sean在介紹webpack插件系統原理時,隆重介紹了一箇中國學生於Google夏令營,在導師Tobias帶領下寫的一個webpack插件,webpack-deep-scope-analysis-plugin,這個插件可以大大提升webpack tree-shaking的效率。前端
tree-shaking 做爲 rollup 的一個殺手級特性,可以利用ES6的靜態引入規範,減小包的體積,避免沒必要要的代碼引入,webpack2也很快引入了這個特性,可是目前,webpack只能作比較簡單的解決方案,好比: webpack
這個例子中,webpack會尋找引入變量的引用,當發現沒有對isNumber的引用時,就會去除isNumber的代碼。這其實不太實用,畢竟在如今的vscode中,沒有引用的變量在ide中都會灰顯提示,通常不會犯這種import某個模塊卻不用的錯誤了。git
若是是接下來這種引入方式呢,我寫了一個demo以下github
這個例子很是簡單,若是用圖來表示是這樣 web
在index.js中引入了func.js中的func2,並無引入func1,可是func1引入了lodash。webpack檢查的時候發現func.js中的確用到了lodash,因此不會把lodash去掉。實際上,咱們根本沒用到它。json
webpack-deep-scope-analysis-plugin就能夠解決這種判斷。babel
引入前 app
引入後 ide
85.8kb -> 不到1kb函數
固然,我這裏是標題黨了,由於這裏直接把一個lodash庫給去掉了,因此變化才這麼驚人。可是即便在實際項目中,咱們也能輕易用一個插件減小大量的沒必要要的引入。
那麼這個插件是怎麼去解決這個問題的呢?這裏根據原做者在Medium上寫的文章,簡單介紹一下他的作法。
webpack的原理,其實就是遍歷全部的模塊,把它們打包成一個文件,在這個過程當中,它就知道哪些export的模塊有被使用到。那咱們一樣也能夠遍歷全部的scope(做用域),簡化沒有用到的scope,最後只留下咱們須要的。
上圖中,func5層層引用fun4 fun3 fun2 fun1,最後解析出來其實只使用了deepEqual模塊。
什麼是scope呢,其實scope在各個語言中都有存在,在Wikipedia中是做爲計算機術語,有更詳細的解釋,我以爲能夠翻譯爲做用域或者上下文,在ECMAScript中,有如下明確的定義:
// module scope start
// Block
{ // <- scope start
} // <- scope end
// Class
class Foo { // <- scope start
} // <- scope end
// If else
if (true) { // <- scope start
} /* <- scope end */ else { // <- scope start
} // <- scope end
// For
for (;;) { // <- scope start
} // <- scope end
// Catch
try {
} catch (e) { // <- scope start
} // <- scope end
// Function
function() { // <- scope start
} // <- scope end
// Scope
switch() { // <- scope start
} // <- scope end
// module scope end
複製代碼
在ES6中,module是一種根做用域,只有function和class才能做爲子做用域被導出,因此咱們解析的時候,不會把全部的scope都做爲節點算進去。
咱們提到的這個webpack插件,正是內置了這樣一個scope分析器,它可以從入口文件中分析出scope的引用關係,最後排除掉全部沒有用到的模塊。
固然,這個插件也並非本身作了全部的事情,它也是依賴於了前人的工做。 escope 是一個分析ES中scope的工具,插件做者將它改爲了ts版本集成到了插件中,而且利用了webpack暴露的接口,能夠解析出來的模塊的AST樹,基於這個AST就能夠交給escope分析出scope的引用關係。
凡事不能完美,這個插件也有一些狀況會致使判斷失誤
比較典型的是如下這個例子:
import { isNull } from 'lodash-es';
var fun = 1;
fun = function scope(...args) {
return isNull(...args);
}
export { fun }
複製代碼
這個例子中fun變量一開始被賦值爲數字,而後被賦值成一個函數,可是scope分析器會直接跳過這個變量,不把它看成一個單獨的scope。
// copy from rambda/es/allPass.js
import _curry1 from './internal/_curry1';
import curryN from './curryN';
import max from './max';
import pluck from './pluck';
var allPass = /*#__PURE__*/_curry1(function allPass(preds) {
return curryN(reduce(max, 0, pluck('length', preds)), function () {
var idx = 0;
var len = preds.length;
while (idx < len) {
if (!preds[idx].apply(this, arguments)) {
return false;
}
idx += 1;
}
return true;
});
});
export default allPass;
複製代碼
在這個例子中,import allPass
會致使_curry1
的運行,所以它不會被看成一個單獨的scope,由於它可能會有一些「反作用」,好比改變某個所有變量,對全局形成影響。 因此做者給了個方案,能夠在這個函數前加/*#__PURE__*/
,這樣就會把這個函數視爲無反作用的純函數,若是咱們沒有import allPass
,它引用的其餘模塊都會被去除。
首先,要用到tree-shaking,必然要保證引用的模塊都是ES6規範的。這也是爲何我在前面的demo中,引入的是lodash-es
而不是lodash
。
在項目中,注意要把babel
設置module: false
,避免babel將模塊轉爲CommonJS規範。引入的模塊包,也必須是符合ES6規範,而且在最新的webpack中加了一條限制,即在package.json
中定義sideEffect: false
,這也是爲了不出現import xxx
致使模塊內部的一些函數執行後影響全局環境,卻被去除掉的狀況。
當時跟這位插件做者溝通,他說未來有可能Tobias會把這個插件內置到webpack中,這也是符合webpack4零配置的趨勢。可是咱們也看獲得,要將前端工程的dead code elimination作到和其餘靜態語言同樣好,靠這些工具是遠遠不夠的,模塊自身也必須配合作到符合規範。
《IVWEB 技術週刊》 震撼上線了,關注公衆號:IVWEB社區,每週定時推送優質文章。