如何二次封装一个Vue3组件库?
为什么要二次封装组件库
目前开源的Vue3组件库有很多,包括Element Plus、Ant Design Vue、Naive UI、Vuetify、Varlet等等。
在大部分场景中,我们直接使用现有组件库中的组件即可实现功能。如果遇到部分组件的特殊配置或者特殊逻辑,或者当前的组件库不满足需求,需要部分组件组合成为一个更大的组件,例如IP输入框、带固定样式的对话框等等,甚至我们有一些和组件库无关的自定义组件。如果这些组件在工程中有多处复用,我们一般都会将组件单独写到工程的Components中,方便各个页面调用。
假设这时候有多个独立的工程,这些工程都需要复用我们之前在Components中写的自定义组件。这时候,我们可以选择在每个工程都复制一份代码使用,但是这样做不方便维护。如果我们发现组件有一些BUG,我们需要在一个工程中修复,然后复制同步给其它工程。这样维护低效而且容易遗漏。而且如果其它工程的开发者修改了组件(也许有需求但是并未通知我们),那么我们同步复制代码之后,就会把其它开发者的代码覆盖掉。
这时候,更好的方式是将这些在不同的工程中复用的组件抽出来,封装为一个独立的组件库。这样我们的可复用组件代码在一个地方修改和维护。同时也有文档和版本控制等功能,方便其它工程集成。
甚至有时候可以做一个扩展组件库,扩展现有的组件库的能力,给大家提供更多的通用组件。
依赖说明
依赖种类
开发npm包与开发前端页面工程不同,对于依赖有着更精细的控制:
- 开发前端页面工程:安装到
dependencies
和devDependencies
无区别。因为工程都要经过构建成dist成果物,本身是不需要依赖安装的。因此前端需要安装依赖的只有开发模式。 - 开发npm包:开发模式,生产模式有区别,依赖还区分作为自身依赖还是宿主依赖。
注:上述描述是针对大部分工程,如果项目中配置了特殊模式,依赖安装方式也有区别。
这里简述一下package.json
中我们会使用的依赖种类:
dependencies
生产依赖
这类依赖在开发环境,生产环境都会被安装。在npm包被集成到宿主工程时,会作为npm包本身的依赖被安装。
npm add vue
devDependencies
开发依赖
只在开发环境被安装的依赖,生产环境不安装。在npm包被集成到宿主工程时,也不会被安装。
pnpm add -D vue
peerDependencies
对等依赖
我觉得也可以叫做“宿主依赖”。这类依赖在开发环境会被安装,但生产构建时不会打包进成果物中。在npm包被集成到宿主工程时,也不会被安装(npm部分版本会自动安装)。但是会提示要求宿主工程本身安装这个依赖。
pnpm add --save-peer vue
如何选择依赖种类
以vue为例
既然是要作Vue3的库,肯定需要安装vue这个依赖了。那么我们应该怎么选择依赖种类呢?
devDependencies
排除
生产模式下,我们的组件库也是需要vue作为依赖的。(或许不用也行,但是目前不选择那种方式)dependencies
可以但不建议
生产模式和开发模式下都会安装,可以保证我们的组件库一直都能引用到。peerDependencies
推荐
生产模式下会安装,开发模式下被安装到宿主工程中,与宿主工程共享同一个vue依赖。
如果选择dependencies
,那么我们的组件库本身会安装一个vue依赖,宿主工程也会安装一个vue依赖,实际上安装了两个vue。这明显会浪费开发者的磁盘空间,而且降低了依赖安装的效率。(现在大部分包管理器都能处理这种情况,不会实际安装两个,但是这种设计显然不好)因此我们选择peerDependencies
。
以@vueuse/core
为例
那么,是不是宿主工程可能有的依赖,我们就一定要选择peerDependencies
呢?并不是。比如@vueuse/core
,这是一个基于vue的前端使用工具集合。我们在组件库中,仅需要使用其中的几个工具。例如useResizeObserver
。这个依赖宿主工程可能会用,也可能用不到。
peerDependencies
排除
我们需要将其设定为peerDependencies
,强制要求开发者必须安装么?当然不行。
为什么不行?因为这样会影响开发者使用。试着想开发者装了一个包,会提示开发者再手动安装几个依赖包。装完这些依赖包之后又会提示开发者装一堆依赖。虽然这些依赖可能是必要的,但是都手动让开发者装也太不方便了。dependencies
可以
生产模式和开发模式下都会安装,且在宿主工程中时,会作为依赖本身的包进行安装。devDependencies
可以
仅仅开发模式安装也可以??? 是的,但是需要加入构建流程。通过构建使依赖中的代码打入我们的成果物中,就不再需要生产依赖了。
以Vite为例
Vite是一个前端构建工具,大部分Vue3页面工程就是用它打包的。同样的我们的组件库也会使用Vite打包。构建工具仅仅在开发和构建时需要安装,构建之后作为npm包引入时就不需要了。因此Vite适合作为devDependencies
依赖安装。
封装简单组件
我们先从最简单开始,实现一个不需要构建流程,也不需要引入组件库的简单组件。
初始化工程
首先创建工程:
# 初始化项目
pnpm init
# 安装依赖
pnpm add --save-peer vue
创建必要的目录结构。我这里以一个简单的表示状态的圆圈组件功能为例。
|-- .gitignore
|-- package.json
|-- pnpm-lock.yaml
|-- src|-- index.js|-- components|-- StatusCircle.vue
组件实现
首先是src/components/StatusCircle.vue
的实现。我们先不使用TypeScript。通过代码可以看到,这就和正常写单文件组件一样。
<script setup>
defineProps({type: {// 类型 实际控制颜色// 'default' | 'error' | 'warning' | 'success' | 'info'type: String,default: 'default'},size: {// 圆圈的大小type: Number,default: 10}
})
</script><template><span class="circle" :class="[type]" :style="{ width: `${size}px`, height: `${size}px` }" />
</template><style scoped>
.circle {display: inline-block;margin-right: 8px;border-radius: 50%;
}
.default {background-color: #363636;
}
.error {background-color: #d03050;
}
.warning {background-color: #f0a020;
}
.success {background-color: #18a058;
}
.info {background-color: #2080f0;
}
</style>
再写下src/index.js
。
export { default as StatusCircle } from './components/StatusCircle.vue'
如果还有其它给用户使用的辅助函数,其它单文件组件等等也可以在这里导出。
package.json配置(1)
- 配置程序入口,module为ESModule引入方式的程序入口文件:
"module": "src/index.js"
- 需要配置给用户使用的npm包中包含哪些文件,只有配置过的文件才会出现在发布包中。目前我们只有一个src文件夹。
"files": [ "src" ]
然后再修改下部分必要的配置,例如包名称和版本号。
"name": "sec-test",
"version": "1.0.0",
然后就可以直接发布npm包了:
pnpm publish --no-git-checks
工程中尝试引入
发布之后,我们就可以在Vue3页面工程中尝试引用了。
# 安装依赖
pnpm add -D sec-test
<script setup>
import { StatusCircle } from 'sec-test'
</script><template><div><StatusCircle type="warning" /><StatusCircle type="success" /></div>
</template>
此时页面工程中,就成功集成了我们自己的组件库了。
引入组件库
我们的文章标题叫做“二次封装组件库”,那么需要引入一个“一次封装”的组件库作为组件来源。这里使用Naive UI组件库作为示例。
选择依赖种类
“一次封装”的组件库应该作为哪种依赖呢?首先要明确使用场景。二次封装的组件库,一般都作为“一次封装”的主要组件库的补充,即可以理解为,使用二次封装组件库的开发者,在项目中都会引入主要组件库。
而且组件库包的体积一般都比较大。因此可以将“一次封装”的组件库作为peerDependencies
引入。
pnpm add --save-peer naive-ui
目录结构(1)
我们保留上一节的代码为基础。再用一个很简单的,需要二次封装的场景来举例————带提示的按钮组件,由NButton, NPopover组合而成,为按钮添加悬浮提示文本。
|-- .gitignore
|-- package.json
|-- pnpm-lock.yaml
|-- src|-- index.js|-- components|-- StatusCircle.vue|-- TipButton.vue
TipButton组件源码
<script setup>
import { NPopover, NButton } from 'naive-ui'
import { computed, useSlots } from 'vue'defineOptions({ inheritAttrs: false })const props = defineProps({// popover 的触发方式tipTrigger: {type: String,default: 'hover',},// popover 的弹出位置tipPlacement: {type: String,default: 'top',},// popover 内容tip: {type: String,default: '',},// 是否禁用 popovertipDisabled: {type: Boolean,default: false,},// --- 剩余属性继承 HButton
})
const slots = useSlots()const tipDisabledComp = computed(() => {// 手动设置 禁用时肯定禁用if (props.tipDisabled) return props.tipDisabled// slot有值不禁用if (slots.tip) return false// props有值不禁用if (props.tip) return false// 无值的时候禁用return true
})
</script><template><NPopover :trigger="tipTrigger" :placement="tipPlacement" :disabled="tipDisabledComp"><template #trigger><NButton tag="div" v-bind="$attrs"><template v-for="(item, key, index) in $slots" :key="index" #[key]><slot v-if="key !== 'tip'" :name="key" /></template></NButton></template><slot name="tip"><template v-if="tip">{{ tip }}</template></slot></NPopover>
</template>
在index.js中导出:
export { default as StatusCircle } from './components/StatusCircle.vue'
export { default as TipButton } from './components/TipButton.vue'
然后把版本号增加一下,直接发布版本即可。
TipButton使用方式
我们把“一次封装”组件库中的NButton和NPopover组合而成了TipButton,给按钮提供了悬浮提示的扩展能力,而且不影响按钮原本的已有能力,即按钮本身的Props、事件、Slots都能正常使用。
<template><!-- 多语言使用 --><TipButton style="margin-right: 8px" :tip-disabled="true" type="primary" @click="addProject"><template #icon><NIcon><AddOutline /></NIcon></template><template #tip>创建项目</template>新增</TipButton><TipButton secondary tip="删除项目" :disabled="!keySelection.length" @click="deleteProject(keySelection)"><template #icon><n-icon><TrashBinOutline /></n-icon></template>删除</TipButton>
</template><script setup>
import { TipButton } from 'sec-test'
</script>
可以看到在大部分场景下,我们只需要将之前写的NButton标签改名为TipButton,不用修改其它代码。再增加tip相关属性即可。
在tip属性方面,同时提供了prop形式和slot形式。Props形式适用于简单的一句话形式的悬浮提示,slot形式适用于更复杂的自定义提示内容。同时针对NPopover在提示内容为空时,依然展示空的悬浮提示的情况,我们根据prop和slot是否存在内容判断是否展示悬浮提示,如果都不存在则禁用。
有人会说,哪里的按钮需要提示就自己加NPopover就好了,这样封装多此一举。是的,如果只有一个地方的按钮需要用到悬浮提示,这样做确实多余。但是假设如果你有10-20个位置需要这种悬浮提示按钮,而且多个工程都需要,这样的封装就有意义了。这种封装简化了代码结构,并未增加多少心智负担。我们列出了最简单的使用情况,经过封装确实简洁很多。
<!-- 使用封装 -->
<TipButton tip="创建项目">新增</TipButton>
<!-- 未封装 -->
<NPopover><template #trigger><NButton>新增</NButton></template><span>创建项目</span>
</NPopover>
透传能力
像上面这样,在原有组件库的基础上进行封装和扩展的情况还有很多。在扩展能力的同时,也需要注意依然提供原有的组件库能力。通过上面的例子,我们来整理一下如何提供能力。
透传Props
关于这部分我们可以参考Vue3文档中的透传 Attributes。这分为两种情况:
- 希望透传Props的组件正好是二次封装组件的根元素上,那么可以直接利用Vue的透传attribute特性,透传到原有组件的Props上。
<!-- 封装后使用 -->
<SecButton type="primary" /><!-- 二次封装组件 -->
<template><NButton />
</template><!-- 实际透传后效果 -->
<NButton type="primary" />
- 希望透传Props的组件并不是二次封装组件的根元素。这样需要禁用Attributes继承,然后手动指定继承位置。例如上面的TipButton就是这样。简化一下:
<!-- 二次封装组件 -->
<template><NPopover><template #trigger><!-- 指定透传元素 --><NButton v-bind="$attrs" /></template></NPopover>
</template><script setup>
// 禁用Attributes继承
defineOptions({ inheritAttrs: false })
</script>
可以看到,手动指定了继承位置,因此我们的attributes会透传给NButton,作为Props接收。
<!-- 封装后使用 -->
<TipButton type="primary" /><!-- 实际透传后效果 -->
<NPopover><template #trigger><NButton type="primary" /></template>
</NPopover>
如果不禁止根元素Attributes继承,但同时指定了NButton继承,那么根元素(NPopover)和NButton会同时继承attributes。在大部分场景下,这是冲突的。看一个例子:
<!-- 不禁止根元素Attributes继承的示例 -->
<!-- 封装后使用 -->
<TipButton class="button-class" :disabled="true" /><!-- 实际透传后效果 -->
<NPopover class="button-class" :disabled="true" ><template #trigger><NButton class="button-class" :disabled="true" /></template>
</NPopover>
可以看到,class同时被绑定到了NPopover和NButton上。更严重的是,由于NPopover和NButton的禁用prop都是disabled,希望禁用NButton的时候,也会同时禁用NPopover。因此解决方案是禁止根元素Attributes继承,然后为NPopover的disabled重新起一个名字传递。
透传事件
透传事件和透传Props的规则是一致的,都使用Attributes继承的规则,这里不重复描述了。但是事件和class、style类似,都有合并的规则。即自身定义了事件处理器,又透传了事件处理器,可以同时生效。
<!-- 封装后使用 -->
<SecButton @click="click1" /><!-- 二次封装组件 -->
<template><NButton @click="click2" />
</template>
可以看到在二次封装时,在NButton上定义了click事件处理器。我们透传的attributes又提供了一个click事件处理器。这两个并不会覆盖,而是会同时生效。对于手动继承Attributes的场景也一样,也会同时生效。
<!-- 封装后使用 -->
<TipButton @click="click1" /><!-- 二次封装组件 -->
<template><NPopover><template #trigger><!-- 指定透传元素 --><NButton v-bind="$attrs" @click="click2" /></template></NPopover>
</template>
透传插槽Slots
对于插槽,Vue提供了$slots表示父组件所传入插槽的对象。我们遍历这个对象,用对象key来匹配原组件的slot,然后在内部抛出二次封装组件库的slot。(有点绕口,来看下例子)
<!-- 封装后使用 -->
<SecButton><template #icon><AddOutline /></template><span>创建项目</span>
<SecButton><!-- 二次封装组件 -->
<NButton><template v-for="(item, key, index) in $slots" :key="index" #[key]><slot :name="key" /></template>
</NButton><!-- 实际透传后效果 -->
<NButton><template #icon><AddOutline /></template><template #default><span>创建项目</span></template>
</NButton>
使用v-for遍历$slots,key是插槽的key。<template>
那一层表示的是匹配NButton的slot,即<template>
内部的内容就是被嵌入进NButton内部的插槽中。下一层的slot,是我们二次封装的组件抛出给外部的插槽。因此,外部实际传入的插槽内容就被展示到slot处,而slot被<template>
包裹,实际上被嵌入进NButton内部的插槽中。
透传实例方法
在Vue3的组合式写法中,我们使用defineExpose暴露属性和方法。然后使用ref访问。那么在二次封装的组件中,如果想要透传抛出给外部呢?
很遗憾,我没有在Vue3中找到类似$slots,$attrs
这种可以获取到组件暴露的全部属性和方法的对象。因此只能手动一个一个转发了。
<script setup>
const popoverRef = ref(null)
// 手动转发
defineExpose({tipSetShow: (show) => popoverRef.value?.setShow(show),tipSyncPosition: () => popoverRef.value?.syncPosition(),
})
</script>
<template><NPopover ref="popoverRef" />
</template>
加入构建流程
使用构建的理由
既然不通过构建就能开发npm包,为何要进行构建呢?
- 例如上面提到的
@vueuse/core
,将部分依赖直接集成到构建包中,减少生产时的依赖项。利用Treeshaking,只把依赖中用到的代码打进构建包中。 - 经过构建和压缩,可以减少代码体积,提高下载速度。
- 如果是非开源的代码库,可以隐藏源码。虽然即使经过打包和压缩依然是js代码,还是可以分析出来。但是至少分析的难度提高了一点。
- 通过构建时的polyfill等配置,可以提高代码在浏览器中运行的兼容性。
- 将typescript代码翻译为js代码,使开发者不需要ts也能正常使用。
目录结构(2)
我们依然使用之前的代码,在此基础上加入构建模式。这是加入构建流程后的目录结构。
|-- .gitignore
|-- package.json
|-- pnpm-lock.yaml
|-- vite.config.js
|-- dist
| |-- index.mjs
| |-- style.css
|-- src|-- index.js|-- components|-- StatusCircle.vue|-- TipButton.vue
Vite构建配置
我们使用vite作为构建工具。所有构建工具基本都是devDependencies
。安装必要的依赖:
pnpm add -D vite @vitejs/plugin-vue
# CSS预处理器,用于扩展CSS的功能,可以安装其它工具,也可以不装
pnpm add -D sass
创建根目录创建vite.config.js。
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'export default defineConfig({build: {// 库模式配置lib: {// 入口文件entry: './src/index.js',// ESModule模式 formats: ['es'],// 输出的文件名fileName: 'index'},rollupOptions: {// 外部化处理那些你不想打包进库的依赖external: ['vue', 'naive-ui'],output: {// 为外部化的依赖提供一个全局变量globals: {vue: 'Vue'}}}},// 构建插件plugins: [vue()]
})
在Vite中有一种库模式,是专门为了开发库工具的构建模式:Vite文档-库模式
库模式不使用HTML作为入口,而是用一个js/ts文件做入口。我们还要配置模式(提供给浏览器的一般是es模式)以及一些其他配置。在peerDependencies
中的那些不构建的依赖则需要在rollupOptions中声明。上面的配置中都写了对应的注释。
package.json配置(2)
配置好Vite之后,还要修改下项目package.json。首先增加执行构建的脚本:
"scripts": {"build": "vite build"
},
然后就可以尝试进行构建了。执行脚本:
pnpm build
可以看到新增了dist文件夹,里面有构建后的代码逻辑和导出的css。这个dist文件夹属于成果物,并不属于代码,因此在.gitignore
中要排除这个目录。
既然有了构建流程,那么我们提供给开发者集成的就是构建后的dist文件夹而不是src,因此还要继续修改导出配置。
"module": "dist/index.mjs",
"files": [ "dist" ],
这样发布包中就不包含src源码了。然后把版本号增加一下,直接发布版本即可。
注意,开发者在集成我们的npm包时,需要单独引入css,整个项目中引入一次即可。
import 'sec-test/dist/style.css'
支持TypeScript
很多人开发前端工程都喜欢使用TypeScript,它可以提供类型检查,提高代码的规范性和可维护性。虽然网络上对于TypeScript有些争议,但是既然有大量的开发者使用TS,那也应该提供对应的支持。
目录结构(3)
我们依然使用之前的代码,在此基础上增加对TypeScript的支持。这是完成后的目录结构。
|-- .gitignore
|-- package.json
|-- pnpm-lock.yaml
|-- tsconfig.json
|-- tsconfig.tsbuildinfo
|-- vite.config.ts
|-- dist
| |-- index.mjs
| |-- style.css
|-- dts
| |-- tsconfig.tsbuildinfo
| |-- src
| |-- index.d.ts
| |-- components
| |-- StatusCircle.vue.d.ts
| |-- TipButton.vue.d.ts
|-- src|-- index.ts|-- components|-- StatusCircle.vue|-- TipButton.vue
配置TypeScript
首先安装typescript依赖。TypeScript相关的依赖也只是开发模式下使用。还有安装一下vue相关的ts配置文件扩展。
pnpm add -D typescript
pnpm add -D @vue/tsconfig
然后在根目录下创建tsconfig.json
,写入相关配置。这里直接使用了@vue/tsconfig
提供的配置预设。
{"extends": "@vue/tsconfig/tsconfig.dom.json","include": ["src/**/*", "src/**/*.vue"],"outDir": "dts","compilerOptions": {"composite": true,"baseUrl": ".","paths": {"@/*": ["./src/*"]}}
}
其中的outDir是类型文件的输出位置,下面会用到。
使用TypeScript改写代码
首先是TipButton.vue
。
<script lang="ts" setup>
import { NPopover, NButton } from 'naive-ui'
import { computed, useSlots } from 'vue'defineOptions({ inheritAttrs: false })interface Props {// popover 的触发方式tipTrigger?: 'hover' | 'click' | 'focus' | 'manual'// popover 的弹出位置tipPlacement?: 'top-start' | 'top' | 'top-end' | 'right-start' | 'right' | 'right-end' | 'bottom-start' | 'bottom' | 'bottom-end' | 'left-start' | 'left' | 'left-end'// popover 内容tip?: string// 是否禁用 popovertipDisabled?: boolean// --- 剩余属性继承 HButton
}const props = withDefaults(defineProps<Props>(), {tipTrigger: 'hover',tipPlacement: 'top',tip: '',tipDisabled: false,
})
const slots = useSlots()const tipDisabledComp = computed(() => {// 手动设置 禁用时肯定禁用if (props.tipDisabled) return props.tipDisabled// slot有值不禁用if (slots.tip) return false// props有值不禁用if (props.tip) return false// 无值的时候禁用return true
})
</script><template><NPopover :trigger="tipTrigger" :placement="tipPlacement" :disabled="tipDisabledComp"><template #trigger><NButton tag="div" v-bind="$attrs"><template v-for="(item, key, index) in $slots" :key="index" #[key]><slot v-if="key !== 'tip'" :name="key" /></template></NButton></template><slot name="tip"><template v-if="tip">{{ tip }}</template></slot></NPopover>
</template>
然后是StatusCircle.vue
。
<script lang="ts" setup>
interface Props {// 类型 实际控制颜色type?: "default" | "error" | "warning" | "success" | "info";// 圆圈的大小size?: number;
}
withDefaults(defineProps<Props>(), {type: "default",size: 10,
});
</script><template><span class="circle" :class="[type]" :style="{ width: `${size}px`, height: `${size}px` }" />
</template><style scoped>
.circle {display: inline-block;margin-right: 8px;border-radius: 50%;
}
.default {background-color: #363636;
}
.error {background-color: #d03050;
}
.warning {background-color: #f0a020;
}
.success {background-color: #18a058;
}
.info {background-color: #2080f0;
}
</style>
目前工程中的其它部分文件可以直接改个文件后缀名,将js改成ts即可。这个可以参考目录结构中的名字。另外在vite.config.ts
中把入口文件名改掉:
entry: './src/index.ts',
生成类型文件
虽然把文件改写成了ts的形式,但是提供给用户的依然是js文件,文件本身是不带类型的。因为我们的工程也要适配那些不使用ts的开发者。我们以单独类型文件的形式提供。所以构建流程和入口文件不变,但是多了一步生成类型文件的步骤。
继续安装生成类型文件的依赖,使用vue提供的vue-tsc。
pnpm add -D vue-tsc
然后修改package.json,加入生成类型文件的相关脚本,构建的时候也一起生成类型。
"scripts": {"build": "vite build && pnpm build:dts","build:dts": "vue-tsc --declaration --emitDeclarationOnly"
}
然后尝试生成类型:
pnpm build:dts
然后会发现,在dts文件夹(上面配置的目录)中生成了一些类型文件。其中的dts/src/index.d.ts是对应的类型入口文件。
package.json最后配置
package.json中有专门提供ts类型文件的位置,配置为类型入口文件:
"types": "dts/src/index.d.ts",
"files": [ "dist", "dts/src" ]
现在的发布包中不仅要包含构建后的代码,还要包含类型文件。上面脚本配置构建时,也同时生成了类型文件。因此我们把版本号增加一下,直接进行构建,发布版本即可。
提供一下package.json最后的配置。
{"name": "sec-test","version": "1.0.0","description": "","module": "dist/index.mjs","types": "dts/src/index.d.ts","scripts": {"build": "vite build && pnpm build:dts","build:dts": "vue-tsc --declaration --emitDeclarationOnly"},"keywords": [],"author": "","license": "ISC","peerDependencies": {"naive-ui": "^2.35.0","vue": "^3.3.4"},"devDependencies": {"@vitejs/plugin-vue": "^4.4.0","@vue/tsconfig": "^0.4.0","typescript": "^5.2.2","vite": "^4.5.0","vue-tsc": "^1.8.22"},"files": [ "dist", "dts/src" ]
}
总结
上面提到的,仅仅是在封装vue组件库时一些基础的工程化方法。了解这些就可以开发基础的二次封装组件库了。
但实际上不管是组件库还是前端工程化都是一个比较复杂的主题。对于组件库封装,要考虑如何设计组件,还有交互样式、抛出的API、版本兼容、换肤、换不同国家和地区的差异等等。对于工程化,要考虑依赖关系、工程组织、体积优化、按需引入、TreeShaking等等。真正成熟的组件库要复杂的多。
参考
- Element Plus 基于Vue3,面向设计师和开发者的组件库
https://element-plus.org/zh-CN/ - Ant Design Vue
https://www.antdv.com/components/overview-cn - Naive UI 一个 Vue 3 组件库
https://www.naiveui.com/zh-CN/os-theme - Vuetify 一个功能强大的 Vue 组件框架
https://vuetifyjs.com/zh-Hans/ - Varlet 基于 Vue3 开发的 Material 风格移动端组件库
https://varlet.gitee.io/varlet-ui/#/zh-CN/index - 探索Vue 3世界中的12个流行组件库
https://juejin.cn/post/7250091744526647352 - pnpm 文档
https://www.pnpm.cn/cli/add - VueUse Collection of Vue Composition Utilities 文档
https://vueuse.org/ - Vue文档 透传 Attributes
https://cn.vuejs.org/guide/components/attrs.html - Vite文档 构建生产版本 库模式
https://cn.vitejs.dev/guide/build.html#library-mode - Vite文档 构建选项 build-lib
https://cn.vitejs.dev/config/build-options.html#build-lib - 前端工程化学习笔记
https://static.kancloud.cn/cyyspring/webpack/3064323
相关文章:
如何二次封装一个Vue3组件库?
为什么要二次封装组件库 目前开源的Vue3组件库有很多,包括Element Plus、Ant Design Vue、Naive UI、Vuetify、Varlet等等。 在大部分场景中,我们直接使用现有组件库中的组件即可实现功能。如果遇到部分组件的特殊配置或者特殊逻辑,或者当前…...

2024年网络安全比赛--系统渗透测试(超详细)
一、竞赛时间 180分钟 共计3小时 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 1.在渗透机中对服务器主机进行信息收集,将服务器开启的端口号作为 Flag 值提交; 2.在渗透机中对服务器主机进行渗透,在服务器主机中获取服务器主机名称ÿ…...
高效的单行python脚本
#-- coding: utf-8 -- “”" Created on Wed Dec 6 13:42:00 2023 author: czliu “”" 1. 平方列表推导 #使用列表推导法计算从 1 到 10 的数字平方 squares [x**2 for x in range(1, 11)] print(squares)2.求偶数 #可以使用列表推导式从列表中筛选偶数。还可以…...

如何通过内网穿透实现无公网IP也能远程访问内网的宝塔面板
文章目录 一、使用官网一键安装命令安装宝塔二、简单配置宝塔,内网穿透三、使用固定公网地址访问宝塔 宝塔面板作为建站运维工具,适合新手,简单好用。当我们在家里/公司搭建了宝塔,没有公网IP,但是想要在外也可以访问内…...

【广州华锐互动】VR沉浸式体验铝厂安全事故让伤害教育更加深刻
随着科技的不断发展,虚拟现实(VR)技术已经逐渐渗透到各个领域,为我们的生活带来了前所未有的便捷和体验。在安全生产领域,VR技术的应用也日益受到重视。 VR公司广州华锐互动就开发了多款VR安全事故体验系统,…...
CFLAGS、CXXFLAGS、FFLAGS、FCFLAGS、LDFLAGS、LD_LIBRARY_PATH区别
这些环境变量在编译和链接过程中扮演着重要的角色。下面是对每个环境变量的详细说明及示例: CFLAGS:用于设置C编译器的编译选项。 示例:将优化级别设置为最高,启用所有警告信息,并指定目标体系结构为x86-64。 export C…...

阿里云租赁费用_阿里云服务器多配置报价表
阿里云服务器租用费用,云服务器ECS经济型e实例2核2G、3M固定带宽99元一年、轻量应用服务器2核2G3M带宽轻量服务器一年87元,2核4G4M带宽轻量服务器一年165元12个月,ECS云服务器e系列2核2G配置99元一年、2核4G配置365元一年、2核8G配置522元一年…...

网络层(1)——概述
一、概述 网络层毫无疑问是最复杂的一层,涉及到大量的协议与结构的内容。在如今主流的设计中,大家都会把网络层分成两个部分:数据平面、控制平面。其中数据平面指的是网络层中每台路由器的功能,它决定了到达路由器端口输入链路之一…...
计算机网络——网络层
目录 一、网络层的作用 二、网络层的协议 (一)ARP地址解析协议 (二)ICMP国际控制报文协议 (三)IGMP网际组织管理协议 三、ip地址 (一)ip地址的概念 (二ÿ…...
Antd search input无中框
发现input.search, 搜索图标的左侧有个竖线,不是很好看 把它改掉, 新建一个自己的CSS .custom-search-input{.ant-input-affix-wrapper{border-right: none !important;}.ant-input-group-addon{.ant-btn{border-left: none !important;}}}应用 <S…...
【PyTorch】概述
文章目录 1. PyTorch是什么?2. PyTorch的特点3. PyTorch的架构 1. PyTorch是什么? PyTorch是一个深度学习框架,由Facebook于2016年开源发布。PyTorch是基于Torch框架的Python接口,旨在提供易用的强大工具来进行神经网络的构建和训…...
非对象集合交、并、差处理
对于集合取交集、并集的处理其实有很多种方式,这里就介绍3种 第一种 是CollectionUtils工具类 第二种 是List自带方法 第三种 是JDK1.8 stream 新特性 1、CollectionUtils工具类 下面对于基本数据(包扩String)类型中的集合进行demo示例。 public static void main(String[]…...

时间序列预测实战(二十五)PyTorch实现Seq2Seq进行多元和单元预测(附代码+数据集+完整解析)
一、本文介绍 本文给大家带来的时间序列模型是Seq2Seq,这个概念相信大家都不陌生了,网上的讲解已经满天飞了,但是本文给大家带来的是我在Seq2Seq思想上开发的一个模型和新的架构,架构前面的文章已经说过很多次了,其是…...

电子学会C/C++编程等级考试2022年09月(三级)真题解析
C/C++等级考试(1~8级)全部真题・点这里 第1题:课程冲突 小 A 修了 n 门课程, 第 i 门课程是从第 ai 天一直上到第 bi 天。 定义两门课程的冲突程度为 : 有几天是这两门课程都要上的。 例如 a1=1,b1=3,a2=2,b2=4 时, 这两门课的冲突程度为 2。 现在你需要求的是这 n 门课…...

【数据库】基于时间戳的并发访问控制,乐观模式,时间戳替代形式及存在的问题,与封锁模式的对比
使用时间戳的并发控制 专栏内容: 手写数据库toadb 本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。 本专栏会…...

Python 日志(略讲)
日志操作 日志输出: # 输出日志信息 logging.debug("调试级别日志") logging.info("信息级别日志") logging.warning("警告级别日志") logging.error("错误级别日志") logging.critical("严重级别日志")级别设置…...

C++ 指针进阶
目录 一、字符指针 二、指针数组 三、数组指针 数组指针的定义 &数组名 与 数组名 数组指针的使用 四、数组参数 一维数组传参 二维数组传参 五、指针参数 一级指针传参 二级指针传参 六、函数指针 七、函数指针数组 八、指向函数指针数组的指针 九、回调函…...
stm32中滴答定时器与普通定时器的区别
1、两者在单片机中的位置不一样 滴答定时器在内核上,普通定时器在外设上。 由于位置不同,滴答定时器的程序可以移植到所有相同内核的芯片上,但普通定时器的程序却不可以。 2、两者的中断优先级不一样 滴答定时器优先级高,普通定…...

某60区块链安全之薅羊毛攻击实战一学习记录
区块链安全 文章目录 区块链安全薅羊毛攻击实战一实验目的实验环境实验工具实验原理实验内容薅羊毛攻击实战一 实验步骤EXP利用 薅羊毛攻击实战一 实验目的 学会使用python3的web3模块 学会分析以太坊智能合约薅羊毛攻击漏洞 找到合约漏洞进行分析并形成利用 实验环境 Ubun…...

Java程序员,你掌握了多线程吗?(文末送书)
目录 01、多线程对于Java的意义02、为什么Java工程师必须掌握多线程03、Java多线程使用方式04、如何学好Java多线程送书规则 摘要:互联网的每一个角落,无论是大型电商平台的秒杀活动,社交平台的实时消息推送,还是在线视频平台的流…...

TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...

【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...