Vue3 组件库搭建指北:pnpm + monorepo 环境配置
作为一个有追求的前端开发者,拥有一套自己的组件库不仅是技术实力的证明,更是提升团队效率的利器。本文将手把手带你使用最新的技术栈(Vue3 + Vite + TypeScript + pnpm Monorepo)从零搭建一个企业级组件库的基础架构。
1. 什么是 Monorepo?(从“找工具”说起)
在正式动手前,我们要先理解 Monorepo(单代码仓库)。
想象一下你正在装修房子:
- Multirepo(多仓库):你把锤子放在卧室,锯子放在厨房,螺丝钉放在地下室。每次你想钉个架子,得在三个房间之间来回跑。如果锤子升级了,你可能还得去其他房间检查锯子还能不能配合。
- Monorepo(单仓库):你准备了一个巨大的专业工具箱。锤子、锯子、螺丝钉全都整齐地摆在不同的隔层里。
为什么组件库一定要用 Monorepo?
- “近水楼台先得月” (代码共享):
你的组件代码在
packages/components,文档代码在docs。在 Monorepo 里,文档可以直接“看到”并使用最新的组件,不需要像传统方式那样——先给组件发个 NPM 包,再在文档里下载。 - “一人得道,鸡犬升天” (统一规范): 你只需要在根目录放一个 ESLint 配置文件,整个工具箱里的所有代码都会乖乖听话,保持一样的缩进和风格。
- “一损俱损,一荣俱荣” (依赖一致性): 如果你想升级 Vue 版本,在 Monorepo 里只需要改一处,所有相关的演示项目、文档、组件包都会同步升级,不会出现“文档用 Vue3.2,组件用 Vue3.5”导致的诡异报错。
核心成员介绍
在我们的项目中,pnpm 是管理这个巨大工具箱的“管家”。通过 pnpm-workspace.yaml,我们划分了不同的区域:
packages/*:这里是核心,存放组件库、工具函数、主题样式。play:这是我们的“沙盒”,用来一边写组件一边预览效果。docs:这是向外界展示组件库的“门面”。
2. 环境初始化
首先,确保你的 Node.js 版本 >= 18,并全局安装 pnpm:
npm install -g pnpm
初始化项目结构:
mkdir my-antd-ui
cd my-antd-ui
pnpm init
新建 pnpm-workspace.yaml,告诉 pnpm 这是一个 workspace 项目:
packages:
- 'packages/*'
- 'play'
- 'docs'
此时的目录结构应该如下:
my-antd-ui/
├── packages/ # 存放核心代码 (components, theme, utils)
├── play/ # 本地调试项目 (Playground)
├── docs/ # 文档站点 (VitePress)
├── package.json
└── pnpm-workspace.yaml
3. 依赖管理入门:安装外部包
在 Monorepo 中,安装依赖是有讲究的。首先我们来看最基础的“进货”——从 npm 官方仓库下载包。
3.1 公共依赖 (Root Dependencies)
场景:就像给整个房子装中央空调,所有房间都能享受到。
例子:TypeScript, ESLint, Prettier。这些开发工具在所有子包里都需要用到。
命令:使用 -w (workspace-root) 参数。
# -w 表示“装到根目录”,-D 表示开发依赖
pnpm add -w -D typescript eslint prettier
3.2 局部依赖 (Package Dependencies)
场景:就像给厨房买个烤箱,卧室并不需要它。
例子:packages/components 需要 vue,但 packages/utils 可能只需要 lodash。
命令:使用 --filter 参数,指定安装到哪个子包,千万不要加 -w。
# 给 components 包安装 vue
# 注意:@my-antd-ui/components 是该包 package.json 中的 name
pnpm add vue --filter @my-antd-ui/components
4. 依赖管理进阶:内部互联与工作区机制
Monorepo 的精髓在于“本地包互相引用”。比如我们的演示项目 play 需要直接使用 components 里的组件,而不需要去 npm 下载。这一章我们将深入探讨这一机制。
4.1 核心解密:pnpm-workspace.yaml 的作用
你可能注意到了根目录下的 pnpm-workspace.yaml 文件,它是整个工作区的**“地图”**。
packages:
- 'packages/*'
- play
- docs
它的作用至关重要:
- 画圈:告诉 pnpm,“在这个圈子里的文件夹(如 play, packages/*),都是一家人”。
- 寻址:当你在项目里引用
@my-antd-ui/components时,pnpm 会先看这张地图。如果发现目标在圈子里,它就不会去外网下载,而是直接指向本地目录。
如果没有这个文件:
pnpm 就会“失忆”,把 play 和 components 当成两个完全陌生的路人项目,导致无法进行本地联调。
4.2 实战演练:Play 项目引用本地组件
让我们来模拟一个真实场景:一边写组件,一边在 Play 项目里看效果。
我们需要把 packages/components 安装到 play 中:
# 语义:“给 play 安装 components,且强制从本地工作区获取”
pnpm add @my-antd-ui/components --filter @my-antd-ui/play --workspace
执行成功后,play/package.json 会出现:
"dependencies": {
"@my-antd-ui/components": "workspace:*"
}
4.3 幕后原理:软链接 (Symlink)
这里的“安装”并不是把代码复制过去,而是建立了一个快捷方式。
pnpm 会在 play/node_modules/@my-antd-ui/components 创建一个软链接,直接指向你的 packages/components 目录。
这意味着:
你在 packages/components 里修改了 Button 的颜色,play 项目因为引用的是同一个文件夹,刷新页面就能立刻看到变化。零时差,无需重新构建,无需发包。
4.4 避坑指南:一张表看懂 -w 和 --workspace
这两个参数长得很像,是初学者最容易混淆的地方。
| 参数 | 英文全称 | 核心作用 | 决定维度 | 常见搭配 | 例子 |
|---|---|---|---|---|---|
-w | --workspace-root | 装到根目录 | 目标位置 (Go To) | -D (开发依赖) | 安装全局工具 ESLint, TS |
--workspace | --workspace | 只从本地找 | 依赖来源 (Come From) | --filter (指定子包) | play 引用 components |
口诀:
- 要往根目录装工具?用
-w。 - 要连本地的兄弟包?用
--workspace。
5. TypeScript 配置
在根目录创建 tsconfig.json,作为所有子项目的基准配置:
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"skipLibCheck": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
}
}
6. 规范体系 (Lint & Format)
我们可以一步到位,使用 @antfu/eslint-config,它集成了 ESLint 和 Prettier 的最佳实践。
pnpm add -D -w eslint prettier @antfu/eslint-config typescript
新建 eslint.config.js:
import antfu from '@antfu/eslint-config'
export default antfu({
vue: true,
typescript: true,
ignores: ['**/dist', '**/node_modules']
})
7. 代码提交规范 (Husky + Commitlint)
为了防止像 fix: bug 这样随意的提交信息,我们需要引入 Commitlint。
pnpm add -D -w husky lint-staged @commitlint/cli @commitlint/config-conventional
npx husky init
在 .husky/commit-msg 中添加钩子:
npx --no -- commitlint --edit $1
新建 commitlint.config.js:
export default {
extends: ['@commitlint/config-conventional']
}
现在,如果你尝试提交 git commit -m "update",将会被拒绝;必须使用 git commit -m "feat: add button component" 这样符合规范的格式。
8. 样式架构设计:理解 BEM 规范
在编写组件库样式时,最头疼的就是样式冲突。如果大家都在 CSS 里写 .item,那全局样式就会乱成一团。为了解决这个问题,主流组件库(如 Element Plus)都采用了 BEM 命名规范。
什么是 BEM?
BEM 将类名拆解为三个部分:
- Block (块):组件的根节点。例如
my-button。 - Element (元素):组件内部的子节点。用双下划线
__连接。例如my-button__icon。 - Modifier (修饰符):组件的不同状态或外观。用双连字符
--连接。例如my-button--primary或my-button--disabled。
通俗例子: 想象一个“人”组件(Person):
person(Block)person__hand(Element: 人的手)person--female(Modifier: 女性的人)
为什么要这么写?
- 语义清晰:一眼就能看出这个类名是属于哪个组件的哪个部分。
- 避免冲突:每个组件都有独一无二的前缀(Namespace),样式不会互相污染。
- 性能友好:减少了 CSS 选择器的嵌套深度(尽量保持一级类名选择器)。
自动化实现:useNamespace
在 packages/utils/src/namespace.ts 中,我们封装了一个工具函数,让类名的生成变得半自动化:
export const useNamespace = (block: string) => {
const namespace = 'my' // 你的组件库前缀
const b = () => `${namespace}-${block}` // 生成 my-button
const e = (el: string) => el ? `${b()}__${el}` : '' // 生成 my-button__icon
const m = (mod: string) => mod ? `${b()}--${mod}` : '' // 生成 my-button--primary
return { b, e, m }
}
在 Vue 组件中使用:
<template>
<!-- 最终生成 class="my-button my-button--primary" -->
<button :class="[ns.b(), ns.m(type)]">
<!-- 最终生成 class="my-button__content" -->
<span :class="ns.e('content')">
<slot />
</span>
</button>
</template>
<script setup>
const ns = useNamespace('button')
</script>
9. 结语
至此,我们的组件库地基已经打牢。我们配置了高效的 Monorepo 环境,统一了代码规范,并设计了样式架构。接下来,我们将逐步实现组件库的核心功能,并编写文档和示例项目。
敬请期待后续更新!