如何让自己开发的组件库能按需自动引入

前言

最近在参考 Element Plus 开发一套自己的组件库,发现 Element Plus 可以实现按需自动导入控件,于是我就自己也来捣鼓了一个自己组件库的自动引入插件。

unplugin-vue-components

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

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

Components({
  resolvers: [
    // 自动引入 eui 的组件
    (componentName) => {
      return { name: componentName, from: '@eui/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 组件命名规范的排除
    if (!name.match(/^E[A-Z]/)) return;

    // 将 camelCased 形式名称转化为 kebab-case 形式,并去除开头的 `E`
    // eui 约定 `ETableColumn ` 组件目录是 `components/table-column/`
    // 所以可以根据组件名推断出组件的目录
    const dirName = kebabCase(name.slice(1)); // ETableColumn -> table-column

    return {
        name,
        from: `@eui/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/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/resolver';

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

总结

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

全部评论

相关推荐

这个难度和bat相比如何?一、技术原理与底层机制1.&nbsp;你在项目中提到优化CLS指标,能否从浏览器渲染机制的角度解释CLS的计算逻辑?针对font-display:&nbsp;swap的优2化方案,当字体未加载完成时如何避免布局偏移?2.&nbsp;在浮边窗组件中使用了Intersection&nbsp;Observer&nbsp;API,该API的回调触发时机如何控制?当父容器存在transform属性时,对交叉检测的准确性是否有影响?3.&nbsp;你提到阅读过Vue/React源码,能否对比两者的响应式系统实现差异?Vue3的Proxy方案与React的Fiber架构在更新粒度控制上有何本质区别?二、性能优化深度追问4.&nbsp;首屏渲染时间优化涉及骨架屏技术,如何保证骨架屏与真实DOM结构的尺寸一致性?当异步组件加载失败时,如何实现骨架屏到错误状态的平滑过渡?5.&nbsp;火山监控SDK的JS错误率优化中,你们是如何区分&amp;quot;噪音错误&amp;quot;(如第三方库错误)与关键业务错误的?针对Script&nbsp;error.这类跨域错误有何具体解决策略?6.&nbsp;在Vite插件开发中,如何实现资源加载失败的重试机制?请描述从拦截HTTP请求到触发重试的完整流程设计。三、架构设计与工程化7.&nbsp;微前端场景下,主应用与子应用如何实现样式隔离?当使用qiankun时,如果子应用使用了React&nbsp;Portals等脱离DOM层级的技术,会带来哪些潜在问题?8.&nbsp;部署工作台项目中提到&amp;quot;分支与需求一对一&amp;quot;的机制,当出现跨分支的需求依赖时(如需求A依赖需求B的接口),你们的版本兼容性策略是什么?9.&nbsp;在脚手架工具中集成Husky时,如何处理多人协作场景下的Git&nbsp;Hook同步问题?当Hook脚本需要动态更新时如何保证开发者的本地环境一致性?四、AI工程化实践10.&nbsp;AI助手的Schema生成准确率提升方案中,如何通过Prompt&nbsp;Engineering解决LLM输出格式漂移问题?当遇到模型返回非法JSON结构时,你的容错机制如何设计?11.&nbsp;使用SSE流式传输时,如何保证分块数据到达顺序与渲染顺序的一致性?当网络抖动导致数据包乱序时,前端应采取何种补偿策略?12.&nbsp;在多轮对话链设计中,Redis缓存的会话历史如何平衡存储成本与响应速度?当用户连续操作超过Token窗口限制时,你们的上下文截断策略是什么?五、异常处理与边界场景13.&nbsp;Websocket心跳机制中,你提到使用指数退避算法重连,请说明具体退避策略(如初始间隔、最大重试次数)。当服务端主动断开连接时,客户端应如何区分是维护性停机还是异常故障?14.&nbsp;在埋点管理系统的批量上传场景中,如何实现断点续传功能?当服务器返回429状态码时,前端应如何设计智能限流策略?15.&nbsp;低代码平台的组件渲染兜底策略中,如何建立Snippets与失败组件特征的映射关系?当预置代码片段也无法渲染时,如何保障平台可用性?六、系统设计开放性题目16.&nbsp;如果要求你设计一个跨团队的Chrome插件性能看板,需要聚合不同部门的监控数据,请描述你的架构设计方案,重点说明数据权限隔离、实时性保障、横向扩展能力等关键点。17.&nbsp;假设需要为C端项目设计离线模式(如课程预约信息本地持久化),请阐述你的数据同步方案,包括冲突解决策略、离线状态检测机制、数据加密方案等关键设计。七、职业发展与工程思维18.&nbsp;在重构低代码平台过程中,你是如何评估&amp;quot;逐步迁移&amp;quot;与&amp;quot;整体重构&amp;quot;的利弊的?当业务方持续提出新需求时,如何平衡重构进度与功能交付压力?19.&nbsp;你提到在团队输出技术文章,请分享一次你通过技术布道推动团队技术决策的经历。当遇到技术方案争议时,你通常如何建立技术影响力?20.&nbsp;从职业发展角度看,你未来3年希望在哪个技术领域建立深度壁垒?你目前的技术体系存在哪些短板?计划如何突破?
点赞 评论 收藏
分享
02-20 19:55
已编辑
网易有道_Android(实习员工)
查看21道真题和解析 面试体验感最好的是哪家?
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务