美文网首页
用 unplugin-vue-components 插件实现 V

用 unplugin-vue-components 插件实现 V

作者: lb888 | 来源:发表于2023-06-12 17:26 被阅读0次

    背景

    框架 eui-vue 组件的开发参考 Element Plus,采用了 monorepos 模式来组织整个组件库的代码。其中组件的样式文件和组件的代码是分开成两个工程的,并且在组件的代码中也没有显示的去引用样式文件,但是在使用时只需引入组件代码文件即可自动将样式也一起引入。本文就来分析一下如何实现上述的样式自动引入的功能。

    组件库目录结构

    首先我们来看一下 eui-vue 组件库的目录结构:

    |-- eui-vue
        |-- docs    // 文档目录
        |-- packages    // 组件资源目录
        |   |-- components    // vue 源码目录
        |   |   |-- button
        |   |       |-- src
        |   |       |   |-- button.vue
        |   |       |-- style
        |   |           |-- css.ts
        |   |           |-- index.ts
        |   |-- locale    // 语言目录
        |   |-- 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 }) {
            ...
      },
    }
    

    可以看到组件中没有 style 标签写样式,也没有 import 任何样式文件。而 button 组件的 style 目录里的两个 ts 文件源码如下:

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

    两个文件直接引入了组件样式目录 theme-chalk 中对应的 button 组件的样式文件。

    button 组件的 vue 源码是怎么与这个 style 目录中的 index.ts (或者 css.ts) 关联起来的呢?

    unplugin-vue-components

    unplugin-vue-components 是由 Vue 官方人员开发的一款自动引入插件。使用此插件后,不需要手动编写 import { ElButton } from 'element-plus' 这样的代码了,插件会自动识别 template 中使用的自定义组件并自动注册。

    unplugin-vue-components 插件中已内置了包括 Ant Design Vue、Arco Design Vue、Element Plus、Element UI 等 20 多种主流组件库的解析器。而对于我们自定义的组件库,参照官方文档我们也很容易就写出了自动引入组件的配置代码:

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

    但是这样只是自动引入了组件的 vue 代码,我们还需要将样式也要自动引入才行,这就需要我们自己来写一个解析器了。

    编写解析器

    我们可以直接参考它内置的解析器代码来编写我们自己的解析器。首先我们来定义下我们解析器的配置项:

    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[];
    }
    

    我们的配置项比较简单,共三个:

    • importStyle: 引入的样式类型,当是 boolean 类型时,true 代表引入 css ,false 代表不引入。
    • exclude:需要排除了控件,配置在这里面的控件不会被自动引入
    • noStylesComponents:没有样式的控件,配置在这里的控件不会引入样式,即在处理该控件时, importStyle 会变成 false

    下面我们来开始实现我们的解析器 EuiVueResolver。根据 Componentsresolvers 配置项的签名:

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

    我们的自定义解析器需要返回一个 ComponentResolver 类型的值。继续查看 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;
    

    可以看到核心就是要实现一个 ComponentResolverFunction 类型的方法,该方法需要返回一个 ComponentInfo 类型的对象。

    export function EuiVueResolver(options: EuiVueResolverOptions = {}): ComponentResolver {
        let optionsResolved: EuiVueResolverOptions;
        // 合并配置项
        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)) {
              // 没有样式的控件,importStyle 设置成 `false`
              // resolveComponent 方法需要返回一个 `ComponentInfo` 类型的对象
              return resolveComponent(name, { ...options, importStyle: false });
            } else return resolveComponent(name, options);
        };
    }
    

    下面我们来实现 resolveComponent 方法:

    function resolveComponent( name: string, options: EuiVueResolverOptions): ComponentInfo | undefined {
        // exclude 中的组件需排除
        if (options.exclude && name.match(options.exclude)) return;
        // 不符合 eui-vue 组件命名规范的排除
        if (!name.match(/^E[A-Z]/)) return;
    
        // 将 camelCased 形式名称转化为 kebab-case 形式,并去除开头的 `E`
        // eui-vue 约定 `ETableColumn ` 组件目录是 `components/table-column/`
        // 所以可以根据组件名推断出组件的目录
        const dirName = kebabCase(name.slice(1)); // ETableColumn -> table-column
    
        return {
            name,
            from: `@eui-vue/components`,
            sideEffects: getSideEffects(dirName, options)
        };
    }
    

    resolveComponent 方法的核心是要获取到 ComponentInfo 中的 sideEffects 属性值。从上面 sideEffects 属性的类型 SideEffectsInfo 可以看出,其值就是一个 string 类型或者 ImportInfo 类型,其实本质就是样式文件的路径(ImportInfo.from)。我们这里就简单点,直接用 string 类型来表示这个样式文件路径:

    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`;
        }
    }
    

    自此,我们的解析器就实现出来了。通过上面的实现过程,我们可以发现解析器的实现核心就是通过传入的参数组件 name 来返回需要一起合并的资源的路径。

    解析器编写完后,我们把它作为一个单独的工程,编译打包成 commonjs 规范的库。我们的 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' })],
                })
            ],
            ...
        }
    }
    

    总结

    unplugin-vue-components 插件可以让我在 VUE 中自动引入组件,并且在引入的同时还可以将组件分散的资源合并起来。文章中只实现了一下样式的合并,unplugin-vue-components 插件还可以实现许多其他的效果,大家想学习的可以阅读下它内置的20多个解析器的代码。

    相关文章

      网友评论

          本文标题:用 unplugin-vue-components 插件实现 V

          本文链接:https://www.haomeiwen.com/subject/bkjqydtx.html