当前位置: 首页 > news >正文

如何二次封装一个Vue3组件库?

为什么要二次封装组件库

目前开源的Vue3组件库有很多,包括Element Plus、Ant Design Vue、Naive UI、Vuetify、Varlet等等。

在大部分场景中,我们直接使用现有组件库中的组件即可实现功能。如果遇到部分组件的特殊配置或者特殊逻辑,或者当前的组件库不满足需求,需要部分组件组合成为一个更大的组件,例如IP输入框、带固定样式的对话框等等,甚至我们有一些和组件库无关的自定义组件。如果这些组件在工程中有多处复用,我们一般都会将组件单独写到工程的Components中,方便各个页面调用。

假设这时候有多个独立的工程,这些工程都需要复用我们之前在Components中写的自定义组件。这时候,我们可以选择在每个工程都复制一份代码使用,但是这样做不方便维护。如果我们发现组件有一些BUG,我们需要在一个工程中修复,然后复制同步给其它工程。这样维护低效而且容易遗漏。而且如果其它工程的开发者修改了组件(也许有需求但是并未通知我们),那么我们同步复制代码之后,就会把其它开发者的代码覆盖掉。

这时候,更好的方式是将这些在不同的工程中复用的组件抽出来,封装为一个独立的组件库。这样我们的可复用组件代码在一个地方修改和维护。同时也有文档和版本控制等功能,方便其它工程集成。

甚至有时候可以做一个扩展组件库,扩展现有的组件库的能力,给大家提供更多的通用组件。

依赖说明

依赖种类

开发npm包与开发前端页面工程不同,对于依赖有着更精细的控制:

  • 开发前端页面工程:安装到dependenciesdevDependencies无区别。因为工程都要经过构建成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。这分为两种情况:

  1. 希望透传Props的组件正好是二次封装组件的根元素上,那么可以直接利用Vue的透传attribute特性,透传到原有组件的Props上。
<!-- 封装后使用 -->
<SecButton type="primary" /><!-- 二次封装组件 -->
<template><NButton />
</template><!-- 实际透传后效果 -->
<NButton type="primary" />
  1. 希望透传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包,为何要进行构建呢?

  1. 例如上面提到的@vueuse/core,将部分依赖直接集成到构建包中,减少生产时的依赖项。利用Treeshaking,只把依赖中用到的代码打进构建包中。
  2. 经过构建和压缩,可以减少代码体积,提高下载速度。
  3. 如果是非开源的代码库,可以隐藏源码。虽然即使经过打包和压缩依然是js代码,还是可以分析出来。但是至少分析的难度提高了一点。
  4. 通过构建时的polyfill等配置,可以提高代码在浏览器中运行的兼容性。
  5. 将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组件库有很多&#xff0c;包括Element Plus、Ant Design Vue、Naive UI、Vuetify、Varlet等等。 在大部分场景中&#xff0c;我们直接使用现有组件库中的组件即可实现功能。如果遇到部分组件的特殊配置或者特殊逻辑&#xff0c;或者当前…...

2024年网络安全比赛--系统渗透测试(超详细)

一、竞赛时间 180分钟 共计3小时 二、竞赛阶段 竞赛阶段 任务阶段 竞赛任务 竞赛时间 分值 1.在渗透机中对服务器主机进行信息收集&#xff0c;将服务器开启的端口号作为 Flag 值提交; 2.在渗透机中对服务器主机进行渗透&#xff0c;在服务器主机中获取服务器主机名称&#xff…...

高效的单行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也能远程访问内网的宝塔面板

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

【广州华锐互动】VR沉浸式体验铝厂安全事故让伤害教育更加深刻

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术已经逐渐渗透到各个领域&#xff0c;为我们的生活带来了前所未有的便捷和体验。在安全生产领域&#xff0c;VR技术的应用也日益受到重视。 VR公司广州华锐互动就开发了多款VR安全事故体验系统&#xff0c…...

CFLAGS、CXXFLAGS、FFLAGS、FCFLAGS、LDFLAGS、LD_LIBRARY_PATH区别

这些环境变量在编译和链接过程中扮演着重要的角色。下面是对每个环境变量的详细说明及示例&#xff1a; CFLAGS&#xff1a;用于设置C编译器的编译选项。 示例&#xff1a;将优化级别设置为最高&#xff0c;启用所有警告信息&#xff0c;并指定目标体系结构为x86-64。 export C…...

阿里云租赁费用_阿里云服务器多配置报价表

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

网络层(1)——概述

一、概述 网络层毫无疑问是最复杂的一层&#xff0c;涉及到大量的协议与结构的内容。在如今主流的设计中&#xff0c;大家都会把网络层分成两个部分&#xff1a;数据平面、控制平面。其中数据平面指的是网络层中每台路由器的功能&#xff0c;它决定了到达路由器端口输入链路之一…...

计算机网络——网络层

目录 一、网络层的作用 二、网络层的协议 &#xff08;一&#xff09;ARP地址解析协议 &#xff08;二&#xff09;ICMP国际控制报文协议 &#xff08;三&#xff09;IGMP网际组织管理协议 三、ip地址 &#xff08;一&#xff09;ip地址的概念 &#xff08;二&#xff…...

Antd search input无中框

发现input.search&#xff0c; 搜索图标的左侧有个竖线&#xff0c;不是很好看 把它改掉, 新建一个自己的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是什么&#xff1f;2. PyTorch的特点3. PyTorch的架构 1. PyTorch是什么&#xff1f; PyTorch是一个深度学习框架&#xff0c;由Facebook于2016年开源发布。PyTorch是基于Torch框架的Python接口&#xff0c;旨在提供易用的强大工具来进行神经网络的构建和训…...

非对象集合交、并、差处理

对于集合取交集、并集的处理其实有很多种方式,这里就介绍3种 第一种 是CollectionUtils工具类 第二种 是List自带方法 第三种 是JDK1.8 stream 新特性 1、CollectionUtils工具类 下面对于基本数据(包扩String)类型中的集合进行demo示例。 public static void main(String[]…...

时间序列预测实战(二十五)PyTorch实现Seq2Seq进行多元和单元预测(附代码+数据集+完整解析)

一、本文介绍 本文给大家带来的时间序列模型是Seq2Seq&#xff0c;这个概念相信大家都不陌生了&#xff0c;网上的讲解已经满天飞了&#xff0c;但是本文给大家带来的是我在Seq2Seq思想上开发的一个模型和新的架构&#xff0c;架构前面的文章已经说过很多次了&#xff0c;其是…...

电子学会C/C++编程等级考试2022年09月(三级)真题解析

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

【数据库】基于时间戳的并发访问控制,乐观模式,时间戳替代形式及存在的问题,与封锁模式的对比

使用时间戳的并发控制 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会…...

Python 日志(略讲)

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

C++ 指针进阶

目录 一、字符指针 二、指针数组 三、数组指针 数组指针的定义 &数组名 与 数组名 数组指针的使用 四、数组参数 一维数组传参 二维数组传参 五、指针参数 一级指针传参 二级指针传参 六、函数指针 七、函数指针数组 八、指向函数指针数组的指针 九、回调函…...

stm32中滴答定时器与普通定时器的区别

1、两者在单片机中的位置不一样 滴答定时器在内核上&#xff0c;普通定时器在外设上。 由于位置不同&#xff0c;滴答定时器的程序可以移植到所有相同内核的芯片上&#xff0c;但普通定时器的程序却不可以。 2、两者的中断优先级不一样 滴答定时器优先级高&#xff0c;普通定…...

某60区块链安全之薅羊毛攻击实战一学习记录

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

Java程序员,你掌握了多线程吗?(文末送书)

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

排序算法——桶排序/基数排序/计数排序

桶排序 是计数排序的升级版。它利用了函数的映射关系&#xff0c;高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理&#xff1a; 假设输入数据服从均匀分布&#xff0c;将数据分到有限数量的桶里&#xff0c;每个桶再分别排序&#xff08;有可能再使…...

FFmpeg之将视频转为16:9(横屏)或9:16(竖屏)(三十六)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…...

git学习笔记02(小滴课堂)

window 安装教程&#xff1a;https://www.yuque.com/u1106272/cai80g/skawco 查看&#xff1a; 创建文件夹&#xff1a; 我们把这个文件夹当作我们的暂存区。 这样就进入了工作区。 初始化&#xff1a; 可以看到.git文件夹。 查看本地仓库状态&#xff1a; 我们进入这个ide…...

2022 RedisDays 内容揭秘

上个月&#xff0c;Redis举办了3场线上会议&#xff0c;分别介绍了即将正式发布的Redis 7中包括的重要更新的内容&#xff0c;还有Redis完全重写的RedisJSON 2.0模块&#xff0c;和新发布的Redis Stack模块。除此之外&#xff0c;在此次线上会议中还介绍了现代化的软件架构与Re…...

论文阅读——Img2LLM(cvpr2023)

arxiv&#xff1a;[2212.10846] From Images to Textual Prompts: Zero-shot VQA with Frozen Large Language Models (arxiv.org) 一、介绍 使用大语言模解决VQA任务的方法大概两种&#xff1a;multi-modal pretraining and language-mediated VQA&#xff0c;即多模态预训练…...

南京大学考研机试题DP

3. dp 求子序列的个数 https://www.acwing.com/problem/content/description/3716/ #include <iostream> #include <cstring> #include <algorithm> #include <unordered_set> #include <vector> using namespace std; const int N 1e4 10…...

如何进行多ip服务器租用?

如何进行多ip服务器租用&#xff1f; 对于网络时代来说&#xff0c;是需要很多设备才能维持的&#xff0c;比如说多ip服务器就是互联网时代常见的设备&#xff0c;所以我们需要对多ip服务器有足够的了解&#xff0c;这样才能更好的获取互联网上的信息&#xff0c;满足我们工作…...

(动手学习深度学习)第13章 实战kaggle竞赛:树叶分类

文章目录 实战kaggle比赛&#xff1a;树叶分类1. 导入相关库2. 查看数据格式3. 制作数据集4. 数据可视化5. 定义网络模型6. 定义超参数7. 训练模型8. 测试并提交文件 竞赛技术总结1. 技术分析2. 数据方面模型方面3. AutoGluon4. 总结 实战kaggle比赛&#xff1a;树叶分类 kagg…...

vue中shift+alt+f格式化防止格式掉其它内容

好处就是使得提交记录干净&#xff0c;否则修改一两行代码&#xff0c;习惯性按了一下格式化快捷键&#xff0c;遍地飘红&#xff0c;下次找修改就费时间 1.点击设置图标-设置 2.点击这个转成配置文件 {"extensions.ignoreRecommendations": true,"[vue]":…...

WPS导出的PDF比较糊,和原始的不太一样,将带有SVG的文档输出为PDF

一、在WPS的PPT中 你直接输出PDF可能会导致一些问题&#xff08;比如照片比原来糊&#xff09;/ 或者你复制PPT中的图片到AI中类似的操作&#xff0c;得到的照片比原来糊&#xff0c;所以应该选择打印-->高级打印 然后再另存为PDF 最后再使用AI打开PDF文件再复制到你想用…...

Linux /etc/hosts文件

Linux的 /etc/hosts 文件用于静态地映射主机名到 IP 地址。 通常用于本地网络中的名称解析&#xff0c;它可以覆盖 DNS 的设置。当你访问一个域名时&#xff0c;系统会首先检查 /etc/hosts 文件&#xff0c;如果找到了匹配项&#xff0c;就会使用该 IP 地址&#xff0c;否则会…...

webpack学习-3.管理输出

webpack学习-3.管理输出 1.简单练手2.设置 HtmlWebpackPlugin3.清理 /dist 文件夹4.manifest5.总结 1.简单练手 官网的第一个预先准备&#xff0c;是多入口的。 const path require(path);module.exports {entry: {index: ./src/index.js,print: ./src/print.js,},output: …...

【Go语言反射reflect】

Go语言反射reflect 一、引入 先看官方Doc中Rob Pike给出的关于反射的定义&#xff1a; Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of …...

LC-1466. 重新规划路线(DFS、BFS)

1466. 重新规划路线 中等 n 座城市&#xff0c;从 0 到 n-1 编号&#xff0c;其间共有 n-1 条路线。因此&#xff0c;要想在两座不同城市之间旅行只有唯一一条路线可供选择&#xff08;路线网形成一颗树&#xff09;。去年&#xff0c;交通运输部决定重新规划路线&#xff0c…...

自动数据增广论文笔记 | AutoAugment: Learning Augmentation Strategies from Data

谷歌大脑出品 paper: https://arxiv.org/abs/1805.09501 这里是个论文的阅读心得&#xff0c;笔记&#xff0c;不等同论文全部内容 文章目录 一、摘要1.1 翻译1.2 笔记 二、(第3部分)自动增强:直接在感兴趣的数据集上搜索最佳增强策略2.1 翻译2.2 笔记 三、跳出论文&#xff0c…...

CTF 7

信息收集 存活主机探测 arp-scan -l 端口探测 nmap -sT --min-rate 10000 -p- 192.168.0.5 服务版本等信息 nmap -sT -sV -sC -O -p22,80,137,138,139,901,5900,8080,10000 192.168.0.5Starting Nmap 7.94 ( https://nmap.org ) at 2023-11-02 21:23 CST Stats: 0:01:30 elaps…...

无公网IP环境Windows系统使用VNC远程连接Deepin桌面

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…...

java--枚举

1.枚举 枚举是一种特殊类 2.枚举类的格式 注意&#xff1a; ①枚举类中的第一行&#xff0c;只能写一些合法的标识符(名称)&#xff0c;多个名称用逗号隔开。 ②这些名称&#xff0c;本质是常量&#xff0c;每个常量都会记住枚举类的一个对象。 3.枚举类的特点 ①枚举类的…...

JVM垃圾回收机制GC

一句话介绍GC&#xff1a; 自动释放不再使用的内存 一、判断对象是否能回收 思路一&#xff1a;引用计数 给这个对象里安排一个计数器&#xff0c; 每次有引用指向它&#xff0c; 就把计数器1&#xff0c; 每次引用被销毁&#xff0c;计数器-1&#xff0c;当计数器为0的时候…...

详解JAVA中的@ApiModel和@ApiModelProperty注解

目录 前言1. ApiModel注解2. ApiModelProperty注解3. 实战 前言 在Java中&#xff0c;ApiModel和ApiModelProperty是Swagger框架&#xff08;用于API文档的工具&#xff09;提供的注解&#xff0c;用于增强API文档的生成和展示。这两者搭配使用更佳 使用两者注解&#xff0c;…...

TiDB专题---2、TiDB整体架构和应用场景

上个章节我们讲解了TiDB的发展和特性&#xff0c;这节我们讲下TiDB具体的架构和应用场景。首先我们回顾下TiDB的优势。 TiDB的优势 与传统的单机数据库相比&#xff0c;TiDB 具有以下优势&#xff1a; 纯分布式架构&#xff0c;拥有良好的扩展性&#xff0c;支持弹性的扩缩容…...

性能调优入门

从公众号转载&#xff0c;关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、性能定律和数理基础 1.三个定律法则 (1)帕累托法则 我它也被称为 80/20 法则、关键少数法则&#xff0c;或者八二法则。人们在生活中发现很多…...

JavaWeb | 验证码 、 文件的“上传”与“下载”

目录&#xff1a; 验证码 和 文件的“上传”与“下载”1.验证码1.1在JSP上开发验证码 2.“文件上传” 和 “文件下载”2.1“文件上传 ”2.2“文件下载” 验证码 和 文件的“上传”与“下载” 1.验证码 验证码&#xff1a;就是由服务器生成的一串随机数字或符号形成一幅图片&am…...

服务器感染了.halo勒索病毒,如何确保数据文件完整恢复?

导言&#xff1a; 随着科技的不断发展&#xff0c;网络安全问题日益突出&#xff0c;而.halo勒索病毒正是这个数字时代的一大威胁。本文将深入介绍.halo勒索病毒的特点&#xff0c;解释在受到攻击后如何有效恢复被加密的数据文件&#xff0c;并提供一些建议以预防未来可能的威…...

docker安装elasticsearch8.5.0和kibana

服务器环境&#xff0c;centos7 一、安装elasticsearch 1. 创建一个es和kibana通用的网络 docker network create es-net 2. 拉取es镜像&#xff0c;这里选择8.5.0版本 docker pull elasticsearch:8.5.03. 创建挂载目录&#xff0c;并授权 mkdir /usr/local/install/ela…...

如何使用内网穿透工具实现公网访问GeoServe Web管理界面

文章目录 前言1.安装GeoServer2. windows 安装 cpolar3. 创建公网访问地址4. 公网访问Geo Servcer服务5. 固定公网HTTP地址6. 结语 前言 GeoServer是OGC Web服务器规范的J2EE实现&#xff0c;利用GeoServer可以方便地发布地图数据&#xff0c;允许用户对要素数据进行更新、删除…...

koa2项目中封装log4js日志输出

1.日志输出到控制台 npm i log4js -D 封装log4js文件&#xff1a; 注意&#xff1a;每次都要重新获取log4js.getLogger(debug)级别才能生效 const log4js require("log4js");const levels {trace: log4js.levels.TRACE,debug: log4js.levels.DEBUG,info: log4js.…...

C# WPF上位机开发(抽奖软件)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 每到年末或者是尾牙的时候&#xff0c;很多公司都会办一些年终的清楚活动&#xff0c;感谢员工过去一年辛苦的付出。这个时候&#xff0c;作为年会…...

搭建部署Hadoop2.x和3.x的区别

文章目录 2.x 和 3.x 的区别Java最小支持版本常用的端口号配置文件Classpath隔离NodeManager重连 进入官网自行查阅 2.x 和 3.x 的区别 Java最小支持版本 Hadoop 2.x&#xff1a;2.7 版本需要 Java 7&#xff0c;2.6 以及更早期版本支持 Java 6Hadoop 3.x&#xff1a;最低要求…...

Java爬虫攻略:应对JavaScript登录表单

问题背景 在进行网络抓取数据时&#xff0c;经常会遇到需要登录的网站&#xff0c;特别是使用JavaScript动态生成登录表单的情况。传统的爬虫工具可能无法直接处理这种情况&#xff0c;因此需要一种能够模拟用户行为登录的情况解决方案。 在实际项目中&#xff0c;我们可能需要…...