如何设计一套可复用的前端组件库?
一套可复用的前端组件库,本质是「三层结构 + 单仓多包 + 稳定 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 个月,再考虑扩到表格、日期选择器这类重组件。组件库的成本从来不在写组件,而在维护接口兼容性。
原文链接:https://www.gj0.com/thread-354.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。