这是我参与更文挑战的第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,
},
痛点:
- 同样是父子组件,要引入两遍,比如
Form
和FormItem
,与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
但是实际上该组件使用相对路径引入了这个方法
希望大佬们可以帮忙一起看看上述问题该如何解决,感觉现在的代码写的有些麻烦,如果有更优雅的方法可以交流一下
-----------20210625更新------------
版本2.2.0-beta.5: 官方已经支持vite-plugin-components
待完善小问题,主要是名字拼写错误
- LayoutContent错写成layouContent
- PassWord导出为InputPassoword等组件名称重命名
备注:经过查看官方已经修复了,暂未发版,坐等beta.6发版~~~
经过更改node_modules源码后,发现可以正常加载了, 然后官方的vite-plugin-compoennts的resolver还是不好用,还是需要自定义一下,代码如下
const importName = name.slice(1);
return {
importName: importName,
path: `ant-design-vue/es`,
sideEffects: "ant-design-vue/es/style",
};
舒服多了,赞一下antd-vue团队唐老大的响应速度和支持力度!!!
更新
---------20210626更新-----------
版本2.2.0-beta.6: 官方修复了导入拼写错误的问题,按照上面的导入方式可以正常使用了,
建议:style样式上面的方式是全量导入,不知有没有按需导入的方式,使用vite-plugin提供的resolver导入之后没有样式,但也没有报错
--------20210627更新-------------
vite-plugin-components:已经合并了pr,可以用了,就是style是全量引入
全量引入style还是太慢,加载很多没有用的style,开始尝试按需加载style,
由于antdv官方解决了js导入的问题,剩下style导入似乎没有那么麻烦,只要找对了目录就可以了, 参考之前的写法,还是写了一个配置表,用于匹配组件的目录,思路如下:
- 子组件或者需要特殊处理的组件,根据正则匹配,如果匹配到则返回配置好的目录
- 如果没有匹配到的组件,直接转换kebabCase(烤肉串形式)形式
接口interface如下:
interface IMatcher {
pattern: RegExp;
styleDir: string;
}
经过测试后需要配置的组件信息如下:
const matchComponents: IMatcher[] = [
{
pattern: /^Avatar/,
styleDir: 'avatar',
},
{
pattern: /^AutoComplete/,
styleDir: 'auto-complete',
},
{
pattern: /^Anchor/,
styleDir: 'anchor',
},
{
pattern: /^Badge/,
styleDir: 'badge',
},
{
pattern: /^Breadcrumb/,
styleDir: 'breadcrumb',
},
{
pattern: /^Button/,
styleDir: 'button',
},
{
pattern: /^Checkbox/,
styleDir: 'checkbox',
},
{
pattern: /^Card/,
styleDir: 'card',
},
{
pattern: /^Collapse/,
styleDir: 'collapse',
},
{
pattern: /^Descriptions/,
styleDir: 'descriptions',
},
{
pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
styleDir: 'date-picker',
},
{
pattern: /^Dropdown/,
styleDir: 'dropdown',
},
{
pattern: /^Form/,
styleDir: 'form',
},
{
pattern: /^InputNumber/,
styleDir: 'input-number',
},
{
pattern: /^Input|^Textarea/,
styleDir: 'input',
},
{
pattern: /^Statistic/,
styleDir: 'statistic',
},
{
pattern: /^CheckableTag/,
styleDir: 'tag',
},
{
pattern: /^Layout/,
styleDir: 'layout',
},
{
pattern: /^Menu|^SubMenu/,
styleDir: 'menu',
},
{
pattern: /^Table/,
styleDir: 'table',
},
{
pattern: /^Radio/,
styleDir: 'radio',
},
{
pattern: /^Image/,
styleDir: 'image',
},
{
pattern: /^List/,
styleDir: 'list',
},
{
pattern: /^Tab/,
styleDir: 'tabs',
},
{
pattern: /^Mentions/,
styleDir: 'mentions',
},
{
pattern: /^Mentions/,
styleDir: 'mentions',
},
{
pattern: /^Step/,
styleDir: 'steps',
},
{
pattern: /^Skeleton/,
styleDir: 'skeleton',
},
{
pattern: /^Select/,
styleDir: 'select',
},
{
pattern: /^TreeSelect/,
styleDir: 'tree-select',
},
{
pattern: /^Tree|^DirectoryTree/,
styleDir: 'tree',
},
{
pattern: /^Typography/,
styleDir: 'typography',
},
{
pattern: /^Timeline/,
styleDir: 'timeline',
},
]
写个循环进行遍历,得到样式的真实路径
const importName = name.slice(1);
let styleDir;
const total = matchComponents.length;
for (let i = 0; i < total; i++) {
const matcher = matchComponents[i];
if (importName.match(matcher.pattern)) {
styleDir = matcher.styleDir;
break;
}
}
if (!styleDir) {
styleDir = kebabCase(importName);
}
return {
importName: importName,
path: `ant-design-vue/es`,
sideEffects: `ant-design-vue/es/${styleDir}/style`,
};
提个pr,希望能够早日通过: PR 链接
---------20210629更新------------
vite-plugin-components: antfu已经发版了0.11.4 ,舒服啦~~~
增加功能: 增加css加载,也是看到了issue得到的启发,之前一直想着加载index.css却没有想到还可以直接加载css.js,之前一加载到popconfirm,找不到index.css就会报错,因为他本身是没有样式,全都是依赖另外两个组件的样式,现在直接加载css.js一下就解决这个问题了,在js中按需加载相应的样式就好了 代码更改如下:
const getSideEffects: (
compName: string,
opts: AntDesignVueResolverOptions
) => string | undefined = (compName, opts) => {
const { importStyle = true, importCss = true, importLess = false } = opts
if (importStyle) {
if (importLess) {
const styleDir = getStyleDir(compName)
return `ant-design-vue/es/${styleDir}/style`
}
else if (importCss) {
const styleDir = getStyleDir(compName)
return `ant-design-vue/es/${styleDir}/style/css`
}
}
}
已经合了pr
插件使用配置方式如下:
一共加了三个参数,默认什么都不传,如果传参则通过对象的形式传 默认值:
importStyle: true
importCss:true
importLess:false
- 只加载css
AntDesignVueResolver()
- 只加载less
AntDesignVueResolver({
importLess: true
})
说明:需要开启less的 javascriptEnabled: true,
- 不加载组件样式:
AntDesignVueResolver({
importStyle: false
})
插件参数定义如下:
export interface AntDesignVueResolverOptions {
/**
* import style along with components
*
* @default true
*/
importStyle?: boolean
/**
* import css along with components
*
* @default true
*/
importCss?: boolean
/**
* import less along with components
*
* @default false
*/
importLess?: boolean
}
性能测试
有同学建议我的优化循环算法,将时间复杂度O(n)优化为O(1)使用map进行映射, 但是使用正则匹配我也是有我自己的取舍的,因为如果用map存储组件和样式路径的映射,有以下缺点
- 需要写大量的映射配置, 比如ListItem和ListItemMeta就要配置成两项
var map = {
ListItem: 'list',
ListItemMeta: 'list'
}
而使用正则,只需要配置一行,而且无论List再加子组件,一行正则也能匹配上,而用上面的map就需要再追加配置,可维护性降低
var reg = {
pattern: /^List/
dir: 'list'
}
- 性能差别,为了彻底搞明白,映射和正则在这种小数据量到底有多大差别,特意写了一个性能测试的方法,查看区别,代码地址:codesandbox.io/s/zhengzepi…
结果:基本上两者差别在0.1ms左右,而且互有胜负,map并不具备压倒性优势,也许数据量小,数据量大了可能就有区别了,但是我觉得在性能差别不大的情况下,选择可维护性更高的方案
相关issue和代码
vite-plugin-components 有关该问题的issue
代码: github——todo项目地址
其他
最近支持了部分vue2组件库(view ui 和element ui) 总结了两篇文章如下: