技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

这一年我对组件的思考

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/104-这一年我的对组件的思考
本文目录15 个章节
  1. 一、从“把需求做完”到“让成果可复用”
  2. 二、Bit 给我的启发
  3. 三、团队里常见的组件形态
  4. 四、直接在业务项目里写组件,为什么一开始很快
  5. 五、大组件库的维护成本
  6. 六、组件体积和加载性能
  7. 七、组件说明和可索引性
  8. 八、原文的 comp 脚手架思路
  9. 九、Usage:让组件拥有独立开发能力
  10. 十、Props 元数据和自动文档
  11. 十一、Preview:自动截图和视觉回归
  12. 十二、发布和治理
  13. 十三、Vue 3 / Nuxt 4 中的组件边界
  14. 总结
  15. 参考资料

这一年我对组件的思考

原文写于 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、图表库等依赖被多个入口重复打包;
  • 公共组件的破坏性修改影响大量页面。

解决办法不只是“拆成更多仓库”,而是先分析依赖和发布边界:

  1. 用 Bundle Analyzer 或构建报告定位体积来源;
  2. 使用 ESM、tree-shaking 和正确的 sideEffects 配置;
  3. 将 Vue、Vue Router、UI 库等设置为合适的 peer dependency;
  4. 重型功能按需加载或拆成独立包;
  5. 通过 workspace 复用构建配置,避免每个包复制一份 webpack 配置;
  6. 使用明确的包入口和 exports,防止深层路径依赖;
  7. 对公共包采用 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 里复制一段可能已经失效的代码。

独立组件 Demo 和 Usage 示例(原文图片已本地化)

在线预览和编辑组件的示例(原文图片已本地化)

CodePen、StackBlitz 等在线编辑器可以作为分享渠道,但企业内部组件通常还要考虑私有依赖、权限、版本锁定和源码泄漏风险。

十、Props 元数据和自动文档

原文使用 react-docgen-typescript 分析 React TypeScript props,并提到 loader 和 __docInfo。这条链路在 React 项目仍有参考价值,但它不能直接分析 Vue SFC。

Vue 3 项目可以考虑:

  • definePropsdefineEmits 和 TypeScript 类型生成元数据;
  • 使用 Volar 生态的 vue-component-meta
  • 使用适合 Vue 版本的 vue-docgen-api
  • 由 Storybook/Histoire 根据组件类型和示例展示 API;
  • 把类型检查放在 CI,避免文档生成悄悄失败。

自动生成的文档适合覆盖“名称、类型、默认值、事件和插槽”等结构化信息,不应代替人工撰写的目的、交互规则和迁移说明。

组件 Props 元数据展示(原文图片已本地化)

如果需要在组件构建产物中添加元数据,应优先放在独立的 manifest 文件中,而不是向运行时组件对象写入不可控的私有字段。这样可以避免增加生产包体积,也不会污染组件实例。

十一、Preview:自动截图和视觉回归

原文建议在构建过程中使用 Puppeteer 启动 docs.ts 并截图,再把图片发布到 CDN。今天也可以使用 Playwright 完成:

  1. 启动固定版本的文档站;
  2. 等待字体、网络和组件状态稳定;
  3. 截取关键 story 或页面;
  4. 做视觉差异比较;
  5. 在 CI 中保留失败截图和 trace。

截图不应只是装饰,它可以成为组件 API 和样式的回归基线。需要特别处理动画、时间、随机数、网络请求和字体,否则截图会产生大量无意义差异。

自动生成组件预览截图(原文图片已本地化)

组件文档生成流程示意图(原文图片已本地化)

自动生成 README 和组件信息的示例(原文图片已本地化)

组件配置面板与可视化编辑示例(原文图片已本地化)

十二、发布和治理

一个可持续维护的组件平台,通常还需要:

  • 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/视觉测试
  + 文档和索引
  + 版本与变更
  + 构建与发布自动化
  = 可持续维护的组件平台

最重要的结论有:

  1. 不是所有 UI 都值得成为公共组件;
  2. 复用组件要隔离业务 store、路由和接口等隐式依赖;
  3. 大型组件库必须同时治理体积、依赖、文档、版本和测试;
  4. 独立 playground、可执行 Usage 和自动预览能降低文档维护成本;
  5. Vue 组件文档要同时覆盖 props、emits、slots、v-model、可访问性和边界行为;
  6. Bit、monorepo、Storybook、Vite、Changesets 是不同层面的工具,不应互相替代;
  7. 组件平台的目标不是收集最多组件,而是让正确的组件容易被发现、使用和升级。

参考资料

原文作者:YeeWang。原文关于并行工作背景、Bit 的启发、组件类型、组件库痛点、comp 脚手架、Usage/Props/Preview 自动生成和组件平台的主线予以保留;招聘、邮箱、个人推广、抓取噪声、失效图片和过时工具的绝对结论已删除或改为历史说明。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS