為了降低首屏代碼大小,對于一些大的第三方庫或者團隊的基礎(chǔ)工具庫,需要按需導(dǎo)入模塊。如:
import Button from 'antd/lib/button';
但這在需要導(dǎo)入非常多的組件場景時,開發(fā)繁瑣,體驗不友好。在這些組件庫的官方文檔或者社區(qū)會推薦一些babel插件,幫助達(dá)到良好的開發(fā)體驗和性能優(yōu)化。
本文將詳細(xì)探究這些工具的原理。
antd等UI組件庫按需加載
在使用antd的老版本時,會推薦使用babel-plugin-import工具按需導(dǎo)入組件。工具可以做到如下的轉(zhuǎn)換:
import { Button } from 'antd';
ReactDOM.render(<Button>xxxx</Button>);
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style');
ReactDOM.render(<_button>xxxx</_button>);
antd和element-ui都叫做按需加載,但我覺得叫做按需導(dǎo)入更加貼合意境。
eleemnt-ui配套按需導(dǎo)入工具為babel-plugin-component。
babel-plugin-import工具,會在編譯時會分析模塊導(dǎo)入語句。當(dāng)為需要導(dǎo)入的目標(biāo)庫組件時,會將原導(dǎo)入刪除,生成多個新的導(dǎo)入(如果你配置了style,會額外導(dǎo)入樣式)。由于重新生成的導(dǎo)入語句會導(dǎo)致模塊變量發(fā)生變化(如上文案例演示中Button會被轉(zhuǎn)換為_button),這導(dǎo)致插件程序還會分析當(dāng)前模塊的所有變量,對使用原變量的語句中,將變量名修復(fù)。
最新的antd已經(jīng)推薦使用webpack的tree shaking機制來按需加載。babel-plugin-import也基本沒什么更新。對于vue生態(tài)來說,很多組件庫為了支持全局注冊的方式,無法使用tree shaking。
雖然babel-plugin-import等工具支持一些配置定制,但還是存在下面缺點:
- 每個插件都是針對特定的組件庫,需要符合特定的目錄和文件維護(hù)規(guī)范。如babel-plugin-import導(dǎo)入的模塊需要支持目錄為:
|--component
|----index.js
|----*.js
|----style
|------index.js
|------*.css
- babel-plugin-import由于底層實現(xiàn)改變了導(dǎo)入的模塊變量,然后再全模塊枚舉語句類型中找到使用變量將其修復(fù),再某一些非常不常見的語句中,會出現(xiàn)沒有轉(zhuǎn)變模塊變量導(dǎo)致語法錯誤問題。
對任意庫支持按需導(dǎo)入
如果也想對項目中公共基礎(chǔ)模塊(公共組件,公共工具文件等)支持源碼中全量導(dǎo)入但實際按需加載的效果,除了可以fork babel-plugin-import等工具調(diào)整邏輯來實現(xiàn),還可以使用工具babel-plugin-transform-imports來支持。
babel-plugin-transform-imports是自己配置轉(zhuǎn)換格式,如可以達(dá)到下面效果:
import { Row, Grid as MyGrid } from 'react-bootstrap';
import { merge } from 'lodash';
↓ ↓ ↓ ↓ ↓ ↓
import Row from 'react-bootstrap/lib/Row';
import MyGrid from 'react-bootstrap/lib/Grid';
import merge from 'lodash/merge';
此時需要的配置為:
{
"plugins": [
["transform-imports", {
"react-bootstrap": {
"transform": "react-bootstrap/lib/${member}",
"preventFullImport": true
},
}]
]
}
transform是可以支持函數(shù)的,實現(xiàn)高級定制,擴展性非常高。
且babel-plugin-transform-imports實現(xiàn)的方式是直接對導(dǎo)入語句進(jìn)行了修復(fù),比babel-plugin-import更加優(yōu)雅和適用性更廣,閱讀他的源碼也可以發(fā)現(xiàn)比babel-plugin-import簡潔的多。
babel-plugin-import底層是將原來導(dǎo)入刪除,然后生成新的目標(biāo)導(dǎo)入,導(dǎo)致了導(dǎo)入模塊的變量發(fā)生了變更。
但babel-plugin-transform-imports同樣有一些缺陷:它無法由一個導(dǎo)入生成多個導(dǎo)入。這也就意味著使用babel-plugin-transform-imports無法對antd,element-ui這樣的UI組件庫進(jìn)行模塊按需導(dǎo)入:這些UI組件庫,除了js模塊的導(dǎo)入外,往往還有一個樣式模塊。
可以fork babel-plugin-transform-imports擴展其邏輯,如這個庫babel-plugin-transform-module-imports。我參照他的實現(xiàn)原理,實現(xiàn)了一個工具babel-plugin-transform-import-module,可以支持更多的配置,基本可以使用它滿足所有的模塊在源碼中全量導(dǎo)入但實際按需導(dǎo)入的效果。
像lodash一樣根據(jù)調(diào)用按需導(dǎo)入
在使用lodash是,可以使用babel-plugin-lodash插件進(jìn)行導(dǎo)入優(yōu)化。能夠?qū)ⅲ?/p>
import _ from 'lodash'
import { add } from 'lodash/fp'
const addOne = add(1)
_.map([1, 2, 3], addOne)
在編譯時轉(zhuǎn)換為:
import _add from 'lodash/fp/add'
import _map from 'lodash/map'
const addOne = _add(1)
_map([1, 2, 3], addOne)
利用babel-plugin-lodash可以提升lodash使用時的開發(fā)體驗:代碼中是全量導(dǎo)入使用,無需關(guān)注其內(nèi)部結(jié)構(gòu),將按需導(dǎo)入對應(yīng)的工具函數(shù)模塊交給底層babel插件處理。使用者除了不需要操心是否導(dǎo)入額外的代碼外,還可以配合typescipt使用,擁有更好的代碼提示,從而降低認(rèn)知。
當(dāng)前這個插件只支持lodash庫使用。借鑒其理念,可以實現(xiàn):
import utils from 'utils@'
utils.downloadFile('path/to/file')
↓ ↓ ↓ ↓ ↓ ↓
import _downloadFile from 'utils@/downloadFile'
_downloadFile('path/to/file')
如果改造更加深入一點,可以對一些市面上流行庫的優(yōu)化,如antd庫的使用:
import * as Antd from 'antd';
ReactDOM.render(<Antd.Button>xxxx</Antd.Button>);
↓ ↓ ↓ ↓ ↓ ↓
import Button from 'antd/lib/button';
import('antd/lib/button');
ReactDOM.render(<Button>xxxx</Button>);
上面的效果在我實現(xiàn)的一個babel插件babel-plugin-module-call-import都可以支持。如果你需要完全像babel-plugin-lodash一樣根據(jù)文件目錄結(jié)構(gòu)自動轉(zhuǎn)換加載器,可以直接fork源碼上調(diào)整。
我的團隊里面多個項目基本使用這種方式。
在vue代碼中根據(jù)標(biāo)簽按需導(dǎo)入
在vue應(yīng)用注冊組件時,有兩種方式:全局注冊和局部注冊。全局注冊開發(fā)體驗式最好的,但會導(dǎo)入UI組件庫的所有代碼,增加很多無用代碼從而首屏激增。如何做到像全局注冊組件那樣開發(fā)友好,又支持按需導(dǎo)入組件提高性能呢?
這里提供一個方案:在vue文件的模板在解析的時候,記錄模板文件使用的標(biāo)簽,然后在文件對應(yīng)的js代碼編譯的時候,根據(jù)標(biāo)簽自動導(dǎo)入UI組件,然后在組件配置對象的components屬性中注冊上去。
假如代碼:
<template>
<div>
<el-button>按鈕</el-button>
</div>
</template>
<script>
export default {
created() {
},
};
</script>
在編譯時,將會轉(zhuǎn)換類似于:
<template>
<div>
<el-button>按鈕</el-button>
</div>
</template>
<script>
import ElButton from 'element-ui/lib/form-item';
export default {
components: {
ElButton,
},
created() {
},
};
</script>
我開發(fā)了兩個工具實現(xiàn)持這個方案:
- babel插件:babel-plugin-vue-import-component-by-tag
- webpack loader: vue-record-tags-loader
只支持與vue2版本,vue3可以參照方案自己實現(xiàn)下。
你有什么更好的想法,歡迎討論。