背景
框架 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ù) Components
的 resolvers
配置項(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è)解析器的代碼。