Vue3 + TS 搭建组件库
开始
在编写组件库之前,我们首先要对整个代码项目的解构有一个清晰的划分,以及用到的大多数规范,和代码风格有一个约定,这篇文章主要就围绕着下面图中的几个问题展开描述一下。
1、搭建 monorepo 环境
我们使用pnpm
当做包管理工具,用pnpm workspace
来实现monorepo
。可以看下面参考文章里面的介绍,结合官网有个基础的了解。下面我们正式开始搭建。
新建一个文件夹z-vue3-ui
npm install pnpm -g # 全局安装pnpm
pnpm init # 初始化package.json配置⽂件 私有库
pnpm install vue typescript -D # 全局下添加依赖
添加.npmrc
文件,
shamefully-hoist = true // 作用依赖包都扁平化的安装在node_modules下面
创建tsconfig.json
文件
{
"compilerOptions": {
"module": "ESNext", // 打包模块类型ESNext
"declaration": false, // 默认不要声明⽂件
"noImplicitAny": false, // ⽀持类型不标注可以默认any
"removeComments": true, // 删除注释
"moduleResolution": "node", // 按照node模块来解析
"esModuleInterop": true, // ⽀持es6,commonjs模块
"jsx": "preserve", // jsx 不转
"noLib": false, // 不处理类库
"target": "es6", // 遵循es6版本
"sourceMap": true,
"lib": [
// 编译时⽤的库
"ESNext",
"DOM"
],
"allowSyntheticDefaultImports": true, // 允许没有导出的模块中导⼊
"experimentalDecorators": true, // 装饰器语法
"forceConsistentCasingInFileNames": true, // 强制区分⼤⼩写
"resolveJsonModule": true, // 解析json模块
"strict": true, // 是否启动严格模式
"skipLibCheck": true, // 跳过类库检测
"types": ["unplugin-vue-define-options"] // sfc 添加 name属性的包需要的
},
"exclude": [
// 排除掉哪些类库
"node_modules",
"**/__tests__",
"dist/**"
]
}
在项目根目录下面创建pnpm-workspace.yaml
配置文件。
packages:
- "packages/**" # 存放所有组件
- docs # 文档
- play # 测试组件
2、创建组件测试环境
pnpm create vite play --template vue-ts
cd play
pnpm install
在根目录新建一个typings
目录,用来存放项目中通用的自定义的类型,然后把用vite
创建的play/src
下面的vite-env.d.ts
移动到typings
下面去。
启动测试项目, 在根目录下面的package.json
下面添加scripts
脚本。
"scripts": {
"dev": "pnpm -C play dev"
}
测试环境搭建完成,下面开始搭建packages
下面的文件目录了。
3、引入 scss,并式实现 Bem
先手动在根目录下面创建如下目录
packages
├─components # 存放所有的组件
├─utils # 存放⼯具⽅法
└─theme-chalk # 存放对应的样式
在执行下面的命令,在各自的根目录下面创建package.json
文件。
cd components && pnpm init
cd theme-chalk && pnpm init
cd utils && pnpm init
这个时候需要手动修改每个包的名字,让其属于z-vue3-ui
的子包,我们分别进行以下的修改,在对应package.json
文件中修改其name
属性的值。
@z-vue3-ui /components
@z-vue3-ui/theme-thalk
@z-vue3-ui/utils;
然后执行一下命令,将这三个包安装在根目录下面,注意名字哦。
pnpm i @z-vue3-ui/components -w
pnpm i @z-vue3-ui/theme-chalk -w
pnpm i @z-vue3-ui/utils -w
下面我们就开始实现Bem
规范了。
1. Bem Js 实现部分
先来实现在js
中创建class
的几个函数。
utils/create.ts
// block 代码块
// element 元素
// modifier 装饰
// z-button
// z-button__element--disable
/**
*
* @param prefixName 前缀名
* @param blockName 代码块名
* @param elementName 元素名
* @param modifierName 装饰符名
* @returns 说白了 ,就是提供一个函数,用来拼接三个字符串,并用不同的符号进行分隔开来
*/
function _bem(prefixName, blockName, elementName, modifierName) {
if (blockName) {
prefixName += `-${blockName}`;
}
if (elementName) {
prefixName += `__${elementName}`;
}
if (modifierName) {
prefixName += `--${modifierName}`;
}
return prefixName;
}
/**
*
* @param prefixName 前缀
* @returns
*/
function createBEM(prefixName: string) {
const b = (blockName?) => _bem(prefixName, blockName, "", "");
const e = (elementName) =>
elementName ? _bem(prefixName, "", elementName, "") : "";
const m = (modifierName) =>
modifierName ? _bem(prefixName, "", "", modifierName) : "";
const be = (blockName, elementName) =>
blockName && elementName
? _bem(prefixName, blockName, elementName, "")
: "";
const bm = (blockName, modifierName) =>
blockName && modifierName
? _bem(prefixName, blockName, "", modifierName)
: "";
const em = (elementName, modifierName) =>
elementName && modifierName
? _bem(prefixName, "", elementName, modifierName)
: "";
const bem = (blockName, elementName, modifierName) =>
blockName && elementName && modifierName
? _bem(prefixName, blockName, elementName, modifierName)
: "";
const is = (name, state?) => (state ? `is-${name}` : "");
return {
b,
e,
m,
be,
bm,
em,
bem,
is,
};
}
export function createNamespace(name: string) {
const prefixName = `z-${name}`;
return createBEM(prefixName);
}
下面我们找个地方,说一下上面的bem
怎么使用。因为现在我们的代码都是ems
的,在node
环境中跑起来不方便,所以就在play
测试的小模块中演示了。
const bem = createNamespace("icon");
console.log(bem.b());
console.log(bem.e("wrapper"));
console.log(bem.m("disabled"));
console.log(bem.is("checked", true));
console.log(bem.bem("box", "element", "disabled"));
2. Bem scss 部分
theme-chalk
├── package.json
└── src
├── icon.scss
├── index.scss
├── mixins
│ ├── config.scss
│ └── mixins.scss
config.scss
$namespace: "z";
$element-separator: "__"; // 元素连接符
$modifier-separator: "--"; // 修饰符连接符
$state-prefix: "is-"; // 状态连接符
* {
box-sizing: border-box;
}
mixins.scss
@use "config" as *;
@forward "config";
// z-icon
@mixin b($block) {
$B: $namespace + "-" + $block;
.#{$B} {
@content;
}
}
// z-icon.is-xxx
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
// .z-icon--primary
@mixin m($modifier) {
@at-root {
#{& + $modifier-separator + $modifier} {
@content;
}
}
}
// z-icon__header
@mixin e($element) {
@at-root {
#{& + $element-separator + $element} {
@content;
}
}
}
index.scss
@use "./icon.scss";
icon.scss
@use "./mixins/mixins.scss" as *;
@keyframes transform {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@include b(icon) {
width: 1em;
height: 1em;
line-height: 1em;
display: inline-flex;
vertical-align: middle;
svg.loading {
animation: transform 1s linear infinite;
}
}
4. 编写 Icon 组件
目录结构如下:
components
├── icon
│ ├── index.ts
│ └── src
│ ├── icon.ts
│ └── icon.vue
└── package.json
icon.vue
<template>
<i :class="bem.b()" :style="style">
<slot></slot>
</i>
</template>
<script lang="ts" setup>
import { computed, CSSProperties } from "vue";
import { createNamespace } from "../../../utils/create";
import { iconProps } from "./icon";
const bem = createNamespace("icon");
defineOptions({
name: "ZIcon",
});
const props = defineProps(iconProps);
const style = computed<CSSProperties>(() => {
if (!props.color && !props.size) {
return {};
}
return {
...(props.size ? { "font-size": props.size + "px" } : {}),
...(props.color ? { color: props.color } : {}),
};
});
</script>
icon.ts
import { ExtractPropTypes, PropType } from "vue";
export const iconProps = {
size: [Number, String] as PropType<number | string>,
color: String,
} as const;
export type IconProps = ExtractPropTypes<typeof iconProps>;
index.ts
import _Icon from "./src/icon.vue";
import { withInstall } from "@z-vue3-ui/utils/withInstall";
const Icon = withInstall(_Icon); // 生成带有 install 方法的组件
export default Icon; // 导出组件
export type { IconProps } from "./src/icon"; // 导出组件 props 的类型
// 这里为了给 volar 用的,具体可以看下面的文档
declare module "vue" {
export interface GlobalComponents {
ZIcon: typeof Icon;
}
}
编写一个方法用来把我们自己编写的组件包装成一个插件,方便后序导入使用,直接可以用Vue.use()
utils 下面的目录结构
utils
├── create.ts
├── package.json
└── withInstall.ts
import { Plugin } from "vue";
export type withInstallSFC<T> = T & Plugin;
// 给传入的组件添加一个 install 方法
export function withInstall<T>(comp: T) {
(comp as withInstallSFC<T>).install = function (app) {
const { name } = comp as unknown as { name: string };
app.component(name, comp); // 这一块的类型还有点问题,还在研究中。
};
return comp as withInstallSFC<T>;
}
play
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
│ └── vite.svg
├── src
│ ├── App.vue
│ ├── assets
│ └── main.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
并且在main.ts
中引入样式文件,并安装sass
包
mian.ts
import { createApp } from "vue";
import "@z-vue3-ui/theme-chalk/src/index.scss";
import App from "./App.vue";
createApp(App).mount("#app");
我们的 icon 内容并不由本库提供,需要安装另一个库,这个组件只是将其进行了整合
pnpm add @vicons/ionicons5 -w
App.vue
<script setup lang="ts">
import ZIcon from "@z-vue3-ui/components/icon";
import { AccessibilityOutline } from "@vicons/ionicons5";
</script>
<template>
<div>
<ZIcon>
<AccessibilityOutline></AccessibilityOutline>
</ZIcon>
</div>
</template>
不出意外的话,现在已经可以看见下面的 icon 组建了
还有更详细的关于BEM
和Element
实现主题的文章请参考下面这一篇,ElementUI 组件库样式与自动化设计。
5、Eslint 配置
npx eslint --init
检验语法并提示错误行数
使用 js-module
项目采用语法
是否使用 ts
代码跑在哪里
这里需要我们手动使用pnpm
进行包的安装
pnpm i eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest -D -w
pnpm i @vue/eslint-config-typescript -D -w
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended", // vue3解析 https://eslint.vuejs.org/
"plugin:@typescript-eslint/recommended",
"@vue/typescript/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multi-word-component-names": "off",
"vue/prefer-import-from-vue": "off",
},
globals: {
defineOptions: "readonly",
},
};
6、Prettier 配置
安装插件,并添加给 vscode 添加配置文件
.prettierrc.js
// 此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写
module.exports = {
printWidth: 80, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: true, // 句末使用分号
singleQuote: true, // 使用单引号
quoteProps: "as-needed", // 仅在必需时为对象的key添加引号
jsxSingleQuote: true, // jsx中使用单引号
trailingComma: "all", // 多行时尽可能打印尾随逗号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: "always", // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
requirePragma: false, // 无需顶部注释即可格式化
insertPragma: false, // 在已被preitter格式化的文件顶部加上标注
proseWrap: "preserve", // 不知道怎么翻译
htmlWhitespaceSensitivity: "ignore", // 对HTML全局空白不敏感
vueIndentScriptAndStyle: false, // 不对vue中的script及style标签缩进
endOfLine: "lf", // 结束行形式
embeddedLanguageFormatting: "auto", // 对引用代码进行格式化
};
.prettierignore
node_modules
dist
编辑器配置文件
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
7、编辑器配置
.editorconfig
# http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
并安装EditorConfig for VS Code
插件即可
8、lint-staged 配置
git init
pnpm install mrm husky lint-staged -w -D
npx mrm lint-staged
强制执行常规提交的可共享commitlint
配置。与@commitlint/cli和@commitlint/prompt-cli 一起使用。
pnpm install @commitlint/cli @commitlint/config-conventional -D -w
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"build", // 编译相关的修改,例如发布版本、对项⽬构建或者依赖的改动
"chore", // 其他修改, ⽐如改变构建流程、或者增加依赖库、⼯具等
"ci", // 持续集成修改
"docs", // ⽂档修改
"feat", //新特性、新功能
"fix", // 修改 bug
"perf", // 优化相关,⽐如提升性能、体验
"refactor", // 代码重构
"revert", // 回滚到上⼀个版本
"style", // 代码格式修改
"test", // 测试⽤例修改
],
],
},
};
git commit -m"feat: 初始化⼯程"
9、Vitepress 编写组件文档
在根目录下面创建docs
文件夹,用来存放文档。
1. 安装 vitepress
cd docs
pnpm init
pnpm install vitepress -D # 在doc⽬录下安装
package.json
"scripts": {
"dev": "vitepress dev ."
},
然后在根目录下面的添加脚本
"scripts": {
"docs:dev": "pnpm -C docs dev",
},
2. 创建第一篇文章
---
layout: home
hero:
name: z-ui 组件库
text: 基于 Vue 3 的组件库.
tagline: 掌握 vue3 组件编写
actions:
- theme: brand
text: 快速开始
link: /guide/quickStart
features:
- icon: 🛠️
title: 组件库构建流程
details: Vue3 组件库构建...
- icon: ⚙️
title: 组件库单元测试
details: Vue3 组件库测试...
---
启动docs
目录
pnpm run docs:dev
下面我们就可以看见这个页面了
3. 文档配置文件
.vitepress/config.js
module.exports = {
title: "Z-UI",
description: "zi-shui UI",
themeConfig: {
lastUpdated: "最后更新时间",
docsDir: "docs",
editLinks: true,
editLinkText: "编辑此⽹站",
repo: "https://gitee.com/login",
footer: {
message: "Released under the MIT License.",
copyright: "Copyright © 2022-present Zi Shui",
},
nav: [
{ text: "指南", link: "/guide/installation", activeMatch: "/guide/" },
{ text: "组件", link: "/component/icon", activeMatch: "/component/" },
],
sidebar: {
"/guide/": [
{
text: "指南",
items: [
{ text: "安装", link: "/guide/installation" },
{ text: "快速开始", link: "/guide/quickStart" },
],
},
],
"/component/": [
{
text: "基础组件",
items: [{ text: "Icon", link: "/component/icon" }],
},
],
},
},
};
4. 主题配置
.vitepress/theme/index.ts
import DefaultTheme from "vitepress/theme";
import "@z-vue3-ui/theme-chalk/src/index.scss";
import ZIcon from "@z-vue3-ui/components/icon";
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.use(ZIcon); // 注册组件
},
};
添加vite.config.ts
让vite
也可以支持defineOptions
。
vite.config.ts
import { defineConfig } from "vite";
import DefineOptions from "unplugin-vue-define-options/vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [DefineOptions()],
});
5. 编写 Icon 组件文档
component/icon.md
# Icon 图标
z-ui 推荐使用 xicons 作为图标库。
$ pnpm install @vicons/ionicons5
## 使用图标
- 如果你想像用例一样直接使用,你需要全局注册组件,才能够直接在项目里使用。
<script setup lang="ts">
import { AccessibilityOutline, ArrowRedoOutline } from "@vicons/ionicons5";
const handleClick = () => {
alert(1);
};
</script>
<ZIcon color="#B1B2FF" size="40" @click="handleClick">
<AccessibilityOutline/>
</ZIcon>
<ZIcon color="#AAC4FF" size="40">
<AccessibilityOutline/>
</ZIcon>
<ZIcon color="#D2DAFF" size="40">
<AccessibilityOutline/>
</ZIcon>
<div>
<ZIcon color="#EBC7E8" size="60">
<ArrowRedoOutline/>
</ZIcon>
<ZIcon color="#645CAA" size="60">
<ArrowRedoOutline/>
</ZIcon>
<ZIcon color="#A084CA" size="60">
<ArrowRedoOutline/>
</ZIcon>
</div>
<script setup lang="ts">
import { CashOutline } from "@vicons/ionicons5";
</script>
<template>
<ZIcon color="red" size="40">
<CashOutline />
</ZIcon>
</template>
API
Icon Props
| 名称 | 类型 | 默认值 | 说明 |
| ----- | ---------------- | --------- | -------- |
| color | string | undefined | 图标颜色 |
| size | number \| string | undefined | 图片大小 |
10. 展望
现在已经写了四个组件了,希望可以坚持写更多的组件,从简单的开始,才能解决更复杂的问题,奥利给。
components
├── checkbox
├── icon
├── package.json
├── tree
└── virtual-list
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南