淺析組件庫實(shí)現(xiàn)按需引入的幾種方式

按需加載是所有組件庫都會(huì)提供的一個(gè)基礎(chǔ)能力,本文會(huì)分析ElementUI阻问、Vantvarlet幾個(gè)組件庫的實(shí)現(xiàn)并進(jìn)行相應(yīng)實(shí)踐吟逝,幫助你徹底搞懂其實(shí)現(xiàn)原理。

先搭個(gè)簡單的組件庫

筆者從ElementUIcopy了兩個(gè)組件:AlertTag琳袄,并將我們的組件庫命名為XUI江场,當(dāng)前目錄結(jié)構(gòu)如下:

image-20211202134343241.png

組件都放在packages目錄下,每個(gè)組件都是一個(gè)單獨(dú)的文件夾窖逗,最基本的結(jié)構(gòu)是一個(gè)js文件和一個(gè)vue文件址否,組件支持使用Vue.component方式注冊,也支持插件方式Vue.use注冊碎紊,js文件就是用來支持插件方式使用的佑附,比如Alertjs文件內(nèi)容如下:

import Alert from './src/main';

Alert.install = function(Vue) {
  Vue.component(Alert.name, Alert);
};

export default Alert;

就是給組件添加了一個(gè)install方法,這樣就可以使用Vue.use(Alert)來注冊仗考。

組件的主題文件統(tǒng)一放在/theme-chalk目錄下音同,也是每個(gè)組件一個(gè)樣式文件,index.css包含了所有組件的樣式秃嗜,ElementUI的源碼內(nèi)是scss文件权均,本文為了簡單,直接復(fù)制了其npm包內(nèi)已經(jīng)編譯后的css文件锅锨。

最外層還有一個(gè)index.js文件叽赊,這個(gè)文件很明顯是用來作為入口文件導(dǎo)出所有組件:

import Alert from './packages/alert/index.js';
import Tag from './packages/tag/index.js';

const components = [
    Alert,
    Tag
]

const install = function (Vue) {
    components.forEach(component => {
        Vue.component(component.name, component);
    });
};

if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
}

export default {
    install,
    Alert,
    Tag
}

首先依次引入組件庫的所有組件,然后提供一個(gè)install方法必搞,遍歷所有組件必指,依次使用Vue.component方法注冊,接下來判斷是否存在全局的Vue對象恕洲,是的話代表是CDN方式使用塔橡,那么自動(dòng)進(jìn)行注冊,最后導(dǎo)出install方法和所有組件霜第。

Vue的插件就是一個(gè)帶有install方法的對象葛家,所以我們可以直接引入所有組件:

import XUI from 'xui'
import 'xui/theme-chalk/index.css'
Vue.use(XUI)

也可以單獨(dú)注冊某個(gè)組件:

import XUI from 'xui'
import 'xui/theme-chalk/alert.css'
Vue.use(XUI.Alert)

為什么不直接通過import { Alert } form 'xui'來引入呢,很明顯泌类,會(huì)報(bào)錯(cuò)癞谒。

因?yàn)槲覀兊慕M件庫并沒有發(fā)布到NPM,所以通過npm link將我們的組件庫鏈接到全局。

接下來筆者使用Vue CLI搭建了一個(gè)測試項(xiàng)目扯俱,運(yùn)行npm link xui來鏈接到組件庫。然后使用前面的方式注冊組件庫或某個(gè)組件喇澡,這里我們只使用Alert組件迅栅。

通過測試可以發(fā)現(xiàn),無論是注冊所有組件晴玖,還是只注冊Alert組件读存,最后打包后的js里都存在Tag組件的內(nèi)容:

image-20211202140753879.png

接下來開啟本文的正文,看看如何把Tag去掉呕屎。

最簡單的按需引入

因?yàn)槊總€(gè)組件都可以單獨(dú)作為一個(gè)插件让簿,所以我們完全可以只引入某個(gè)組件,比如:

import Alert from 'xui/packages/alert'
import 'xui/theme-chalk/alert.css'

Vue.use(Alert)

這樣我們只引入了alert相關(guān)的文件秀睛,當(dāng)然最后只會(huì)包含alert組件的內(nèi)容尔当。這樣的問題是比較麻煩,使用上成本比較高蹂安,最理想的方式還是下面這種:

import { Alert } from 'xui'

通過babel插件

使用babel插件是目前大多數(shù)組件庫實(shí)現(xiàn)按需引入的方式椭迎,ElementUI使用的是babel-plugin-component

image-20211202140924245.png

可以看到能直接使用import { Alert } form 'xui'方式來引入Alert組件,也不需要手動(dòng)引入樣式田盈,那么這是怎么實(shí)現(xiàn)的呢畜号,接下來我們來擼一個(gè)極簡版的。

原理很簡單允瞧,我們想要的是下面這種方式:

import { Alert } from 'xui'

但是實(shí)際按需使用需要這樣:

import Alert from 'xui/packages/alert'

很明顯简软,我們只要幫用戶把第一種方式轉(zhuǎn)換成第二種就可以了,而通過babel插件來轉(zhuǎn)換對用戶來說是無感的述暂。

首先在babel.config.js同級(jí)新增一個(gè)babel-plugin-component.js文件痹升,作為我們插件文件,然后修改一下babel.config.js文件:

module.exports = {
  // ...
  plugins: ['./babel-plugin-component.js']
}

使用相對路徑引用我們的插件贸典,接下來就可以愉快的編碼了视卢。

先來看一下import { Alert } from 'xui'對應(yīng)的AST

image-20211202150933670.png

整體是一個(gè)ImportDeclaration,通過souce.value可以判斷導(dǎo)入的來源廊驼,specifiers數(shù)組里可以找到導(dǎo)入的變量据过,每個(gè)變量是一個(gè)ImportSpecifier,可以看到里面有兩個(gè)對象:ImportSpecifier.importedImportSpecifier.local妒挎,這兩個(gè)有什么區(qū)別呢绳锅,在于是否使用了別名導(dǎo)入,比如:

import { Alert } from 'xui'

這種情況importedlocal是一樣的酝掩,但是如果使用了別名:

import { Alert as a } from 'xui'

那么是這樣的:

image-20211202152548991.png

我們這里簡單起見就不考慮別名情況鳞芙,只使用imported

接下來的任務(wù)就是進(jìn)行轉(zhuǎn)換,看一下import Alert from 'xui/packages/alert'AST結(jié)構(gòu):

image-20211202154442551.png

目標(biāo)AST結(jié)構(gòu)也清楚了接下來的事情就簡單了原朝,遍歷specifiers數(shù)組創(chuàng)建新的importDeclaration節(jié)點(diǎn)驯嘱,然后替換掉原來的節(jié)點(diǎn)即可:

// babel-plugin-component.js
module.exports = ({
    types
}) => {
    return {
        visitor: {
            ImportDeclaration(path) {
                const {
                    node
                } = path
                const {
                    value
                } = node.source
                if (value === 'xui') {
                    // 找出引入的組件名稱列表
                    let specifiersList = []
                    node.specifiers.forEach(spec => {
                        if (types.isImportSpecifier(spec)) {
                            specifiersList.push(spec.imported.name)
                        }
                    })
                    // 給每個(gè)組件創(chuàng)建一條導(dǎo)入語句
                    const importDeclarationList = specifiersList.map((name) => {
                        // 文件夾的名稱首字母為小寫
                        let lowerCaseName = name.toLowerCase()
                        // 構(gòu)造importDeclaration節(jié)點(diǎn)
                        return types.importDeclaration([
                            types.importDefaultSpecifier(types.identifier(name))
                        ], types.stringLiteral('xui/packages/' + lowerCaseName))
                    })
                    // 用多節(jié)點(diǎn)替換單節(jié)點(diǎn)
                    path.replaceWithMultiple(importDeclarationList)
                }
            }
        },
    }
}

接下來打包測試結(jié)果如下:

image-20211202171657728.png
image-20211202165120365.png

可以看到Tag組件的內(nèi)容已經(jīng)沒有了。

當(dāng)然喳坠,以上實(shí)現(xiàn)只是一個(gè)最簡單的demo鞠评,實(shí)際上還需要考慮樣式的引入、別名壕鹉、去重剃幌、在組件中引入、引入了某個(gè)組件但是實(shí)際并沒有使用等各種問題晾浴,有興趣的可以直接閱讀babel-plugin-component源碼负乡。

Vantantd也都是采用這種方式,只是使用的插件不一樣脊凰,這兩個(gè)使用的都是babel-plugin-import抖棘,babel-plugin-component其實(shí)也是forkbabel-plugin-import

Tree Shaking方式

Vant組件庫除了支持使用前面的Babel插件按需加載外還支持Tree Shaking方式狸涌,實(shí)現(xiàn)也很簡單钉答,Vant最終發(fā)布的代碼里提供了三種規(guī)范的源代碼,分別是commonjs杈抢、umd数尿、esmodule,如下圖:

image-20211202200713184.png
image-20211202200806070.png

commonjs規(guī)范是最常見的使用方式惶楼,umd一般用于cdn方式直接在頁面引入右蹦,而esmodule就是用來實(shí)現(xiàn)Tree Shaking的,為什么esmodule能實(shí)現(xiàn)Tree Shakingcommonjs規(guī)范不行呢歼捐,原因是esmodule是靜態(tài)編譯的何陆,也就是在編譯階段就能確定某個(gè)模塊導(dǎo)出了什么,引入了什么豹储,代碼執(zhí)行階段不會(huì)改變贷盲,所以打包工具在打包的時(shí)候就能分析出哪個(gè)方法被使用了,哪些沒有剥扣,沒有用到的就可以放心的刪掉了巩剖。

接下來修改一下我們的組件庫,讓它也支持Tree Shaking钠怯,因?yàn)槲覀兊慕M件本身就是esmodule模塊佳魔,所以不需要修改,但是要修改一下導(dǎo)出的文件index.js晦炊,因?yàn)槟壳斑€不支持下面這種方式導(dǎo)出:

import { Alert } from 'xui'

增加如下代碼:

// index.js
// ...

export {
    Alert,
    Tag
}

// ...

接下來需要修改package.json鞠鲜,我們都知道package.json里的main字段是用來指示包的入口文件宁脊,那么是不是只要把這個(gè)字段指向esmodule的入口文件就行了呢,其實(shí)是不行的贤姆,因?yàn)橥ǔG闆r下它都是指向commonjs模塊入口榆苞,而且一個(gè)包有可能支持nodejsweb兩種環(huán)境使用,nodejs環(huán)境是有可能不支持esmodule模塊的霞捡,既然不能修改舊的字段语稠,那么就只能引入新的字段,也就是pkg.module弄砍,所以修改package.json文件如下:

// package.json
{
    // ...
    "mains": "index.js",
    "module": "index.js",// 增加該字段
    // ...
}

因?yàn)槲覀兊慕M件庫只有esmodule模塊,所以其實(shí)這兩個(gè)字段指向的是一樣的输涕,在實(shí)際開發(fā)中音婶,需要向Vant一樣編譯成不同類型的模塊,而且發(fā)布到npm的模塊一般也需要編譯成es5語法的莱坎,因?yàn)檫@些不是本文的重點(diǎn)衣式,所以就省略了這個(gè)步驟。

添加了pkg.module字段檐什,如果打包工具能識(shí)別這個(gè)字段碴卧,那么會(huì)優(yōu)先使用esmodule規(guī)范的代碼,但是到這里并沒有結(jié)束乃正,此時(shí)打包后發(fā)現(xiàn)Tag組件的內(nèi)容依然在住册,這是為什么呢,不妨看看下面幾種導(dǎo)入場景:

import 'core-js'
import 'style.css'

這兩個(gè)文件都只引入了瓮具,但是并沒有明顯的進(jìn)行使用荧飞,可以把它們刪了嗎,顯然是不行的名党,這被稱為存在“副作用”叹阔,所以我們需要告訴打包工具哪些文件是沒有副作用的,可以刪掉传睹,哪些是有的耳幢,給我留著,Vue CLI使用的是webpack欧啤,對應(yīng)的我們需要在package.json文件里新增一個(gè)sideEffects字段:

// package.json
{
    // ...
    "sideEffects": ["**/*.css"],
    // ...
}

我們的組件庫里只有樣式文件是存在副作用的睛藻。

接下來再打包測試,發(fā)現(xiàn)沒有引入的Tag組件的內(nèi)容已經(jīng)被去除了:

image-20211203095656755.png

更多關(guān)于Tree Shaking的內(nèi)容可以閱讀Tree Shaking邢隧。

使用unplugin-vue-components插件

varlet組件庫官方文檔上按需引入一節(jié)里提到使用的是unplugin-vue-components插件:

image-20211203144240710.png

這種方式的優(yōu)點(diǎn)是完全不需要自己來引入組件修档,直接在模板里使用,由插件來掃描引入并注冊府框,這個(gè)插件內(nèi)置支持了很多市面上流行的組件庫吱窝,對于已經(jīng)內(nèi)置支持的組件庫讥邻,直接參考上圖引入對應(yīng)的解析函數(shù)配置一下即可,但是我們的小破組件庫它并不支持院峡,所以需要自己來寫這個(gè)解析器兴使。

首先這個(gè)插件做的事情只是幫我們引入組件并注冊,實(shí)際上按需加載的功能還是得靠前面兩種方式照激。

Tree Shaking

我們先在上一節(jié)的基礎(chǔ)上進(jìn)行修改发魄,保留package.jsonmodulesideEffects的配置,然后從main.js里刪除組件引入和注冊的代碼俩垃,然后修改vue.config.js文件励幼。因?yàn)檫@個(gè)插件的官方文檔比較簡潔,看不出個(gè)所以然口柳,所以筆者是參考內(nèi)置的vant解析器來修改的:

image-20211203151054535.png

返回的三個(gè)字段含義應(yīng)該是比較清晰的苹粟,importName表示引入的組件名,比如Alert跃闹,path表示從哪里引入嵌削,對于我們的組件庫就是xuisideEffects就是存在副作用的文件望艺,基本就是配置對應(yīng)的樣式文件路徑苛秕,所以我們修改如下:

// vue.config.js
const Components = require('unplugin-vue-components/webpack')

module.exports = {
    configureWebpack: {
        plugins: [
            Components({
                resolvers: [{
                    type: "component",
                    resolve: (name) => {
                        if (name.startsWith("X")) {
                            const partialName = name.slice(1);
                            return {
                                importName: partialName,
                                path: "xui",
                                sideEffects: 'xui/theme-chalk/' + partialName.toLowerCase() + '.css'
                            };
                        }
                    }
                }]
            })
        ]
    }
}

筆者怕前綴和ElementUI重合,所以組件名稱前綴都由El改成了X找默,比如ElAlert改成了XAlert艇劫,當(dāng)然模板里也需要改成x-alert,接下來進(jìn)行測試:

image-20211203151815019.png
image-20211203151932298.png

可以看到運(yùn)行正常惩激,打包后也成功去除了未使用的Tag組件的內(nèi)容港准。

單獨(dú)引入

最后讓我們再看一下單獨(dú)引入的方式,先把pkg.modulepkg.sideEffects字段都移除咧欣,然后修改每個(gè)組件的index.js文件浅缸,讓其支持如下方式引入:

import { Alert } from 'xui/packages/alert'

Alert組件修改如下:

// index.js
import Alert from './src/main';

Alert.install = function(Vue) {
  Vue.component(Alert.name, Alert);
};

// 新增下面兩行
export {
  Alert
}

export default Alert;

接下來再修改我們的解析器:

const Components = require('unplugin-vue-components/webpack')

module.exports = {
    configureWebpack: {
        mode: 'production',
        plugins: [
            Components({
                resolvers: [{
                    type: "component",
                    resolve: (name) => {
                        if (name.startsWith("X")) {
                            const partialName = name.slice(1);
                            return {
                                importName: partialName,
                                // 修改path字段,指向每個(gè)組件的index.js
                                path: "xui/packages/" + partialName.toLowerCase(),
                                sideEffects: 'xui/theme-chalk/' + partialName.toLowerCase() + '.css'
                            };
                        }
                    }
                }]
            })
        ]
    }
}

其實(shí)就是修改了path字段魄咕,讓其指向每個(gè)組件的index.js文件衩椒,運(yùn)行測試和打包測試后結(jié)果也是符合要求的。

小節(jié)

本文簡單分析了一下組件庫實(shí)現(xiàn)按需引入的幾種方式哮兰,有組件庫開發(fā)需求的朋友可以自行抉擇毛萌,示例代碼請移步:https://github.com/wanglin2/ComponentLibraryImport

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喝滞,一起剝皮案震驚了整個(gè)濱河市阁将,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌右遭,老刑警劉巖做盅,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缤削,死亡現(xiàn)場離奇詭異,居然都是意外死亡吹榴,警方通過查閱死者的電腦和手機(jī)亭敢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來图筹,“玉大人帅刀,你說我怎么就攤上這事≡妒#” “怎么了扣溺?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瓜晤。 經(jīng)常有香客問我锥余,道長,這世上最難降的妖魔是什么活鹰? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮只估,結(jié)果婚禮上志群,老公的妹妹穿的比我還像新娘。我一直安慰自己蛔钙,他們只是感情好锌云,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吁脱,像睡著了一般桑涎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兼贡,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天攻冷,我揣著相機(jī)與錄音,去河邊找鬼遍希。 笑死等曼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凿蒜。 我是一名探鬼主播禁谦,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼废封!你這毒婦竟也來了州泊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤漂洋,失蹤者是張志新(化名)和其女友劉穎遥皂,沒想到半個(gè)月后力喷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡渴肉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年冗懦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仇祭。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡披蕉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乌奇,到底是詐尸還是另有隱情没讲,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布礁苗,位于F島的核電站爬凑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏试伙。R本人自食惡果不足惜嘁信,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疏叨。 院中可真熱鬧潘靖,春花似錦、人聲如沸蚤蔓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秀又。三九已至单寂,卻和暖如春夺脾,著一層夾襖步出監(jiān)牢的瞬間赂毯,已是汗流浹背蟋定。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工或油, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睬魂,地道東北人嗦哆。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓絮蒿,卻偏偏與公主長得像早处,于是被迫代替她去往敵國和親捷雕。 傳聞我的和親對象是個(gè)殘疾皇子椒丧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容