npm包支持tree-shaking能夠實現按需引入,對生產環境優化有重要意義,本次實踐參考antd,對移動端組件庫@fx-ui/jdy-design-mobile進行了改造node
在闡述理論以前,先看一下該npm包處理先後的體積對比,用實力說話💪react
處理前該npm包體積418.74KB webpack
處理後該npm包體積274.19KB git
減小的145KB就是該npm包被tree-shaking剔除的代碼es6
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export.
複製代碼
webpack文檔給出的解釋中有個關鍵詞就是ES2015
,大多數狀況下,咱們開發的npm包爲了更好的瀏覽器兼容性,會用babel將es6轉譯成es5或者更低的版本,從而丟失了tree-shaking的能力。github
另外,tree-shaking須要配合壓縮工具例如UglyfiJs來使用,UglyfiJs會識別代碼中的/*#__PURE__*/
標註,並將未被使用的函數移除:web
var renderContent = function renderContent() {
return (
/*#__PURE__*/
React.createElement("div", {
className: cls,
style: getStyle(),
ref: elRef
}, children)
);
};
複製代碼
你不須要手動添加這些標註,babel會在轉化的時候自動幫你加上算法
打包工具(Webpack, Rollup)會優先經過package.json來判斷一個npm包是否支持tree shaking:typescript
sideEffects
以外的文件時,纔會應用tree-shaking{
"sideEffects": [
"dist/*",
"es/components/**/style/*",
"lib/components/**/style/*",
"*.less"
]
}
複製代碼
main
字段的以外的module
字段,該字段將指定npm包的es6版本{
"main": "lib/index.js",
"module": "es/index.js",
}
複製代碼
打包工具優先經過module和sideEffects指定的路徑來引入該包的es6版本,並應用tree-shaking,若是發現es6版本不可用,則會使用備選項,即main
字段指定的低版本。npm
這裏有個疑問就是爲何不直接讓pkg.main指向es6格式的源碼呢? 有2個緣由:
大部分的開發者在使用babel的時候都會避開node_modules來提升編譯速度,此時若是使用es6的包則須要配置複雜的編譯規則來將該npm包加入白名單。
有些開發者可能會在nodejs環境中引用該npm包,好比lodash,此時es6就不適合了
雖然webpack的功能更強大,但gulp能夠更好的控制整個打包流程,相比於項目的開發,gulp和rollup更適合庫的開發。
lib/
和 es/
參考antd來講,通常具備tree-shaking機制的包都會有lib/
和es/
兩個文件夾,gulp會經過gulp-typescript
和gulp-babel
將src
目錄下的.ts
, .tsx
完整的映射到lib
或者es
目錄,當咱們在babel配置中添加modules: false
時,轉換出的就是es6語法的js,若是不添加modules
字段,則默認轉換出es5:
const getBabelConfig = (modules) => ({
presets: [
resolve('@babel/preset-react'),
[
resolve('@babel/preset-env'),
{
modules,
targets: {
browsers: [
'last 2 versions',
'Firefox ESR',
'> 1%',
'ie >= 9',
'iOS >= 8',
'Android >= 4',
],
},
},
],
],
plugins: []
});
複製代碼
src的同級目錄
到如今爲止,一切都很完美,但實際狀況卻稍微複雜一些,好比說像jdy-design-mobile
這個項目下除了src還有個同級目錄biz,打包後生成了business文件夾,項目中可能會直接經過路徑來引用business下的模塊,好比:
import { SearchInput } from '@fx-ui/jdy-design-mobile/business';
複製代碼
這個時候就無法經過package.json
中的字段來動態引用了,因而business下面也必須存在2個目錄lib
和es
,而後在項目中手動引入business/lib
或者business/es
,此時是否使用tree-shaking是由開發者決定的。
biz -> business/(lib|es) 和 src -> (es|lib)的步驟是同樣的,但前者由於目錄結構發生了很大變化,須要對import語句作一些處理:
修正biz和src之間的相對引用
biz以前引用的是src,如今business(lib|es)引用(lib|es)
// 針對business/es
gulp.src(rawSourceBiz).pipe(replace(/(import.*from.*)\/src(.*)/g, '$1/es$2')
// 針對business/lib
gulp.src(rawSourceBiz).pipe(replace(/(import.*from.*)\/src(.*)/g, '$1/lib$2')
複製代碼
修正biz目錄下的模塊間的相對引用
原來的buessiness/somefile.js
如今變成了buessiness/(lib|es)/somefile.js
由於如今的buessiness/(lib|es)/somefile.js
已是babel處理以後的了,咱們無法再經過字符串替換的方式來處理import語句,可是babel提供了自定義插件的方式,容許咱們在ast階段處理字符串,好比ImportDeclaration
就對應着源代碼中的import語句,這時再作替換就很方便了:
function replacePath(path) {
if (path.node.source && /\/(lib|es)/.test(path.node.source.value)) {
const esModule = path.node.source.value.replace(/\/(lib|es)/, '/../$1');
path.node.source.value = esModule;
}
}
// babel插件,修改import語句的字符串
function replaceLiborEs() {
return () => ({
visitor: {
ImportDeclaration: replacePath,
ExportNamedDeclaration: replacePath,
},
});
}
複製代碼
若是咱們使用的npm包A依賴了包B,那麼當咱們選擇A的es6版本時,它所依賴的包B也應該自動切換到es6版本,在理想狀況下,npm的模塊機制已經自動實現了這個算法。
但若是就像上面說的,當包B存在src的同級目錄時,狀況就會變得複雜,若是A在src源碼中引用了B的lib版本,好比:
// a.js
import SomeBModule from 'B/business/lib'
複製代碼
那麼在A的es6版本代碼中則必須將B/business/lib
改爲B/business/es
,才能將tree-shaking的效果發揮到極致,antd也是利用自定義babel插件replaceLib來實現這一替換的:
const { dirname } = require('path');
const fs = require('fs');
const { getProjectPath } = require('./utils/projectHelper');
function replacePath(path) {
if (path.node.source && /\/lib\//.test(path.node.source.value)) {
// 替換import語句中lib爲es
const esModule = path.node.source.value.replace('/lib/', '/es/');
// 確保包B的es6版本確實存在
const esPath = dirname(getProjectPath('node_modules', esModule));
if (fs.existsSync(esPath)) {
path.node.source.value = esModule;
}
}
}
function replaceLib() {
return {
visitor: {
// 修改ast的ImportDeclaration節點
ImportDeclaration: replacePath,
ExportNamedDeclaration: replacePath,
},
};
}
複製代碼
npm包的tree-shaking機制是在確保可使用es5代碼的基礎上,提供es6代碼做爲可選項。在改造的過程當中,除了在package.json聲明字段,還須要注意打包先後的文件引用路徑的變化。
它的樣式通常採用總體引入(不作tree-shaking),固然若是須要按需引入樣式也能夠配合babel-plugin-import來作,不過antd官方已經不推薦了