如何设计一套可复用的前端组件库?

liulian
liulian 正式会员超兽战士
发布于 2026-10-07 14:57 ·0 浏览 ·0 回复

一套可复用的前端组件库,本质是「三层结构 + 单仓多包 + 稳定 API + 自动化发布」这四件事的组合:用 design tokens 管样式变量,用 headless 组件管逻辑和无障碍,用业务组件做场景封装;代码放一个 pnpm workspace 单仓里,每个包独立构建、独立发版;对外 API 稳定优先于实现优雅。下面按落地顺序拆开讲。

组件库应该分成哪几层?

结论:至少分三层——tokens 层、headless 行为层、业务组件层,三层单向依赖,下层不知道上层的存在。

tokens 层只导出设计变量,比如 --color-primary: #2563eb、--space-4: 16px,产物是 CSS 变量 + TS 常量(export const space4 = 'var(--space-4)')。headless 层负责逻辑与无障碍:键盘导航、焦点管理、aria-expanded 这些,不含任何视觉样式,典型实现参考 Radix UI 或 React Aria 的思路。业务层才引入样式和具体业务语义,比如 <SearchInput />。

这样分层的好处是换肤只改 tokens,换视觉方案只改第三层,逻辑 bug 只在一个地方修。踩过的坑基本都出在把颜色写死在业务组件里。

目录结构和包怎么划分?

结论:用 pnpm workspace + Turborepo 做单仓多包,每个包一个 package.json,命名按 @scope/xxx。

packages/
  tokens/          @acme/tokens
  react/           @acme/react      # headless
  ui/              @acme/ui         # 带样式的业务组件
  icons/           @acme/icons
apps/
  docs/            Storybook + 站点

pnpm-workspace.yaml 写 packages: ['packages/*', 'apps/*'];turbo.json 里给 build 配 "dependsOn": ["^build"],保证 tokens 先于 ui 构建。命令就两条:pnpm i 安装,pnpm turbo build --filter=@acme/ui... 只构建 ui 及其依赖。单包仓库在组件超过 30 个、或者要被 3 个以上项目复用时,几乎一定会拆成这种结构。

组件的 API 怎么设计才不容易被改?

结论:三条硬规则——受控与非受控都要支持、原生属性必须能透传、外观差异用 variant 枚举而不是堆布尔值。

<Button primary ghost large> 这种 API 到第三个变体就失控了,改成 <Button variant="primary" size="lg">,枚举联合类型写死,TS 会在编译期拦住拼错的值。

透传用 rest 展开实现:

type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
};

export function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) {
  return <button data-variant={variant} data-size={size} {...rest} />;
}

组件名、props 名一旦发布就视为公开契约。改名字要走 major 版本,这条不妥协,否则下游项目升级时会集体炸掉。

构建产物和 package.json 怎么配?

结论:每个包同时产出 ESM 和 CJS,exports 字段写全,标记 sideEffects: false 以保住 tree-shaking。

用 tsup 8 打包最省事:tsup src/index.ts --format esm,cjs --dts --clean,一条命令出 dist/index.mjs、dist/index.cjs 和类型声明。package.json 里配:

{
  "sideEffects": false,
  "exports": {
    ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" },
    "./styles.css": "./dist/styles.css"
  },
  "peerDependencies": { "react": ">=18", "react-dom": ">=18" }
}

两条必须注意:结论是 React 一定要放 peerDependencies 而不是 dependencies,否则下游会出现两份 React;每个组件的样式最好单独出口或纯 CSS 变量化,不要在一个 index.ts 里做全局样式副作用,不然 sideEffects: false 会让样式被摇掉。

文档、测试和发布怎么自动化?

结论:Storybook 8 写用例即文档,Vitest + Testing Library 测行为,Changesets 管版本和 CHANGELOG,CI 全自动。

Storybook 的每个 story 同时就是视觉回归的样本,接 Chromatic 后每次 PR 自动出截图 diff。单元测试只测行为不测类名:断言"点击后 aria-expanded 变为 true",而不是断言"class 里包含 open"。

发布流程:pnpm changeset 记录改动,pnpm changeset version 按 semver 自动升版本号并生成 CHANGELOG,pnpm changeset publish 推到 npm。破坏性变更必须记成 major,新增组件记 minor,修样式记 patch。CI(GitHub Actions)里跑 build → test → publish,人工只审 PR。

别一开始就写 100 个组件

从 5 个高频组件起步:Button、Input、Select、Modal、Tooltip。这五个能覆盖大部分表单和弹层场景,也最容易暴露 API 设计问题。等有 3 个项目真实接入、API 稳定超过 1 个月,再考虑扩到表格、日期选择器这类重组件。组件库的成本从来不在写组件,而在维护接口兼容性。

版权声明:本文来自 GJ站长论坛《如何设计一套可复用的前端组件库?》
原文链接:https://www.gj0.com/thread-354.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~