最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 按需加载更优雅的解决方案——vite-plugin-components

    正文概述 掘金(内谁)   2021-06-23   3211

    这是我参与更文挑战的第3天,活动详情查看 更文挑战

    按需加载最早的使用方式

    antd按需加载

    插件配置

    在vite.config.ts

    import usePluginImport from "vite-plugin-importer";
    
    export default defineConfig({
       plugins: [
          usePluginImport({
               libraryName: "ant-design-vue",
               libraryDirectory: "es",
               style: "css",
          }),
       ]
     })
    
    

    使用

    import { Button, List, Checkbox, Popconfirm, Input } from "ant-design-vue";
    
     components: {
        [Checkbox.name]: Checkbox,
        [Input.name]: Input,
        [List.name]: List,
        [List.Item.name]: List.Item,
         AListItemMeta: List.Item.Meta, //这里用框架List.Item.Meta.name注册不上只能写死,可能使用displayName可以
        [Button.name]: Button,
        [Popconfirm.name]: Popconfirm,
     },
    
    

    痛点:

    • 由于antd组件库使用了很多子组件,比如List下的组件ListItem,如果少注册了一个都会造成模板解析不了
    • 需要引入一遍,然后注册时候key写一遍,值又写一遍,同样的代码需要写3遍,并要关注子组件和父组件的关系
    • 部分组件并未提供name属性,比如AListItemMeta 所以需要写死,导致风格不一致

    element-plus按需加载

    插件配置

    在vite.config.ts

    import styleImport from "vite-plugin-style-import";
    
    export default defineConfig({
       plugins: [
          styleImport({
          libs: [
            {
              libraryName: "element-plus",
              esModule: true,
              ensureStyleFile: true,
              resolveStyle: name => {
                return `element-plus/lib/theme-chalk/${name}.css`;
              },
              resolveComponent: name => {
                return `element-plus/lib/${name}`;
              },
            },
          ],
        }),
       ]
     })
    
    

    使用方式

    import { ElForm, ElButton, ElFormItem, ElInput } from "element-plus";
     
    components: {
       ElForm,
       ElButton,
       ElInput,
       ElFormItem,
    },
    
    

    痛点:

    • 同样是父子组件,要引入两遍,比如FormFormItem,与antd不同的是element需要分别引入父子组件,并且只能使用components的注册方式,但是antd除此还支持插件方式注册,用app.use(xxx)

    改进

    为了解决antd父子组件引入的父子组件造成困扰,antd是提供app.use(xx)这种插件注册的方式,antd内部自动解决了子组件依赖的问题,比如要使用List组件,只需要app.use(List)即可使用List和ListItem,ListItemMeta,这样方便了不少

    于是写了一个useComp的方法使用 app.use(comp);进行注册 在vue3中首先要获取到app实例,vue3提供了getCurrentInstance这个方法可以获取到当前组件的实例,然后在通过当前实例获取到全局上下文中的app对象,instance?.appContext.app;,这样就可以使用app.use进行注册了,还要注意的是,同一个组件避免重复注册,需要记录一下已经注册过的组件

    代码useAntd.ts如下:

    import { Plugin, getCurrentInstance } from "vue";
    
    interface Registed {
      [key: string]: boolean;
    }
    let registed: Registed = {};
    
    type Comp = {
      displayName?: string;
      name?: string;
    } & Plugin;
    
    type RegisteComps = (...comps: Comp[]) => void;
    
    export const useComp: RegisteComps = (...comps) => {
      comps.forEach(comp => {
        const name = comp.displayName || comp.name;
    
        if (name && !registed[name]) {
          const instance = getCurrentInstance();
         
          const app = instance?.appContext.app;
    
          if (app) {
            app.use(comp);
            registed[name] = true;
          }
        }
      });
    };
    
    
    

    使用方式:

    
     import { List, Table, Button, Space } from "ant-design-vue";
     import { useComp } from "@/hooks/useAntd";
     //...略
     setup() {
         useComp(List, Table, Button, Space);
         return {} 
     }
    
    

    解决痛点:

    • 无需关系父子组件依赖关系
    • 减少components注册的代码
    • 用到哪些组件,直接将import 括号中的组件名称,复制粘贴到useComp方法中即可,属于无脑操作,

    遗留痛点:

    • element,naive还是需要用components进行一一注册
    • 相较tsx在setup中还是多了一行注册的代码

    理想形态:

    所用组件无需关心引入和注册,就像全量引入一样,直接在模板使用即可,既可以写起来舒服,又无需关注忘记注册组件带来的烦恼

    长时间一直在寻找类似的方法,直到逛社区,发现大佬antfu开源了一个叫vite-plugin-components的插件,这正是我想要寻找的。 它实现的功能就是:自动解析模板中所用到的组件,然后自动按需引入,再也不需要手动进行注册了。

    但是理想很丰满,现实很骨感,踩到了一些坑

    插件配置:

    import ViteComponents, {
      AntDesignVueResolver,
      ElementPlusResolver,
      ImportInfo,
      kebabCase,
      NaiveUiResolver,
      pascalCase,
    } from "vite-plugin-components";
    
     
    export default defineConfig({
      plugins: [
        ViteComponents({
          customComponentResolvers: [
            AntDesignVueResolver(),//官方插件提供
            ElementPlusResolver(),//官方插件提供
            NaiveUiResolver(),//官方插件提供
          ]
        })
      ]
    })
    
    

    尝试结果:

    • naiveui完美支持,赞!
    • elementui官方用的scss,需要安装scss依赖才行
    • antdv 只有少部分组件可以解析,像layout,list table这种常用组件不能解析

    解决:

    element-plus重写resolver:

    重写的理由:由于项目没有使用scss只用了less,所以把scss转成css加载样式的方式

    官方的写法如下:

     const { importStyle = true } = options
      if (name.startsWith('El')) {
        const partialName = name[2].toLowerCase() + name.substring(3).replace(/[A-Z]/g, l => `-${l.toLowerCase()}`)
        return {
          path: `element-plus/es/el-${partialName}`,
          sideEffects: importStyle ? `element-plus/packages/theme-chalk/src/${partialName}.scss` : undefined,
        }
      }
    
    

    重写之后:改为从lib目录引入的路径,直接使用组件名称作为文件目录名引入,不在做复杂的组件名转换了。 代码如下:

      customComponentResolvers: [
            // AntDesignVueResolver(),
            // ElementPlusResolver(),
            NaiveUiResolver(),
            name => {
              if (name.startsWith("El")) {
                // Element UI
                const partialName = kebabCase(name); //ElButton->el-button
                return {
                  path: `element-plus/lib/${partialName}`,
                  sideEffects: `element-plus/lib/theme-chalk/${partialName}.css`,
                };
              } 
            }
         ]
    
    
    

    antdv

    官方的做法是:

    export const AntDesignVueResolver = (): ComponentResolver => (name: string) => {
      if (name.match(/^A[A-Z]/))
        return { importName: name.slice(1), path: 'ant-design-vue/es' }
    }
    
    

    存在的问题:

    • <a-list-item>这种组件就没法到ant-design-vue/es/list-item这个目录找,并不存在这样的目录,实际上他的真实目录是ant-design-vue/es/list/Item.js

    • <a-layout-content组件也没有对应的路径,他是通过layout中的生成器generator方法生成的,并绑定到layout对象上,他的实际路径应该是ant-design-vue/es/layout/layout.js的Content属性

    • <a-select-option>这个组件也是绑定到select上的,但是实际上他引入是引入的vc-select/Option.js属于基层组件

    • <a-menu-item>组件是属于menu的子组件,目录是ant-design-vue/es/menu/MenuItem.js,这个和之前规则不一样,我以为应该叫Item才对,但是这里去不同,所以需要特殊处理,

    • 还有<a-tab-pane>这种组件,他所需要的样式目录是在tabs,但是实际上它的文件目录是在vc-tabs/src下,也需要特殊处理

    以上问题都是官方的写法无法正常加载到对应组件的原因,因此为了解决以上问题,我针对不同的情况写了一大坨判断逻辑,来修正组件的引入路径,但是依旧有部分组件无法引入到,因为有些组件是functional的,或者是generator生成的,并不具备独立的子组件文件,暂时也没有找到合适的方法引入对应的子组件属性

    解析组件路径的getCompPath方法,代码如下:

    function getCompPath(
      compName: string
    ): {
      dirName: string;
      compName: string;
      styleDir: string;
      importName?: string;
      sideEffects?: ImportInfo;
    } {
      const hasSubComp = [
        "Menu",
        "Layout",
        "Form",
        "Table",
        "Modal",
        "Radio",
        "Button",
        "Checkbox",
        "List",
        "Collapse",
        "Descriptions",
        "Tabs",
        "Mentions",
        "Select",
        "Anchor",
        "Typography",
        // "TreeSelect",
      ]; //包含子组件的组件
      const keepSelf = [
        "MenuItem",
        "SubMenu",
        "FormItem",
        "RadioButton",
        "CollapsePanel",
        "TabPane",
        "AnchorLink",
      ]; //保留原子组件名称
      const keepFather = [
        "LayoutHeader",
        "LayoutContent",
        "LayoutFooter",
        "DescriptionsItem",
      ]; //需要使用父组件名称的子组件  LayoutFooter->''  之所以转成空是因为最后拼接的结果是dirName+compName,避免重复
      const rootName = hasSubComp.find((name: string) => compName.startsWith(name));
      const usePrevLevelName = ["ListItemMeta"]; //使用当前组件的上一级名称  ListItemMeta->Item
      const getPrevLevelName = () => {
        const split = kebabCase(compName).split("-");
        return pascalCase(split[split.length - 2]);
      };
    
      const fatherAlias = {
        TabPane: "vc-tabs/src",
        MentionsOption: "vc-mentions/src",
        SelectOption: "vc-select",
        TreeSelectNode: "vc-tree-select/src",
      };
    
      const compAlias = {
        TreeSelectNode: "SelectNode",
      };
    
      const styleMap = {
        TabPane: "tabs",
        MentionsOption: "mentions",
        SelectOption: "select",
        TreeSelectNode: "tree-select",
      };
      // const importNameMap = {
      //   LayoutContent: "Content",
      //   LayoutHeader: "Header",
      //   LayoutFooter: "Footer",
      // };
    
      let dirName = rootName?.toLowerCase() ?? kebabCase(compName);
    
      if (fatherAlias[compName]) {
        dirName = fatherAlias[compName];
      }
    
      let compNameStr = "";
      if (keepSelf.includes(compName)) {
        compNameStr = compName;
      } else if (keepFather.includes(compName)) {
        compNameStr = "";
      } else if (usePrevLevelName.includes(compName)) {
        compNameStr = getPrevLevelName();
      } else if (rootName) {
        compNameStr = compName.replace(rootName, "");
      }
      const compRequired = {
        TypographyTitle: "ant-design-vue/es/" + dirName + "/Base",
        TypographyText: "ant-design-vue/es/" + dirName + "/Base",
      };
    
      return {
        // importName: importNameMap[compName],
        dirName: fatherAlias[compName] ?? dirName,
        styleDir: `${styleMap[compName] ?? dirName}`,
        compName: compAlias[compName] ?? compNameStr,
        sideEffects: compRequired[compName]
          ? {
              path: compRequired[compName],
            }
          : undefined,
      };
    }
    
    
    

    自定义resolver,代码如下

     ViteComponents({
          customComponentResolvers: [
        
            name => {
              if (name.match(/^A[A-Z]/)) {
                //ant-design-vue
    
                const importName = name.slice(1);
                const dirName = kebabCase(importName);
                const compName = pascalCase(importName); //AListItem->ListItem
                const compPath = getCompPath(compName);//这里解析组件的真实路径
    
                const sideEffects = [
                  {
                    path: `ant-design-vue/es/${compPath.styleDir}/style`,
                  },
                ];
                if (compPath.sideEffects) {
                  sideEffects.push(compPath.sideEffects);
                }
                return {
                  path: `ant-design-vue/es/${compPath.dirName}/${compPath.compName}`,
                  sideEffects,
                };
              }
              return null;
            },
          ],
          globalComponentsDeclaration: true,
        }),
    
    

    经过解析,绝大部分组件可以使用,还有遗留的部分组件不能正常使用

    插件生成部分组件的声明文件如下:

    
    declare module 'vue' {
      export interface GlobalComponents {
        AMenuItem: typeof import('ant-design-vue/es/menu/MenuItem')['default']
        AMenu: typeof import('ant-design-vue/es/menu/')['default']
        ALayoutHeader: typeof import('ant-design-vue/es/layout/')['default']
        ALayoutContent: typeof import('ant-design-vue/es/layout/')['default']
        ALayoutFooter: typeof import('ant-design-vue/es/layout/')['default']
        ALayout: typeof import('ant-design-vue/es/layout/')['default']
        AButton: typeof import('ant-design-vue/es/button/')['default']
        ADivider: typeof import('ant-design-vue/es/divider/')['default']
        AInput: typeof import('ant-design-vue/es/input/')['default']
        AFormItem: typeof import('ant-design-vue/es/form/FormItem')['default']
        ASpace: typeof import('ant-design-vue/es/space/')['default']
        AForm: typeof import('ant-design-vue/es/form/')['default']
        ACheckbox: typeof import('ant-design-vue/es/checkbox/')['default']
        AListItemMeta: typeof import('ant-design-vue/es/list/Item')['default']
        APopconfirm: typeof import('ant-design-vue/es/popconfirm/')['default']
        AListItem: typeof import('ant-design-vue/es/list/Item')['default']
        AList: typeof import('ant-design-vue/es/list/')['default']
        ATable: typeof import('ant-design-vue/es/table/')['default']
      }
    }
    
    

    具体问题如下:

    • layout下的组件,Content,Header,Footer,由于默认只引入default导出的,如果引入类似['default']['Content'],应该就可以正常,现在问题是把Content等子组件都当做了layout进行解析,导致样式会有问题
    • ListItemMeta组件导入不正常,同样只能引入['default'],如果可以引入['default']['Meta']应该就可以解决了
    • Typography组件 引入title组件会报错:TypeError: baseProps is not a function但是实际上该组件使用相对路径引入了这个方法

    希望大佬们可以帮忙一起看看上述问题该如何解决,感觉现在的代码写的有些麻烦,如果有更优雅的方法可以交流一下

    相关issue和代码

    antdv有关该问题的 issue

    vite-plugin-components 有关该问题的issue

    代码: github——todo项目地址


    起源地下载网 » 按需加载更优雅的解决方案——vite-plugin-components

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元