用 unplugin-vue-components 插件實(shí)現(xiàn) VUE 組件樣式自動(dòng)引入

背景

框架 eui-vue 組件的開(kāi)發(fā)參考 Element Plus,采用了 monorepos 模式來(lái)組織整個(gè)組件庫(kù)的代碼总放。其中組件的樣式文件和組件的代碼是分開(kāi)成兩個(gè)工程的,并且在組件的代碼中也沒(méi)有顯示的去引用樣式文件,但是在使用時(shí)只需引入組件代碼文件即可自動(dòng)將樣式也一起引入派诬。本文就來(lái)分析一下如何實(shí)現(xiàn)上述的樣式自動(dòng)引入的功能佩伤。

組件庫(kù)目錄結(jié)構(gòu)

首先我們來(lái)看一下 eui-vue 組件庫(kù)的目錄結(jié)構(gòu):

|-- eui-vue
    |-- docs    // 文檔目錄
    |-- packages    // 組件資源目錄
    |   |-- components    // vue 源碼目錄
    |   |   |-- button
    |   |       |-- src
    |   |       |   |-- button.vue
    |   |       |-- style
    |   |           |-- css.ts
    |   |           |-- index.ts
    |   |-- locale    // 語(yǔ)言目錄
    |   |-- theme-chalk    // 樣式目錄
    |   |   |-- dist
    |   |   |   |-- e-button.css
    |   |   |   |-- ...
    |   |   |-- src
    |   |       |-- button.less
    |   |       |-- ...
    |-- play    // 演示目錄
    |   |-- index.html
    |   |-- main.ts
    |   |-- vite.config.ts
    |   |-- vite.init.ts
    |   |-- src
    |       |-- App.vue

上面 components 目錄里的 button 組件源碼文件 button.vue 代碼如下:

<template>
    <button class="e-button">
        ...
    </button>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
    name: 'EButton',
    setup(props, { slots }) {
        ...
  },
}

可以看到組件中沒(méi)有 style 標(biāo)簽寫樣式荸实,也沒(méi)有 import 任何樣式文件。而 button 組件的 style 目錄里的兩個(gè) ts 文件源碼如下:

// css.ts
import '@eui-vue/theme-chalk/e-button.css'

// index.ts
import '@eui-vue/theme-chalk/src/button.less'

兩個(gè)文件直接引入了組件樣式目錄 theme-chalk 中對(duì)應(yīng)的 button 組件的樣式文件献汗。

button 組件的 vue 源碼是怎么與這個(gè) style 目錄中的 index.ts (或者 css.ts) 關(guān)聯(lián)起來(lái)的呢?

unplugin-vue-components

unplugin-vue-components 是由 Vue 官方人員開(kāi)發(fā)的一款自動(dòng)引入插件王污。使用此插件后罢吃,不需要手動(dòng)編寫 import { ElButton } from 'element-plus' 這樣的代碼了,插件會(huì)自動(dòng)識(shí)別 template 中使用的自定義組件并自動(dòng)注冊(cè)玉掸。

unplugin-vue-components 插件中已內(nèi)置了包括 Ant Design Vue刃麸、Arco Design Vue、Element Plus司浪、Element UI 等 20 多種主流組件庫(kù)的解析器泊业。而對(duì)于我們自定義的組件庫(kù),參照官方文檔我們也很容易就寫出了自動(dòng)引入組件的配置代碼:

Components({
  resolvers: [
    // 自動(dòng)引入 eui-vue 的組件
    (componentName) => {
      return { name: componentName, from: '@eui-vue/components' };
    },
  ]
})

但是這樣只是自動(dòng)引入了組件的 vue 代碼啊易,我們還需要將樣式也要自動(dòng)引入才行吁伺,這就需要我們自己來(lái)寫一個(gè)解析器了。

編寫解析器

我們可以直接參考它內(nèi)置的解析器代碼來(lái)編寫我們自己的解析器租谈。首先我們來(lái)定義下我們解析器的配置項(xiàng):

export interface EuiVueResolverOptions {
  /**
   * import style css or less with components
   *
   * @default 'css'
   */
  importStyle?: boolean | 'css' | 'less';

  /**
   * exclude component name, if match do not resolve the name
   */
  exclude?: RegExp;

  /**
   * a list of component names that have no styles, so resolving their styles file should be prevented
   */
  noStylesComponents?: string[];
}

我們的配置項(xiàng)比較簡(jiǎn)單篮奄,共三個(gè):

  • importStyle: 引入的樣式類型捆愁,當(dāng)是 boolean 類型時(shí),true 代表引入 css 窟却,false 代表不引入昼丑。
  • exclude:需要排除了控件,配置在這里面的控件不會(huì)被自動(dòng)引入
  • noStylesComponents:沒(méi)有樣式的控件夸赫,配置在這里的控件不會(huì)引入樣式菩帝,即在處理該控件時(shí), importStyle 會(huì)變成 false茬腿。

下面我們來(lái)開(kāi)始實(shí)現(xiàn)我們的解析器 EuiVueResolver呼奢。根據(jù) Componentsresolvers 配置項(xiàng)的簽名:

resolvers?: (ComponentResolver | ComponentResolver[])[];

我們的自定義解析器需要返回一個(gè) ComponentResolver 類型的值。繼續(xù)查看 ComponentResolver 的簽名:

interface ImportInfo {
    as?: string;
    name?: string;
    from: string;
}
declare type SideEffectsInfo = (ImportInfo | string)[] | ImportInfo | string | undefined;
interface ComponentInfo extends ImportInfo {
    sideEffects?: SideEffectsInfo;
}
declare type ComponentResolveResult = Awaitable<string | ComponentInfo | null | undefined | void>;
declare type ComponentResolverFunction = (name: string) => ComponentResolveResult;
interface ComponentResolverObject {
    type: 'component' | 'directive';
    resolve: ComponentResolverFunction;
}
declare type ComponentResolver = ComponentResolverFunction | ComponentResolverObject;

可以看到核心就是要實(shí)現(xiàn)一個(gè) ComponentResolverFunction 類型的方法切平,該方法需要返回一個(gè) ComponentInfo 類型的對(duì)象握础。

export function EuiVueResolver(options: EuiVueResolverOptions = {}): ComponentResolver {
    let optionsResolved: EuiVueResolverOptions;
    // 合并配置項(xiàng)
    function resolveOptions() {
        if (optionsResolved) return optionsResolved;
        optionsResolved = {
          importStyle: 'css',
          exclude: undefined,
          noStylesComponents: options.noStylesComponents || [],
          ...options,
        };
        return optionsResolved;
    }

    return (name: string) => {
        const options = resolveOptions();
        if ([...options.noStylesComponents, ...noStylesComponents].includes(name)) {
          // 沒(méi)有樣式的控件,importStyle 設(shè)置成 `false`
          // resolveComponent 方法需要返回一個(gè) `ComponentInfo` 類型的對(duì)象
          return resolveComponent(name, { ...options, importStyle: false });
        } else return resolveComponent(name, options);
    };
}

下面我們來(lái)實(shí)現(xiàn) resolveComponent 方法:

function resolveComponent( name: string, options: EuiVueResolverOptions): ComponentInfo | undefined {
    // exclude 中的組件需排除
    if (options.exclude && name.match(options.exclude)) return;
    // 不符合 eui-vue 組件命名規(guī)范的排除
    if (!name.match(/^E[A-Z]/)) return;

    // 將 camelCased 形式名稱轉(zhuǎn)化為 kebab-case 形式悴品,并去除開(kāi)頭的 `E`
    // eui-vue 約定 `ETableColumn ` 組件目錄是 `components/table-column/`
    // 所以可以根據(jù)組件名推斷出組件的目錄
    const dirName = kebabCase(name.slice(1)); // ETableColumn -> table-column

    return {
        name,
        from: `@eui-vue/components`,
        sideEffects: getSideEffects(dirName, options)
    };
}

resolveComponent 方法的核心是要獲取到 ComponentInfo 中的 sideEffects 屬性值禀综。從上面 sideEffects 屬性的類型 SideEffectsInfo 可以看出,其值就是一個(gè) string 類型或者 ImportInfo 類型他匪,其實(shí)本質(zhì)就是樣式文件的路徑(ImportInfo.from)菇存。我們這里就簡(jiǎn)單點(diǎn),直接用 string 類型來(lái)表示這個(gè)樣式文件路徑:

function getSideEffects(dirName: string, options: EuiVueResolverOptions): SideEffectsInfo | undefined {
    const { importStyle } = options;
    const componentsFolder = '@eui-vue/components';

    if (importStyle === 'less') {
        // 返回組件引用 less 文件的 {dirName}/style/index.ts 文件
        return `${componentsFolder}/${dirName}/style/index`;
    } else if (importStyle === true || importStyle === 'css') {
        // 返回組件引用 css 文件的 {dirName}/style/css.ts 文件
        return `${componentsFolder}/${dirName}/style/css`;
    }
}

自此邦蜜,我們的解析器就實(shí)現(xiàn)出來(lái)了依鸥。通過(guò)上面的實(shí)現(xiàn)過(guò)程,我們可以發(fā)現(xiàn)解析器的實(shí)現(xiàn)核心就是通過(guò)傳入的參數(shù)組件 name 來(lái)返回需要一起合并的資源的路徑悼沈。

解析器編寫完后贱迟,我們把它作為一個(gè)單獨(dú)的工程,編譯打包成 commonjs 規(guī)范的庫(kù)絮供。我們的 unplugin-vue-components 插件配置就可以直接用了:

// vite.config.ts
import { defineConfig } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { EuiVueResolver } from '@eui-vue/resolver';

export default defineConfig(async ({ mode }) => {
    return {
        ...,
        plugins: [
            Components({
                resolvers: [EuiVueResolver({ importStyle: 'less' })],
            })
        ],
        ...
    }
}

總結(jié)

unplugin-vue-components 插件可以讓我在 VUE 中自動(dòng)引入組件衣吠,并且在引入的同時(shí)還可以將組件分散的資源合并起來(lái)。文章中只實(shí)現(xiàn)了一下樣式的合并壤靶,unplugin-vue-components 插件還可以實(shí)現(xiàn)許多其他的效果缚俏,大家想學(xué)習(xí)的可以閱讀下它內(nèi)置的20多個(gè)解析器的代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贮乳,一起剝皮案震驚了整個(gè)濱河市忧换,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌向拆,老刑警劉巖亚茬,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異浓恳,居然都是意外死亡刹缝,警方通過(guò)查閱死者的電腦和手機(jī)碗暗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)梢夯,“玉大人言疗,你說(shuō)我怎么就攤上這事〕恚” “怎么了洲守?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵疑务,是天一觀的道長(zhǎng)沾凄。 經(jīng)常有香客問(wèn)我,道長(zhǎng)知允,這世上最難降的妖魔是什么撒蟀? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮温鸽,結(jié)果婚禮上保屯,老公的妹妹穿的比我還像新娘。我一直安慰自己涤垫,他們只是感情好姑尺,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蝠猬,像睡著了一般切蟋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上榆芦,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天柄粹,我揣著相機(jī)與錄音,去河邊找鬼匆绣。 笑死驻右,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的崎淳。 我是一名探鬼主播堪夭,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拣凹!你這毒婦竟也來(lái)了森爽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咐鹤,失蹤者是張志新(化名)和其女友劉穎拗秘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體祈惶,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雕旨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年扮匠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凡涩。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棒搜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出活箕,到底是詐尸還是另有隱情力麸,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布育韩,位于F島的核電站克蚂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏筋讨。R本人自食惡果不足惜埃叭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悉罕。 院中可真熱鬧赤屋,春花似錦、人聲如沸壁袄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嗜逻。三九已至涩僻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間变泄,已是汗流浹背令哟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妨蛹,地道東北人屏富。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蛙卤,于是被迫代替她去往敵國(guó)和親狠半。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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