Appearance
使用Vite和TypeScript带你从零打造一个属于自己的Vue3组件库
笔者之前就经常有这样的痛点,在某个项目里二次封装了 el-select ,实现 filterable 的时搜索输入框移到下拉列表中,避免多选时多个tag挤压了搜索框的空间。当时是写在一个项目里,然后其他项目也遇到了这样的需求...我在十几个项目里面寻找、回忆,找回当年封装的组件,人都麻了......
大多B端系统都是以 element 、 antd 等ui框架为主,基于各种业务场景,基本都会有自己团队的二次封装。其实类似的功能扩展肯定会有的,如果有组件库把组件都集中起来,就能减少很多重复造轮子的劳动力了!
文章介绍了使用 Vite 和 TypeScript 从零打造 Vue3 组件库“xy-ui”的全过程,包括搭建 Monorepo 环境、使用 pnpm 配置和安装依赖、搭建 Vue3 脚手架项目、本地调试、开发组件、vite 打包(含样式处理)、发布等步骤,还提供了直接使用和后续相关计划的说明。
亮点
- Vite+Vitest+Vitepress 工具链 (项目构建+测试+项目文档)
- monorepo 分包管理
Monorepo环境
Monorepo 就是指在一个大的项目仓库中,管理多个模块/包(package),这种类型的项目大都在项目根目录下有一个packages文件夹,分多个项目管理;简单来说就是单仓库 多项目。
目前很多我们熟知的项目都是采用这种模式,如Vant,ElementUI,Vue3等。打造一个Monorepo环境的工具有很多,如:lerna、pnpm、yarn等,这里我们将使用pnpm来开发我们的UI组件库。
为什么要使用pnpm?
因为它简单高效,它没有太多杂乱的配置,它相比于lerna操作起来方便太多,pnpm 是 performant npm(高性能的 npm),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepo,简化开发者在多包组件开发下的复杂度和开发流程。
pnpm monorepo 搭建
1. 安装 pnpm:
bash
$npm install -g pnpm
新建文件夹作为工作区 ,例如我这里新建文件夹 xy-ui
2. 初始化package.json: cd 到目录下,初始化环境:
bash
# 初始化 - 文件夹下生成了 package.json
$pnpm init
3. 新建配置文件 .npmrc:
ini
shamefully-hoist = true
这里简单说下为什么要配置shamefully-hoist。 如果某些工具仅在根目录的node_modules时才有效,可以将其设置为true来提升那些不在根目录的node_modules,就是将你安装的依赖包的依赖包的依赖包的...都放到同一级别(扁平化)。说白了就是不设置为true有些包就有可能会出问题。
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持,你只需要创建一个 workspace
就可将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。
pnpm版本在 9.0 之后 pnpm,修改了 link-workspace-packages
的默认值为 false。我们需要开启该属性,在安装依赖时优先在本地链接,而不是从 registry(远程) 中下载。
在根目录新建.npmrc并配置以下内容:
ini
link-workspace-packages = true
```
**4. monorepo的实现:**
接下就是pnpm如何实现monorepo的了。
为了我们各个项目之间能够互相引用我们要新建一个`pnpm-workspace.yaml`文件将我们的包关联起来
我们在根目录下新建 `packages` 文件夹,再新建 `pnpm-workspace.yaml` 文件,用来声明对应的工作区,写入如下内容:
```yaml
packages:
# 存放组件库和其他工具库
- 'packages/*'
# 存放组件测试的代码
- 'play'
# 存放文档目录
- 'docs'
```
这样就能将我们项目下的`packages`目录和`play`目录关联起来了,当然如果你想关联更多目录你只需要往里面添加即可。根据上面的目录结构很显然你在根目录下新packages和play文件夹,packages文件夹存放我们开发的包,play用来调试我们的组件。
play文件夹就是接下来我们要使用vite搭建一个基本的Vue3脚手架项目的地方
**5. 安装对应依赖:**
我们开发环境中的依赖一般全部安装在整个项目根目录下,方便下面我们每个包都可以引用,所以在安装的时候需要加个 `-w`(先安装一下相关的依赖最为全局依赖,这样我们工作区域内的包就无需重复安装)
```bash
# 1.@vitejs/plugin-vue用来支持.vue文件的转译
# 2.unplugin-vue-define-options 可以在编写组件的时候 通过 defineOptions 方法为组件设置 name
# 3.vue-tsc 是一个专为 Vue.js 设计的 TypeScript 编译器包装器,它基于 TypeScript 的官方命令行接口(tsc)构建,但添加了对 Vue 单文件组件(SFCs)的支持。该工具允许开发者在使用 Vue 时享受 TypeScript 的静态类型检查优势,同时简化编译过程
# 4.@vitejs/plugin-vue-jsx是Vite官方提供的JSX支持插件,其内部使用了Vue官方提供的@vue/babel-plugin-jsx插件。
$pnpm i vue vite typescript vue-tsc unplugin-vue-define-options @vitejs/plugin-vue @vitejs/plugin-vue-jsx sass @types/node -D -w
# $pnpm i vue typescript sass @vitejs/plugin-vue vite vue-tsc -D -w
```
- `unplugin-vue-define-options` 可以在编写组件的时候 通过 defineOptions 方法为组件设置 name,使用示例如下:
```typescript
defineOptions({
name: "PlayButton"
})
```
vite.config.js 配置:
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import DefineOptions from 'unplugin-vue-define-options/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), DefineOptions()]
})
```
- `vite-plugin-vue-setup-extend`可以`<script name="SButton" setup>` 这样的形式
vite.config.js 配置:
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [vue(),VueSetupExtend()]
})
```
利用安装的插件,直接于script 标签上定义 name 属性。
- `@vitejs/plugin-vue-jsx`
- vite.config.js中引用插件:
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
})
```
- 使用:
```tsx
// App.vue
<script setup lang="tsx">
const JsxComp = (<>
<div className="flex-center mt-20px">
<span>Render By Jsx</span>
</div>
</>
)
</script>
<template>
<JsxComp />
</template>
```
- `unplugin-vue-macros`
unplugin-vue-macros/macros-global 是一个在 Vue 项目中使用的宏插件,它允许你在全局范围内定义宏。这些宏可以是常量、函数或其他可以在你的项目代码的任何位置直接使用的实用工具。
要使用 unplugin-vue-macros/macros-global,你需要先安装 unplugin-vue-macros 插件,然后在你的 Vue 项目中配置它。`npm install unplugin-vue-macros --save-dev`
- 可以新增调试的命令,一般启动项目可以使用 dev:项目名 来进行分别启动项目,后面跟上需要启动的路径即可,在根项目的 package.json里设置如下:
```json
{
"scripts": {
"dev:vue-demo1": "vite packages/vue-demo1",
}
}
```
- 仓库项目内的包相互调用:
如何对hooks、utils和components这三个包进行互相调用呢?我们只需要把这三个包都安装到仓库根目录下的 node_modules 目录中即可。所有的依赖都在根目录下安装,安装到根目录需要加上-w ,表示安装到公共模块的 packages.json 中。
```sh
# 全局安装依赖项 添加全局的依赖项的时候,需要在命令后面加上 -W
$pnpm install xykfz-ui -w
$pnpm install @xykfz-ui/components -w
$pnpm install @xykfz-ui/hooks -w
$pnpm install @xykfz-ui/utils -w
# 全局安装依赖项 比如所有的组件都需要使用到 lodash,就可以执行:
$pnpm i lodash -W
# 局部安装依赖项
进入到指定目录去安装:npm i lodash
或 使用 --filter 修饰符可以实现在根目录指定某个目录进行安装,具体命令为:
$pnpm i lodash --filter vue-demo1
```
以components包的package.json举例,其中"@uv-ui/hooks": "workspace:^1.0.0"和 "@uv-ui/utils": "workspace:^1.0.0"是依赖于其他目录的包,如果这个components包需要用到这两个包,需要在这个目录下也安装上,之后发布npm包的时候将workspace:^去掉即可,这两个依赖的包也需要独立发布npm包,这些在发布组件中有详细描述。
因为我们开发的是vue3组件, 所以需要安装vue3,当然ts肯定是必不可少的(当然如果你想要js开发也是可以的,甚至可以省略到很多配置和写法。但是ts可以为我们组件加上类型,并且使我们的组件有代码提示功能,未来ts也将成为主流);sass为了我们写样式方便。
**6. 配置tsconfig.json:**
```bash
# 生成tsconfig.json
$npx tsc --init
```
tsconfig.json:
```json
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"strict": true,
"target": "ES2015",
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"moduleResolution": "Node",
"lib": ["esnext", "dom"]
}
}
```
**7. 使用vite搭建一个基本的Vue3脚手架项目play:**
```bash
# 在根目录下执行
$pnpm create vite play 或 npm init vite play
```
## 开发
一般`packages`要有`utils`包来存放我们公共方法,工具函数等.
`packages`下的目录结构:
```sh
- core #npm包入口
"name": ""
- components #组件目录
"name": "@xykfz-ui/components"
- hooks #组合式API hooks目录
"name": "@xykfz-ui/hooks"
- theme #主题目录
"name": "@xykfz-ui/theme"
- utils #工具函数目录
"name": "@xykfz-ui/utils"
```
1. utils包
既然它是一个包,所以我们新建utils目录后就需要初始化它,让它变成一个包;终端进入`utils`文件夹执行:`pnpm init` 然后会生成一个package.json文件;这里需要改一下包名,我这里将name改成@xy-ui/utils表示这个utils包是属于xy-ui这个组织下的。所以记住发布之前要登录npm新建一个组织;例如xy-ui
```json
{
"name": "@xykfz-ui/utils",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
```
因为我们使用`ts`写的,所以需要将入口文件`index.js`改为`index.ts`,并新建`index.ts`文件:(先导出一个简单的加法函数)
2. 组件库包(这里命名为@xykfz-ui/components)
`components`是我们用来存放各种UI组件的包
新建`components`文件夹并执行 `pnpm init` 生成`package.json`并修改 package.json 文件的包名 name 和入口文件main。
```json
{
"name": "@xykfz-ui/components",
"version": "0.0.1",
"description": "",
"main": "index.ts",
"peerDependencies": {
"vue": "^3.5.12"
},
"keywords": [],
"author": "",
"license": "MIT"
}
```
以 button 组件为例,创建如下目录:
```js
packages/components
├─ button
├─ src
└─ button.vue // 组件代码
└─ index.ts // 用于导出button组件
└─ index.ts // 集中导出src下的所有组件
```
我们来写一下 button 组件,并默认导出组件:
```js
//button.vue
<template>
<button>按钮组件</button>
</template>
<script lang='ts' setup>
defineOptions({
name: 'XyButton'
})
</script>
// button/index.ts
import XyButton from './src/button.vue'
export {
XyButton
}
// index.ts
export * from './button'
```
3. 包之间本地调试
进入components文件夹执行 `$pnpm install @xy-ui/utils -w`,你会发现pnpm会自动创建个软链接直接指向我们的utils包
4. 测试组件 - `pnpm create vite play`快速创建一个 vite + vue + ts的项目,并且删除掉tsconfig.json 和 tsconfig.node.json
```json
{
"name": "@xykfz-ui/play",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
},
"devDependencies": {
}
}
```
5. 接着我们通过 -w 将我们的私有包安装到根目录下:`pnpm i @xykfz-ui/components -w`
发现根目录的 `package.json`的依赖中多了 "@xykfz-ui/components": "workspace:^" ,其中的 `workspace` 代表安装的是工作区的依赖包,因为在执行pnpm安装的时候,pnpm会优先在工作区的目录下找对应的依赖,如果找到了则会建立对应的软连接。
我们在 `play` 目录新建的项目中的 `App.vue` 来引入对应组件:
```js
<script setup lang="ts">
import { XyButton } from '@xykfz-ui/components'
</script>
<template>
<xy-button></xy-button>
</template>
```
运行效果如下:
使用之前在根项目下的package.json里配置的dev:play,执行`pnpm run dev:play`运行 play项目,出现按钮效果,这样一个简单的组件就完成了!
6. 全局引入组件库
假设我们需要全局使用组件库的话怎么办?
我们在组件包里就需要提供一个install的方法来全部引入。
```js
// packages/components/index.ts
import { App } from 'vue'
import { XyButton } from './button'
const components = [
XyButton
]
//作用:全局引入
export default {
install: (app: App) => {
for (const c of components) {
app.component(c.name, c)
}
}
}
//作用:局部引入
export * from './button'
```
在 `play` 目录的项目中的 `main.ts` 中全局引入,`app`页面中不单独引入按钮组件:
```js
// main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import xUi from '@xykfz-ui/components' // 新增
const app = createApp(App)
app.use(xUi) // 新增
app.mount('#app')
// App.vue
<script setup lang="ts">
</script>
<template>
<xy-button></xy-button>
</template>
```
7. 组件打包
我们使用`vite`来打包,首先在 `packages/components` 目录下创建个vite的配置文件 `vite.config.ts` ,如下
```js
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
build: {
rollupOptions: {
external: ["vue"], // 忽略打包vue文件
output: [
{
format: 'es',
entryFileNames: '[name].js',
dir: './dist',
preserveModulesRoot: 'src'
}
]
},
lib: {
entry: "./index.ts"
},
},
plugins: [vue()],
})
```
打包我们的入口文件 `index.ts`
在 `package.json` 文件中增加脚本执行命令:
```js
// package.json
"scripts": {
"build": "vite build"
}
```
执行命令 `pnpm build`,可以发现多了一个`dist`的目录,里面有一个 `index.js` 的文件,现在我们来测试一下打包的文件是否有效
我们来更改当前包的入口文件为打包后的文件如下:
```js
// package.json
"main": "dist/index.js"
```
然后看看 play 目录下运行的项目是否正常
8. npm 发布
npm 发布就很简单,切换到`packages/components`目录,配置需要发布到npm上的包的内容,增加如下配置,只发布 `dist` 里的内容:
```js
// package.json
"files": [
"dist"
],
```
其中`package.json`中的 `name` 就是代表的包名,即你 pnpm install [name] 的 name。
```js
npm login
npm publish
```
执行以上命令,npm login 先获取npm账号的认证,然后就可以发布!
9. ddd
10. ddd
11. 配置文件 - 在根目录创建一些必要额配置文件,比如刚才删除play中的ts配置,我们在根目录配置