这一年我对组件的思考
原文写于 2020 年左右,主要讨论组件沉淀、组件库维护、文档索引和 Bit 的工作流。本文保留原文的工作背景、组件分类、问题分析、自研
comp脚手架和自动生成 README 的思路,同时删除招聘推广和抓取噪声,并补充 Vue 3.5、Vite、Storybook、pnpm workspace、Changesets、Vitest、Playwright 和 Nuxt 4 时代的实践。
一、从“把需求做完”到“让成果可复用”
这一年接触到的工作大致是这样的:需求经常并行,做完功能还需要整理文档;上次做过的需求要能够找到地址、实现步骤和组件位置;一个零散的实现如果有更好的统一方案,就需要跟进重构;重构上线前还要梳理影响范围、回归页面和发布计划;研究新技术还要形成文档和分享。
刚开始会觉得这些事情挤占了编码时间,但换个角度看,它们其实都在问同一件事:
一个组件或一段代码,除了今天能运行,能不能被团队理解、复用、测试、发布和持续维护?
这也是本文想讨论的主题。
二、Bit 给我的启发
原文曾把 Bit 初步理解成“前端垂直领域的 Git”。这个比喻不够准确,但它确实帮助很多团队思考另一种组件工作流:组件不一定只能作为某个业务项目里的一个文件存在,也可以拥有独立的版本、依赖、文档、示例、测试和发布记录。
Bit 更准确地说是围绕组件开发、版本管理、依赖分析、构建和共享的工具与平台。它不是 Git 的替代品,也不是引入后就能自动解决组件治理问题的魔法。团队仍然需要决定:
- 什么内容值得抽象为组件;
- 公共 API 如何设计;
- 组件如何测试和发布;
- 谁负责维护破坏性变更;
- 业务组件和基础组件如何划分;
- 如何让使用者发现并正确使用组件。
原文提出“用熟悉的方式逐步引入 Bit 的思想”,这个方向仍然有价值。现在不一定要直接采用 Bit,也可以用 monorepo、workspace、包发布和组件文档平台实现相同的核心能力。
三、团队里常见的组件形态
原文把团队中的组件大致分为以下几类,这个分类不是 Vue 官方分类,但很适合用来讨论维护成本。
3.1 大库型
类似 Element Plus、Ant Design Vue、Naive UI 的设计系统或组件库,提供 Button、Input、Table、Dialog 等通用能力。
优点是接口和视觉规范统一,修复可以惠及多个项目;缺点是治理、兼容、构建体积和文档成本都很高。
3.2 一次型
完全贴合某个需求的业务组件,项目结束后可能不再复用。一次型并不等于“写得随便”:它仍然应该有清楚的状态边界、可读的命名和必要的测试,只是不必为了不存在的复用场景设计复杂的公共 API。
3.3 高复用型
一看就可能被多个项目使用,例如视频播放器、上传器、复杂表格、富文本编辑器和地图包装组件。这类组件更适合拥有独立文档、示例、版本和测试。
3.4 二次封装型
把原生 JavaScript 库或第三方库包装成 Vue 组件,例如把播放器包装成 <VideoPlayer>,把图表库包装成 <Chart>。
二次封装时要注意生命周期清理、SSR 环境、容器尺寸变化、事件类型、销毁实例和依赖版本,而不是只把初始化代码放进 mounted。
3.5 项目融合型
组件和业务页面、store、接口强绑定,直接放在业务项目中维护。这种方式启动最快,但如果公共组件直接依赖页面 store、路由和接口,后续复制到其他项目时往往会出现大量爆红和隐式依赖。
四、直接在业务项目里写组件,为什么一开始很快
原文有一个很真实的场景:需要一个展示面板时,直接进入业务项目的 components 目录,取 store 数据,边改边看页面效果,几分钟就能跑起来。
这种方式在以下情况下完全合理:
- 组件只服务一个页面;
- 需求变化快,公共 API 还没有稳定;
- 抽象成本高于预计复用收益;
- 组件依赖页面上下文,强行独立反而会增加适配层。
问题在于,很多“只会用一次”的样式后来会再次出现。复制文件时又遇到 store、路由、接口和全局样式依赖,修复一个 bug 需要在多个项目中重复修改。
因此,抽象不是越早越好,而是要在复用信号出现时建立合适的边界:
页面一次性结构
↓ 需要复用或独立测试
业务组件
↓ 需要跨项目共享
包或组件库
↓ 多团队使用
文档、版本、发布和治理平台
五、大组件库的维护成本
把组件集中到一个仓库,可以让组件问题和业务问题更容易区分,也能统一代码规范、Review、测试和发布。但组件数量增长后,会出现原文提到的几个问题:
- 一个组件库打包后体积变大;
- 组件重复、命名不清或缺少文档;
- 一个业务组件引入了很重的播放器或编辑器;
- lodash、图表库等依赖被多个入口重复打包;
- 公共组件的破坏性修改影响大量页面。
解决办法不只是“拆成更多仓库”,而是先分析依赖和发布边界:
- 用 Bundle Analyzer 或构建报告定位体积来源;
- 使用 ESM、tree-shaking 和正确的
sideEffects配置; - 将 Vue、Vue Router、UI 库等设置为合适的 peer dependency;
- 重型功能按需加载或拆成独立包;
- 通过 workspace 复用构建配置,避免每个包复制一份 webpack 配置;
- 使用明确的包入口和
exports,防止深层路径依赖; - 对公共包采用 SemVer、变更日志和迁移说明。
5.1 一个现代组件包的结构
不必把每一个组件都发布成一个 npm 包。可以先使用 monorepo 管理多个包:
repo/
├─ apps/
│ ├─ docs/ # 组件文档站
│ └─ playground/ # 本地组合调试
├─ packages/
│ ├─ ui/ # 基础组件
│ ├─ business-message/ # 业务组件
│ └─ shared/ # 类型和工具
├─ .changeset/
├─ package.json
└─ pnpm-workspace.yaml
只有拥有独立版本、依赖或发布节奏的边界,才值得成为独立包。pnpm workspace、Nx、Turborepo 等工具都可以帮助共享任务和缓存;选型要根据团队规模和 CI 复杂度决定。
一个组件包的 package.json 可以这样表达入口和依赖:
{
"name": "@example/ui",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./style.css": "./dist/style.css"
},
"peerDependencies": {
"vue": "^3.5.0"
},
"sideEffects": ["**/*.css"]
}
这只是示例,具体字段应与 Vite/Rollup、TypeScript 和发布平台的输出保持一致。
六、组件体积和加载性能
组件体积要从“用户实际下载了什么”来分析,而不是只看源代码行数:
- 公共组件是否按入口 tree-shaking;
- CSS 是否被重复注入;
- 第三方库是否被每个业务包各自打入;
- 首屏是否加载了只在弹窗或二级页面使用的代码;
- 是否把完整语言包、图标包和编辑器全部放进主包。
对于 Vue 3,可以使用异步组件:
import { defineAsyncComponent } from 'vue'
const RichEditor = defineAsyncComponent(() => import('./RichEditor.vue'))
路由级代码分割、组件级异步加载和按需导入需要结合真实性能数据使用。异步加载不是越多越好,过度拆分会增加请求和交互延迟。
如果组件依赖一个很重的播放器或图表库,原文建议建立独立仓库并发布内网 npm 包,这个建议仍然成立;现代 monorepo 也可以在同一仓库中完成独立构建和版本发布,不必为了“独立包”强行维护多个完全重复的工程模板。
七、组件说明和可索引性
“组件库有 200 多个组件”本身不是成果。使用者还需要知道:
- 组件解决什么问题;
- 最小使用示例是什么;
- props、事件、插槽和
v-model如何使用; - 默认行为、边界条件和无障碍要求是什么;
- 依赖哪些样式或 peer dependency;
- 哪些 API 已废弃,如何迁移;
- 组件在哪些项目中可以使用。
原文用“有图有真相”的组件索引图说明这一点。组件平台现在可以用 Storybook、Histoire、VitePress、VuePress 或自研文档站实现。关键不是工具名称,而是让示例、类型和文档尽量来自同一份可执行数据,避免 README 与真实 API 分离。

7.1 一个合格组件文档至少包含
组件目的
安装和导入
最小示例
Props / Emits / Slots / v-model
状态、边界和错误处理
可访问性说明
主题和样式变量
版本与迁移说明
测试和在线预览
Vue 3 + TypeScript 组件可以把 props 类型作为文档的重要来源,但“有类型”不等于“有文档”。类型还需要配合注释、默认值、示例和交互说明。
八、原文的 comp 脚手架思路
原文设计了一个名为 comp 的内部 CLI,希望隐藏 webpack、Babel、TypeScript、Sass/Less、Jest、CI/CD 等重复配置,让开发者只关注组件本身。这个思路仍然值得借鉴:平台工程的价值之一就是提供可靠的默认配置。
原文规划的命令包括:
comp new:按照模板创建组件项目并初始化 Git、CI/CD;comp start:启动独立示例和调试页面;comp watch:监听编译,服务于 link 或本地联调;comp babel:编译 npm 包;comp dev:监听编译 UMD 包;comp build:构建 UMD、npm 包、截图和 README;comp test:执行 Jest 单元测试。
这些命令名属于原文时代的内部实现,不应直接当成今天的标准。现代 Vue 项目通常可以使用:
- Vite library mode 或 Rollup 构建库;
- Vitest + Vue Test Utils 编写单元和组件测试;
- Playwright 做关键交互和文档站端到端测试;
- Storybook 或 Histoire 提供独立开发环境;
- Changesets 管理版本、变更日志和发布;
- GitHub Actions、GitLab CI 或企业 CI 执行检查和发布。
通过 workspace 共享这些能力后,每个组件包仍可以保持干净的目录结构:
packages/ui-button/
├─ src/Button.vue
├─ src/index.ts
├─ stories/Button.stories.ts
├─ test/Button.test.ts
└─ README.md
特殊项目仍可提供 comp.config.ts 或 Vite 配置扩展,但扩展点应该有限、可测试,并且不破坏默认构建。
九、Usage:让组件拥有独立开发能力
原文提出:开发者在调试组件时已经写过一份 mock 数据和用法,只是这些内容散落在业务页面中。如果把示例独立出来,它就可以同时成为:
- 组件开发 playground;
- 文档示例;
- 回归测试输入;
- 在线编辑器的初始代码;
- 截图和视觉测试的数据来源。
例如 Vue 3 的一个 Story 可以描述组件状态:
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'
const meta = {
component: Button,
args: {
label: '保存',
disabled: false,
},
} satisfies Meta<typeof Button>
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {}
export const Disabled: Story = {
args: { disabled: true },
}
无论使用 Storybook、Histoire 还是自研 docs.ts,示例都应该尽量可执行,而不是只在 Markdown 里复制一段可能已经失效的代码。


CodePen、StackBlitz 等在线编辑器可以作为分享渠道,但企业内部组件通常还要考虑私有依赖、权限、版本锁定和源码泄漏风险。
十、Props 元数据和自动文档
原文使用 react-docgen-typescript 分析 React TypeScript props,并提到 loader 和 __docInfo。这条链路在 React 项目仍有参考价值,但它不能直接分析 Vue SFC。
Vue 3 项目可以考虑:
- 从
defineProps、defineEmits和 TypeScript 类型生成元数据; - 使用 Volar 生态的
vue-component-meta; - 使用适合 Vue 版本的
vue-docgen-api; - 由 Storybook/Histoire 根据组件类型和示例展示 API;
- 把类型检查放在 CI,避免文档生成悄悄失败。
自动生成的文档适合覆盖“名称、类型、默认值、事件和插槽”等结构化信息,不应代替人工撰写的目的、交互规则和迁移说明。

如果需要在组件构建产物中添加元数据,应优先放在独立的 manifest 文件中,而不是向运行时组件对象写入不可控的私有字段。这样可以避免增加生产包体积,也不会污染组件实例。
十一、Preview:自动截图和视觉回归
原文建议在构建过程中使用 Puppeteer 启动 docs.ts 并截图,再把图片发布到 CDN。今天也可以使用 Playwright 完成:
- 启动固定版本的文档站;
- 等待字体、网络和组件状态稳定;
- 截取关键 story 或页面;
- 做视觉差异比较;
- 在 CI 中保留失败截图和 trace。
截图不应只是装饰,它可以成为组件 API 和样式的回归基线。需要特别处理动画、时间、随机数、网络请求和字体,否则截图会产生大量无意义差异。




十二、发布和治理
一个可持续维护的组件平台,通常还需要:
- SemVer 和变更日志;
- 发布前类型检查、Lint、单元测试和 E2E 测试;
- 破坏性变更的迁移文档;
- 包大小和依赖变化检查;
- 组件 owner、维护状态和弃用时间;
- 安全漏洞和许可证检查;
- 组件使用量、下载量、缺陷和反馈指标。
原文说“组件发布次数、下载次数、关联 bug 数可以评估代码质量”。这些指标可以作为线索,但不能单独代表质量:下载量可能只是依赖传递,发布次数也可能意味着不稳定。更可靠的判断还包括测试通过率、升级成功率、缺陷严重度和使用者反馈。
微前端也不是只要组件都能独立构建就可以“非常轻松接入”。微前端还涉及运行时隔离、依赖共享、路由、通信、样式和发布回滚;标准化组件包只是其中一部分基础设施。
十三、Vue 3 / Nuxt 4 中的组件边界
现代 Vue 项目可以按下面的方向组织:
components/:展示和交互组件;- composable:复用响应式逻辑、请求、分页和订阅;
server/:Nuxt 服务端接口和服务端逻辑;- Pinia:跨页面、跨模块的领域状态;
pages/:路由页面和页面级数据组合;packages/:跨应用共享的基础或业务包。
组件不应直接依赖 Nuxt 页面、某个具体路由或全局 store 的全部字段。可以通过 props、emits、slots、provide/inject 或明确的 composable 接口缩小依赖面。
<!-- 一个可复用组件的公共边界 -->
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{
loading?: boolean
disabled?: boolean
}>()
const value = ref('')
const emit = defineEmits<{
submit: [value: string]
}>()
function submit() {
emit('submit', value.value)
}
</script>
<template>
<form @submit.prevent="submit">
<slot name="default" />
<input v-model="value" />
</form>
</template>
实际代码应把表单值作为真实状态处理;这里只展示 props、emits 和 slots 是如何形成边界。
总结
原文最后得到的并不只是一个自动生成 README 的功能,而是一组可以继续扩展的数据:组件的 Props、Usage、Preview、版本、包名、构建产物和使用关系。
这套思路今天仍然成立,但实现方式已经从“每个组件复制一份 webpack/Babel 配置”发展为:
组件边界
+ 类型和 API
+ 可执行示例
+ 单元/E2E/视觉测试
+ 文档和索引
+ 版本与变更
+ 构建与发布自动化
= 可持续维护的组件平台
最重要的结论有:
- 不是所有 UI 都值得成为公共组件;
- 复用组件要隔离业务 store、路由和接口等隐式依赖;
- 大型组件库必须同时治理体积、依赖、文档、版本和测试;
- 独立 playground、可执行 Usage 和自动预览能降低文档维护成本;
- Vue 组件文档要同时覆盖 props、emits、slots、
v-model、可访问性和边界行为; - Bit、monorepo、Storybook、Vite、Changesets 是不同层面的工具,不应互相替代;
- 组件平台的目标不是收集最多组件,而是让正确的组件容易被发现、使用和升级。
参考资料
- Vue 3:Component Basics
- Vue 3:Props
- Vue 3:Component Events
- Vue 3:Slots
- Vue 3:Composables
- Vue 3:性能优化
- Vite:构建库模式
- Storybook:Vue 3
- Vitest
- Changesets
- Bit 官方文档
原文作者:YeeWang。原文关于并行工作背景、Bit 的启发、组件类型、组件库痛点、comp 脚手架、Usage/Props/Preview 自动生成和组件平台的主线予以保留;招聘、邮箱、个人推广、抓取噪声、失效图片和过时工具的绝对结论已删除或改为历史说明。