《Vue組件庫工程探索與實踐》系列文章第二篇,聊一聊組件庫按需加載功能。javascript
一個組件庫一般有數十個組件,隨着版本迭代組件數量還可能進一步增長。組件庫文件的體積也隨之膨脹,動輒幾百KB。而咱們的業務項目中,有可能只用到了這個組件庫的少數幾個組件,這時把整個組件庫打包進去,非但沒有必要,還會徒增項目構建文件的體積,這與應用性能優化的方向是背道而馳的。所以,組件庫有必要提供一種更靈活的組件引用方式,容許應用只引用指定的組件。事實上,主流的組件庫基本都具有「按需加載組件」功能。css
最簡單的「按需加載組件」實現方式,就是在應用中直接引用所需組件的源文件,在應用的構建工具中跟應用一塊兒構建。說它簡單,是由於這種方式幾乎不須要組件庫作什麼工做,應用直接引用組件源碼,並不須要通過組件庫的構建過程。vue
這種方式的侷限性也大都與「組件未經組件庫構建」有關。在應用中構建這些組件,就意味着應用的構建工具必需要具有構建這些組件的能力。好比須要有編譯Vue模板、編譯ES6+語法、編譯Scss/Less語法、支持postcss等的能力,若是說上面這些功能很基礎,大多數應用的構建工具都能支持,那麼組件可能還有一些不太常見或者組件庫特有的功能,好比處理SVG、定製主題、國際化等等,一般應用構建工具不具有或者依賴於組件庫配置文件,這就給直接在用戶的應用中編譯組件源碼帶來了困難。另外一方面,未經構建的組件模塊化接口單一,沒法直接在其餘模塊化場景和非模塊化場景使用。還有,若是組件庫支持直接引用組件源碼,則須要把全部組件源碼隨NPM包一塊兒發佈,可能會致使npm包過大,看起來並非一個好主意。java
好吧,咱們換個思路,不直接引用組件源碼,而是讓組件庫對這些用戶指定的組件(而非所有組件)進行構建,生成一個自定義版本的組件庫給用戶應用使用。這就須要組件庫與用戶進行交互,收集用戶所須要的組件信息,而後將指定組件編譯成一個自定義版本的庫文件。這種自定義構建方案常見的狀況有兩種,一種是經過網頁收集信息,在服務端進行構建。遙想當年jQuery時代,jQuery-UI庫提供的自定義構建下載方式[1],讓用戶在線選擇所需組件,而後在服務端進行編譯,完成後提供給用戶下載(固然,服務端也可能存在已經提早編譯完的各類組合的構建包)。那個時代已然遠去,現在下載安裝組件庫「政治正確」的姿式是經過npm/Yarn。node
另外一種方案是經過命令行界面(CLI)收集信息並在客戶端構建。好比jQuery的「不一樣父異母」的小兄弟Zepto.js,官方標準包裏只包含部分模塊,若是須要增長或移除模塊就須要進行自定義構建了:在Zepto.js項目目錄下安裝依賴,在MODULES中指定須要的模塊,而後執行npm run-script dist進行構建,完事兒後dist目錄下zepto.js和zepto.min.js就是自定義構建出來的包,拿到項目裏使用便可。這種方式節約服務器資源,甚至不須要本身的服務器。jquery
# do a custom build
$ MODULES="zepto event data" npm run-script dist
# on Windows
c:\zepto> SET MODULES=zepto event data
c:\zepto> npm run-script dist
複製代碼
NutUI 1.x 時期的按需加載方案,相似上述第二種方案,較之還有一些改進。用戶在NutUI 1.x項目中安裝依賴,而後執行npm run custom命令,這時命令行界面會列出全部組件名,用戶選擇須要的組件後回車,組件庫的構建工具會將所選組件進行構建,獲得與完整組件庫文件同名的構建文件nutui.js,正常使用便可。webpack
只看這種方案自身,彷佛沒什麼問題,確實實現了按需構建,並且並不繁瑣,只是幾行命令而已,也不須要架設服務器。可是若是結合用戶使用場景來看,問題仍是很多:git
因而NutUI 2.0時,咱們決定對按需加載功能進行從新設計。咱們參考了業界優秀組件庫的實現方案。在組件庫構建時,除了構建完整的組件庫包之外,還把每一個組件單獨構建了一個包,這樣就能夠獨立引用每個組件了。github
// 加載構建後的組件JS
import Button from '@nutui/nutui/dist/packages/button/button.js';
//加載構建後的組件CSS
import '@nutui/nutui/dist/packages/button/button.css';
複製代碼
webpack的中如何實現構建多個bundle呢?主要是entry選項的配置,entry的值一般是一個字符串,其實它還能夠是一個對象。咱們新增一個webpack配置文件,基於組件庫的組件配置文件生成一個對象,key是組件名,value是組件的入口js文件,將此對象做爲該配置文件的entry選項值便可,其餘配置與完整版的組件庫webpack配置文件一致(輸出目錄可根據須要自行配置)。構建時執行這兩個配置文件,便可構建出一個完整版的組件庫包和每一個組件獨立的包。web
const cptConf = require('../src/config.json');
const entry = {};
cptConf.packages.map((item)=>{
entry[cptName] = `./src/packages/${item.name.toLowerCase()}/index.js`;
});
module.exports = {
entry
};
複製代碼
若是用戶項目中使用了多個組件,這種分別引用每一個組件及其樣式文件的寫法仍是略顯繁瑣,URL拼寫也容易出錯。代碼潔癖患者的感覺也須要顧及啊~
拋開技術實現和兼容性不談,比較理想的、面向將來的寫法應該是ES6 modules風格的寫法,由於一衆的模塊化方案中,這是親兒子。
import { Button,Switch } from '@nutui/nutui';
複製代碼
咱們考慮支持這種寫法,並提供一個工具在用戶應用編譯階段將代碼自動轉換爲組件單獨引用的寫法:
import Button from '@nutui/nutui/dist/packages/button/button.js';
import Switch from '@nutui/nutui/dist/packages/switch/switch.js';
import '@nutui/nutui/dist/packages/button/button.css';
import '@nutui/nutui/dist/packages/switch/switch.css';
複製代碼
承擔這種轉碼工做最適合的人選非Babel莫屬了。大多數用戶的項目腳手架都會安裝Babel,用來進行ES6+語法向低版本語法的轉換,咱們只須要提供一個Babel的插件,使其在轉換的過程當中捎帶着把咱們組件按需加載的語法也給轉換了便可。咱們先來了解一下Babel的工做原理。
Babel的轉碼工做大體分爲三個階段:
抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。
咱們的Babel插件@nutui/babel-plugin-separate-import[2]的大體工做原理是在代碼被解析成AST抽象語法樹以後,遍歷語法樹找到形如import { Button,Switch } from '@nutui/nutui';的語法相關節點,轉換成單獨引用組件的語法,最後再生成代碼字符串。
這只是基本原理,實際狀況比較複雜,由於還須要考慮樣式文件類型、主題換膚、國際化等因素,這裏就不展開了。下面說下這個插件的基本使用。
經過npm/yarn安裝@nutui/babel-plugin-separate-import 在項目的Babel配置文件(如.babelrc)中配置插件
{
"plugins": [
["@nutui/babel-plugin-separate-import", {
"style": "css"
}]
]
}
複製代碼
而後就可使用ES6 modules風格的語法引用所需的組件了
import Vue from 'vue';
import { Button,Switch } from '@nutui/nutui';
Vue.use(Button);
Vue.use(Switch);
複製代碼
既然說到Babel與AST,咱們不妨進行一些延展(這部份內容屬贈送性質)。Babel自帶的AST操做相關模塊能夠在須要AST的場景獨立使用,無需再安裝其餘AST工具。
好比在NutUI 2.x項目中,咱們爲新增組件提供了一個命令npm run add,可根據錄入信息自動生成新組件的模板,並更新配置文件。其中一個須要更新的組件庫配置文件是src目錄下的nutui.js文件,這個文件很是重要,是整個項目的entry文件。添加新組件的時候,nutui.js文件有兩處須要修改。
增長兩個import,用於加載新組件的入口js文件和scss文件。如:
import Uploader from "./packages/uploader/index.js";
import "./packages/uploader/uploader.scss";
複製代碼
向packages對象添加新組件信息。如:
const packages = {
Cell,
Dialog,
Icon,
Toast,
...
Uploader
}
複製代碼
第一處修改並不困難,能夠經過Node.js將nutui.js文件內容讀取,而後把兩個新的import加在內容頭部,再把新文本內容寫入文件。然鵝,第二處修改就有些困難了,如何向文件中的一個js對象中追加內容呢?一個靠譜的辦法就是AST,即把讀取的文件內容解析成AST,而後遍歷AST找到packages對象,向其中追加新組件信息,最後生成新的代碼字符串,寫入nutui.js文件。而這些操做能夠經過Babel自帶的相關模塊來完成[3]。
const t = require('@babel/types');
const {parse} = require('@babel/parser');
const {default: traverse} = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
複製代碼
好了,這篇文章先談到這裏。留一個思考題吧,咱們知道,webpack 2+ 擁有了Tree-shaking(搖樹)功能,能「搖」掉未用到的代碼,那麼若是咱們不借助Babel插件處理,而直接使用下面這種ES6 modules語法來引入組件,未用到的組件會被「搖」掉嗎?答案固然是否認的,不然何須去開發個Babel插件,因此我真正要問的是爲何不能呢?
import { Button,Switch } from '@nutui/nutui';
複製代碼
連接