写在前面:在完成了核心组件的开发后,我们的组件库
my-antd-ui正式进入了最关键的阶段——工程化完善。这不仅是把代码传到网上那么简单,更是要建立一套标准、稳定、自动化的生产体系。为你深度拆解生产级组件库的打包构建流程。
🚀 发布前的四大核心命题
之前的开发都停留在“本地跑通”阶段。要成为一个真正的“企业级开源产品”,我们需要回答以下四个核心命题:
- 如何统一打包?(解决产物从源码向编译代码的进化)
- 如何建立包依赖?(解决内部包联调与第三方库冲突)
- 如何统一测试?(建立全自动的代码质量守卫)
- 如何发布?(实现规范的版本管理与自动化流水线)
本文(上篇)将围绕“统一打包、包依赖建立、统一测试”这三个核心命题,为你深度拆解生产级组件库的构建全流程。
一、如何统一打包?(效率篇)
在 Monorepo 这种复杂的环境下,我们不能手动进入每个文件夹打包。
1. 一键构建:Monorepo 的魔力
由于我们的项目是 Monorepo 结构,子包非常多。为了不用一个个进入子目录打包,我们在根目录配置了“一键构建”脚本:
// my-antd-ui/package.json
"scripts": {
"build": "pnpm -r --filter \"./packages/**\" run build"
}
pnpm -r(recursive):递归模式,告诉 pnpm 去所有的子包里跑命令。--filter "./packages/**":过滤器,精准锁定packages目录下的所有包。- 作用:只需在根目录运行一次
pnpm build,它就会自动帮我们将所有组件打包好。
2. package.json 的进化:从源码到产物
你可能会发现 packages/components/package.json 的入口配置发生了变化,这其实是它的“发布声明”:
{
"name": "@my-antd-ui/components",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"peerDependencies": {
"vue": "^3.0.0"
},
"dependencies": {
"ant-design-vue": "^4.2.6"
}
}
关键字段解析:
main&module:分别对应 CommonJS 和 ESM 两种规范。确保你的库既能在老项目中跑,也能在现代 Vite 项目中享受 Tree Shaking 的瘦身效果。types(或typings):指定了“补全说明书”的路径。没有它,用户写代码时就没有类型提示。exports:现代 Node.js 的标准。它能智能识别用户的引入方式(是import还是require),并分发最合适的文件。peerDependenciesvsdependencies:- 同辈依赖 (Peer):如
vue。我们不希望把 Vue 打包进去,而是要求用户自己安装,以保证全局只有一个 Vue 实例,避免响应式断开。 - 运行依赖 (Deps):如
ant-design-vue。这是库运行必须的,用户安装时会自动下载。
- 同辈依赖 (Peer):如
3. 库模式基础配置
与普通项目打包输出 HTML 不同,库模式的目标是产出 JS 文件。核心配置如下:
// packages/components/vite.config.ts
build: {
lib: {
// 1. 指定入口文件(通常是 index.ts)
entry: resolve(__dirname, 'index.ts'),
// 2. 库的全局变量名称(用于 UMD/CDN 引入)
name: 'MyAntdUiComponents',
// 3. 输出的文件名
fileName: 'index',
// 4. 导出的格式:ESM (现代) 和 CJS (Node.js)
formats: ['es', 'cjs']
}
}
4. 核心配置:external 与 globals
配置好库模式后,我们还需要处理依赖项,这是两个“保命配置”:
// packages/components/vite.config.ts
rollupOptions: {
// 1. 外部化依赖,不打包进产物中
external: [
'vue',
'@my-antd-ui/utils',
'@ant-design/icons-vue',
'ant-design-vue'
],
output: {
// 2. 在 UMD 模式下,为这些外部依赖提供全局变量映射
globals: {
vue: 'Vue',
'ant-design-vue': 'Antd'
}
}
}
- External (外部化):我们把
vue、ant-design-vue等设为外部依赖。- 原理:告诉打包工具:“这些东西用户家里肯定有,别塞进我的包里”。
- 后果:如果不配置,你的包里会含有一套 Vue 源码。当用户使用时,页面上会有两个 Vue 实例,直接导致响应式失效(数据变了页面没反应)。
- Globals (全局变量):为 UMD/CDN 引入提供“翻译表”。
- 背景:当用户不使用 Vite 或 Webpack,而是直接在 HTML 里通过
<script>标签引入你的库时,浏览器环境是没有模块系统的。 - 通俗解释:你的代码里写着
import { ref } from 'vue',但浏览器在全局环境里根本找不到一个叫vue的模块。它只认识全局变量,比如window.Vue。 - 作用:
globals: { vue: 'Vue' }这行配置就像是一张翻译表,告诉浏览器:“代码里凡是提到vue的地方,请直接去全局变量window.Vue里面取”。如果没有这个配置,浏览器会因为找不到vue而直接报错。
- 背景:当用户不使用 Vite 或 Webpack,而是直接在 HTML 里通过
5. 类型文件的“自动吐出”
我们集成了 vite-plugin-dts。如果没有它,用户在使用你的组件库时,IDE 里将没有任何代码提示。
// packages/components/vite.config.ts
dts({
// 1. 包含的文件范围:告诉插件哪些文件需要生成类型
include: ['src/**/*.ts', 'src/**/*.vue', 'index.ts'],
// 2. 类型文件输出目录:统一存放在 dist/types 下
outDir: 'dist/types'
})
include:就像是给插件画了个“生产圈”。它不仅处理.ts文件,还能自动提取.vue组件中<script setup>里的 Props 和 Emits 定义,非常智能。outDir:指定存放位置。配合package.json中的"types": "dist/types/index.d.ts",用户在写代码时,VSCode 就能顺着这个路径找到“补全说明书”。
Tips:这确保了用户在引入你的库时,能享受到 TypeScript 带来的精准类型检查和秒级代码补全。
4.1 实战中的“填坑”干货
在工程化过程中,我们总结了两个“保命配置”:
- External:在 Vite 打包时将 Vue 排除在外,防止重复打包导致应用崩溃。
- Global.d.ts:通过 TS 的声明合并(Module Augmentation),让用户在写代码时能享受到完美的自动补全提示。
总结:工程化不是把事情变复杂,而是为了让复杂的协作变得简单、标准、自动化。
关联文件:
- 打包配置:
packages/components/vite.config.ts- 全局提示:
packages/components/global.d.ts
6. 完整的构建配置文件示例
为了方便大家参考,这里贴出 packages/components/vite.config.ts 的完整代码:
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
// 获取当前文件的绝对路径,解决 ESM 环境下 __dirname 缺失的问题
const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [
// 处理 .vue 文件
vue(),
// 自动生成 .d.ts 类型定义文件
dts({
// 包含的文件范围
include: ['src/**/*.ts', 'src/**/*.vue', 'index.ts'],
// 类型文件输出目录
outDir: 'dist/types'
})
] as any, // 解决 monorepo 中多版本 vite 导致的插件类型冲突
build: {
// 库模式配置
lib: {
// 入口文件
entry: resolve(__dirname, 'index.ts'),
// 暴露的全局变量名称(用于 UMD 构建)
name: 'MyAntdUiComponents',
// 输出的文件名
fileName: 'index',
// 导出的格式
formats: ['es', 'cjs']
},
rollupOptions: {
// 外部化依赖,不打包进产物中,减小体积并避免多版本冲突
external: [
'vue',
'@my-antd-ui/utils',
'@ant-design/icons-vue',
'ant-design-vue'
],
output: {
// 在 UMD 模式下,为 these 外部依赖提供全局变量映射
globals: {
vue: 'Vue',
'ant-design-vue': 'Antd'
}
}
}
}
})
7. 开发者体验:Volar 提示补全
为了让用户在 VSCode 中像使用 Element Plus 一样丝滑,我们编写了 global.d.ts。
7.1. 为什么需要 global.d.ts?
在 Vue3 中,如果你全局注册了组件(例如通过 app.use(MyUI)),在 .vue 文件的模板中使用这些组件时,编辑器(VSCode)默认是没有任何提示的。这会导致开发者不知道有哪些 props 可以传,极大降低开发效率。
7.2. 原理解析与代码逐行拆解
我们通过扩展 Vue 的核心接口来解决这个问题:
// packages/components/global.d.ts
// 1. 必须先 import,定位到我们要修改的“Vue 总部大楼”
import '@vue/runtime-core'
// 2. 进入大楼,宣布我们要进行“二次装修”
declare module '@vue/runtime-core' {
// 3. 核心:扩展 Volar 插件专用的全局组件接口
export interface GlobalComponents {
// 4. 映射:[标签名]: (从已定义的组件中自动抓取类型)
MyButton: (typeof import('@my-antd-ui/components'))['MyButton']
MyInput: (typeof import('@my-antd-ui/components'))['MyInput']
MyIcon: (typeof import('@my-antd-ui/components'))['MyIcon']
MyRow: (typeof import('@my-antd-ui/components'))['MyRow']
MyCol: (typeof import('@my-antd-ui/components'))['MyCol']
MyVirtualList: (typeof import('@my-antd-ui/components'))['MyVirtualList']
}
}
// 5. 确保该文件被视为一个独立的模块,防止类型污染
export {}
逐行理解:
import '@vue/runtime-core':这是模块扩展 (Module Augmentation) 的前提。它告诉 TypeScript 我们要寻找并修改的是已存在的模块,而不是定义一个全新的空模块。declare module '@vue/runtime-core':这是“二次装修”的开始,通过该声明我们可以向已有的 Vue 核心类型中注入自定义的组件类型映射。GlobalComponents接口:这是 IDE 提示的“水源”。Volar 插件会实时监控这个接口,一旦你把组件名塞进去,它就能在模板中为你提供自动补全、属性校验和悬浮文档。typeof import(...):这是一种极其智能的写法。它不需要你手动维护类型,而是会自动同步你组件源码中的所有props、emits和slots定义。export {}:确保该文件被视为一个独立的模块,符合现代 TS 项目的标准规范。
7.3. 它是怎么生效的?(底层机制)
这套机制的背后依赖于 TypeScript 的两个核心能力:
- 声明合并 (Declaration Merging):通过
declare module,我们并没有替换 Vue 的类型,而是将我们的组件类型“追加”到了 Vue 原有的全局组件清单中。 - 自动扫描与识别:TypeScript 和 Volar 插件会自动扫描项目中的声明文件(.d.ts)。只要这个文件被包含在 TypeScript 的检测范围内,编辑器就能实时识别出模板中的全局标签。
二、如何建立包依赖?(架构篇)
组件库内部往往有很多关联,比如 components 依赖 utils。
内部联调:workspace 协议
在 packages/components/package.json 中:
"dependencies": {
"@my-antd-ui/utils": "workspace:*",
"ant-design-vue": "^4.2.6"
}
workspace:*:这是关键!它告诉 pnpm 优先找项目本地的utils包,而不是去 npm 下载。- PeerDependencies:我们将
vue设为同辈依赖。这确保了用户的项目里只有一份 Vue 实例,避免了响应式失效的致命问题。
三、如何统一测试?(质量篇)
我们要确保每一次发布的代码都是稳如泰山的。
自动化防线:Vitest + GitHub Actions
- 统一脚本:根目录配置
"test": "vitest",一键扫描全库所有组件的测试用例。 - CI 门禁:在
.github/workflows/ci.yml中,我们规定每当代码 Push 到 GitHub:- 自动运行
pnpm lint查错。 - 自动运行
pnpm test验证功能。 - 双端同步(镜像):为了让国内用户访问更快,我们还通过 Actions 自动将代码同步镜像到 Gitee。
- 自动运行
🏁 结语(上篇)
通过本文,我们完成了组件库工程化构建的三大支柱:统一打包、内部依赖关联以及自动化测试体系。
接下来,在《从本地到 NPM(下):版本管理与自动化发布指南》中,我们将深入探讨如何利用 Changesets 管理版本,并实现组件库在 NPM 的正式发布。