23 KiB
Pika 组件库结构优化分析
基于当前代码库(
src/components约 77 个组件目录、80 个组件 TSX、74 个测试文件)的静态分析。
生成日期:2026-05-30
目录
- 现状概览
- 目录与命名规范
- 重复代码与可抽取的公共层
- ConfigProvider 与主题系统
- CSS 与 Design Token 策略
- 类型系统
- Props 诚实性(接口 vs 实现)
- 共享 Hooks 与工具
- 测试覆盖
- 文档与 DX
- 构建与导出
- 优先级路线图
- 附录:文件清单速查
1. 现状概览
1.1 分层结构(已成型,方向正确)
src/
├── components/
│ ├── common/ # 基础通用(Button、ConfigProvider、Icon…)
│ ├── layout/ # 布局(Layout、Flex、Grid、Space…)
│ ├── nav/ # 导航(Menu、Tabs、Breadcrumb…)
│ ├── entry/ # 数据录入(Input、Select、Form…)
│ ├── feedback/ # 反馈(Modal、Message、Alert…)
│ ├── display/ # 数据展示(Table、Card、Tooltip…)
│ └── shared/ # 内部共享(hooks、types、utils)
├── theme/ # JS Token 定义
└── global.css # CSS 变量(--nv-*)
六大分类与 Ant Design 的组件分区思路一致,有利于 AI 生成代码时的语义检索。shared/ 作为内部基础设施层也已建立。
1.2 做得好的地方
| 方面 | 说明 |
|---|---|
| 组件粒度 | 每个组件独立目录,含 *.tsx + *.module.css + index.ts + *.test.tsx,结构清晰 |
| Barrel 导出 | 分类 index.ts → 根 components/index.ts → src/index.ts,对外 API 集中 |
| 测试基线 | 主组件几乎都有 Vitest + Testing Library 测试(74 个 test 文件) |
| forwardRef | 布局/表单类组件普遍支持 ref 透传 |
| data- 属性* | 组件用 data-* 标记状态,利于测试与 CSS 选择器 |
| 共享 Hooks | useClickOutside、useScrollListener 等已在 overlay 类组件中复用 |
1.3 主要问题摘要
| 类别 | 严重度 | 一句话描述 |
|---|---|---|
| 遗留重复路径 | 🔴 高 | src/components/Button/ 与 common/Button/ 并存 |
| ConfigProvider 未接线 | 🔴 高 | 实现了 context,但业务组件几乎不消费 |
| Token 三套并行 | 🔴 高 | JS tokens / CSS --nv-* / 组件 --_* 混用 |
| Overlay 重复实现 | 🟠 中 | Tooltip 与 Popover 各 ~360 行,逻辑高度相似 |
| 类型别名泛滥 | 🟠 中 | PikaSize 几乎无人用,各组件自建 *Size |
| Props 接口超前 | 🟠 中 | 多个 prop 在类型里声明但实现缺失 |
| 文档空白 | 🟠 中 | 80 个组件仅 1 个 index.md |
| 构建 subpath 可能无效 | 🟡 低 | package.json exports "./*" 与源码结构不匹配 |
2. 目录与命名规范
2.1 文件夹命名两套并存
| 风格 | 示例 | 数量 |
|---|---|---|
| PascalCase | common/Button/、common/ConfigProvider/ |
~10 个 |
| kebab-case | display/avatar/、entry/tree-select/ |
~67 个 |
建议: 统一为 kebab-case 目录 + PascalCase 文件名,与绝大多数组件一致:
display/avatar/Avatar.tsx ✅ 推荐
common/Button/Button.tsx ⚠️ 目录应改为 common/button/
迁移成本:仅 common/ 下 10 个目录,可一次性批量 rename + 更新 import。
2.2 导出模式不一致
现状:
// 组件文件 — 普遍 default export
export default Button
// barrel — named export
export { Button } from './Button'
建议: 组件 TSX 改为 named export only,与 barrel 和 tree-shaking 更一致:
// Button.tsx
export const Button = forwardRef(...)
export type { ButtonProps }
2.3 复合组件组织方式不统一
| 模式 | 示例 |
|---|---|
| 同目录多文件 | display/avatar/Avatar.tsx + AvatarGroup.tsx |
| 单目录聚合 | entry/choice/Checkbox.tsx + Radio.tsx + Switch.tsx |
| Object.assign | Layout.Header、Card.Meta |
建议: 制定简单规则写入 CONVENTIONS.md(当前已删除):
- 强关联子组件 → 同目录 +
Object.assign或 compound export - 独立可选组件 → 同目录分文件
- 录入类「形态变体」→ 可聚合(如 choice)
2.4 遗留路径:Button 双份
| 路径 | 状态 |
|---|---|
src/components/common/Button/ |
✅ 现行源码,根 index 导出 |
src/components/Button/ |
⚠️ Git 索引中仍存在,含 index.md、测试 |
行动: 删除 src/components/Button/,文档迁移至 common/Button/index.md(或 docs/components/button.md)。
3. 重复代码与可抽取的公共层
3.1 Tooltip / Popover — 最高优先级抽取
两者共享:
GAP、PLACEMENT_POSITION_MAP、FLIP_MAP常量- Portal 渲染 + 定位计算
useScrollListener+useClickOutside- 显隐状态机
shared/overlay/
├── Overlay.tsx # 定位 + portal + 显隐
├── placement.ts # PLACEMENT_MAP, FLIP_MAP
├── useOverlay.ts # open/close/trigger 逻辑
└── Overlay.module.css
预估可减少 ~300 行重复代码,后续 Popconfirm、Dropdown 也可复用。
3.2 getSemantic 内联重复(22 处)
以下组件各自定义相同模式的 getSemantic helper:
common/Button/Button.tsx
display/badge/Badge.tsx
display/card/Card.tsx
display/table/Table.tsx
display/tooltip/Tooltip.tsx
… 等 22 个文件
建议: 抽到 shared/utils/semantic.ts:
export function getSemanticClass(
styles: Record<string, string>,
semantic?: Record<string, string>,
part: string,
): string | undefined
3.3 Portal 工具已写但未用
shared/utils/portal.ts 提供 getPortalContainer / renderPortal,零引用。
Tooltip、Popover、Tour、Modal 均直接 createPortal(..., document.body)。
建议: overlay 重构时统一接入,并读取 ConfigProvider 的 getPopupContainer。
3.4 Table 内联分页 vs nav/Pagination
display/table/Table.tsx 内置 PaginationInline,功能仅为 prev/next + 页码,不支持:
showSizeChangershowQuickJumperpageSizeOptions
建议: Table 直接组合 nav/pagination/Pagination,或抽取 shared/PaginationMini。
3.5 className 拼接模式重复
大量组件使用:
[className, styles.root].filter(Boolean).join(' ')
建议: shared/utils/classNames.ts(或引入轻量 clsx 依赖):
export function cn(...parts: (string | false | undefined | null)[]): string
4. ConfigProvider 与主题系统
4.1 现状:基础设施已建,消费方缺失
ConfigProvider 提供:
| Context 字段 | 是否被组件消费 |
|---|---|
prefixCls |
❌ CSS 硬编码 nv-* |
iconPrefixCls |
❌ Icon 用全局 .nv-icon |
colorPrimary / theme.token |
⚠️ 仅注入 wrapper inline style |
theme.components |
❌ 接口存在,从未应用 |
locale |
❌ 无组件读取 |
zIndex |
❌ Modal/Drawer 各自硬编码 |
getPopupContainer |
❌ 仅 prop 级,不读 context |
useConfig() 仅出现在 ConfigProvider.tsx 及其测试中。
4.2 建议接线顺序
Phase 1 — 最小可用全局配置:
- 创建
shared/hooks/usePikaContext.ts,封装useConfig()+ 默认值合并 - Overlay 组件(Tooltip、Popover、Dropdown、Modal)读取
getPopupContainer、zIndex - 所有 portal 组件 z-index 从 context 叠加(base + offset)
Phase 2 — 前缀与国际化:
- CSS Module 改为
[data-nv-prefix]或 runtime class prefix(成本较高,可延后) locale接入 DatePicker、Pagination、Modal 按钮文案
Phase 3 — 组件级主题:
- 实现
theme.components.Button等 token 覆盖
4.3 Token 源统一
当前三套 token 并行:
┌─────────────────────────────────────────────────────────┐
│ theme/tokens.ts (JS 对象) │
│ → entry/tokens/index.ts → inline style 注入 │
├─────────────────────────────────────────────────────────┤
│ global.css (:root --nv-*) │
│ → feedback 组件 CSS var fallback │
├─────────────────────────────────────────────────────────┤
│ 组件私有 --_* (TSX inline style → CSS Module) │
│ → Button, Layout, Progress 等 │
└─────────────────────────────────────────────────────────┘
问题:
theme/tokens.ts的color.primary = '#6C5CE7'与global.css暗色模式值可能不同步- Entry 组件 inline style 不响应 ConfigProvider 运行时改色
global.css首行@import './theme/dumi.css'把文档站样式耦合进库
建议目标架构:
theme/
├── tokens.css # 唯一 token 源(--nv-*)
├── tokens.ts # 从 CSS 变量读取或生成类型(可选)
├── dark.css
└── compact.css
组件 CSS:只用 var(--nv-*),组件级 --_* 仅用于 prop 驱动的动态值(width、height)
ConfigProvider:通过 style 覆盖 --nv-color-primary 等根变量
5. CSS 与 Design Token 策略
5.1 当前模式评估
| 模式 | 用途 | 评价 |
|---|---|---|
*.module.css |
组件样式隔离 | ✅ 主流,正确 |
--nv-* + fallback |
设计 token | ✅ 正确方向,但 fallback 硬编码过多 |
--_* inline 注入 |
prop 驱动动态值 | ✅ 适合 Layout/Button 的高度、宽度 |
全局 Icon/style.css |
图标字体 | ⚠️ 应改为 CSS Module 或 CSS-in-JS |
| inline style 色值 | Entry 组件 | ❌ 应迁移到 CSS var |
5.2 硬编码色值示例(需逐步清理)
以下文件存在与 token 并行的硬编码:
display/popover/Popover.module.css—#fff、rgba(0,0,0,0.85)display/collapse/Collapse.module.css— 边框/背景硬编码entry/tokens/index.ts— 直接从 JS 对象读色,不经过 CSS 变量
5.3 CSS 变量命名规范建议
| 层级 | 前缀 | 示例 | 谁设置 |
|---|---|---|---|
| 全局设计 token | --nv- |
--nv-color-primary |
global.css / ConfigProvider |
| 组件内部变量 | --_{part}- |
--_height、--_padding |
组件 TSX inline style |
| 语义 DOM | data-* |
data-collapsed、data-size |
组件 TSX |
避免 --_* 与 --nv-* 语义混淆:前者是实例级动态值,后者是主题级静态 token。
6. 类型系统
6.1 重复 Size / Status 类型
shared/types/common.ts 已定义:
export type PikaSize = 'small' | 'middle' | 'large'
export type PikaStatus = 'default' | 'success' | 'warning' | 'error' | 'info'
但 几乎无组件引用。各模块自建:
| 类型 | 位置 |
|---|---|
DataEntrySize |
entry/common.ts |
ButtonSize |
common/Button/Button.tsx |
AvatarSizeType |
display/avatar/Avatar.tsx |
TagSize, CardSize, TableSize, BadgeSize… |
各 display 组件 |
建议:
// shared/types/common.ts — canonical types
export type PikaSize = 'small' | 'middle' | 'large'
export type PikaStatus = 'default' | 'success' | 'warning' | 'error' | 'info'
// entry/common.ts
import type { PikaSize, PikaStatus } from '../../shared/types'
export type DataEntrySize = PikaSize
export type DataEntryStatus = PikaStatus
// 组件 props
export interface ButtonProps {
size?: PikaSize
}
6.2 公共 Props 基类利用不足
entry/common.ts 定义了良好的基类:
DataEntryBasePropsDataEntryInputPropsDataEntryTextualPropsDataEntrySelectableProps
建议: display / feedback 组件也建立类似基类:
// shared/types/component.ts
export interface PikaComponentProps {
className?: string
style?: CSSProperties
}
export interface PikaInteractiveProps extends PikaComponentProps {
disabled?: boolean
}
6.3 CSSCustomProperties 使用率低
shared/types/css.ts 仅 5 个组件 import,其余用 Record<string, string> 或无类型。
建议: 统一 cssVars 类型:
type CSSVars = CSSProperties & Record<`--${string}`, string>
7. Props 诚实性(接口 vs 实现)
接口声明了但实现缺失的 prop,会误导 AI 和使用者。应 实现 或 从类型中移除(或标注
@deprecated/ 文档说明「即将支持」)。
7.1 已确认未实现的 Props
| 组件 | Prop | 现状 |
|---|---|---|
| Layout.Sider | onBreakpoint |
有 breakpoint data 属性,无 resize 监听 |
| DatePicker | mode, format |
固定 date 模式 + YYYY-MM-DD |
| TimePicker | format, hourStep |
固定格式与步进 |
| Slider | range |
仅单值滑块 |
| TextArea | autoSize, onResize |
未实现自动高度 |
| Upload | directory, onPreview |
未实现文件夹上传与预览 |
| Popconfirm | okType |
确认按钮未区分 danger/primary |
| TreeSelect | treeCheckable |
解构为 _treeCheckable 或未使用 |
| Form | labelCol, wrapperCol |
无栅格布局 |
| Table | filters, filteredValue, filterMultiple |
Column 类型完整,无 filter UI |
| Table | showSizeChanger, showQuickJumper |
PaginationConfig 有类型,内联分页未支持 |
| Mention | showSearch, showClear |
继承自基类但未解构 |
7.2 处理策略
优先级 A — 高频 API(Ant Design 对标)
→ 补实现:DatePicker.format、Slider.range、Layout.onBreakpoint
优先级 B — 低频 / 复杂
→ 从类型移除,CHANGELOG 记录,待迭代再加
优先级 C — 计划内
→ 保留类型,组件 JSDoc 标注 @experimental
7.3 类型笔误
TooltipPlacement 含 'rightRight',疑似应为 'rightBottom'(Tooltip.tsx)。FLIP_MAP 映射也需核对。
8. 共享 Hooks 与工具
8.1 已有 Hooks 使用情况
| Hook | 路径 | 已使用 | 应使用未使用 |
|---|---|---|---|
useClickOutside |
shared/hooks/ |
Select, Dropdown, Tooltip, Popover, Popconfirm, AutoComplete, RangePicker | Cascader, DatePicker, TimePicker, TreeSelect, Mention |
useScrollListener |
同上 | Affix, Anchor, BackTop, Tooltip, Popover, Tour | — |
useEscapeKey |
同上 | Modal, Drawer, Tour, Image | — |
useMatchMedia |
同上 | Grid | Layout.Sider(breakpoint) |
8.2 重复实现需清理
| 组件 | 问题 | 建议 |
|---|---|---|
FloatButton |
自定义 document.addEventListener('click') |
改用 useClickOutside |
Image |
useEscapeKey + 额外 keydown 监听 |
合并为单一 hook |
Tour |
useEscapeKey + 额外 keydown 处理方向键 |
扩展 hook 或统一 handler |
8.3 建议新增 Hooks
| Hook | 用途 |
|---|---|
useControllableState |
统一 controlled/uncontrolled 模式(大量组件重复 isControlled 逻辑) |
useBreakpoint |
Layout.Sider、Grid 共用 responsive 逻辑 |
useMergedRef |
forwardRef + 内部 ref 合并 |
useControllableState 示例 — 当前 Layout、Sider、Modal、Tabs 等均有类似代码:
const isControlled = value !== undefined
const current = isControlled ? value : internal
9. 测试覆盖
9.1 覆盖率概况
- 主组件: 几乎每个一级目录都有
*.test.tsx✅ - 子组件: 以下 7 个 无独立测试:
| 子组件 | 路径 | 风险 |
|---|---|---|
AvatarGroup |
display/avatar/ |
maxCount、overflow 逻辑 |
BadgeRibbon |
display/badge/ |
定位、颜色 |
CardGrid / CardMeta |
display/card/ |
栅格布局 |
ImagePreviewGroup |
display/image/ |
多图预览切换 |
StatisticTimer |
display/statistic/ |
倒计时逻辑 |
CheckableTag |
display/tag/ |
选中态 |
9.2 测试质量建议
| 方向 | 说明 |
|---|---|
| 行为测试优先 | 少测 class 名,多测交互(open/close、keyboard、form submit) |
| 未实现 prop 不测 | 避免测试「接口存在但行为不存在」造成 false positive |
| ConfigProvider 集成测试 | 验证 theme/locale 注入后组件行为变化 |
| a11y 基线 | overlay 组件补充 aria 属性断言 |
9.3 重复测试
src/components/Button/Button.test.tsx 与 common/Button/Button.test.tsx 可能重复 — 随遗留目录一并清理。
10. 文档与 DX
10.1 文档现状
| 类型 | 数量 | 路径 |
|---|---|---|
组件 co-located index.md |
1 | components/Button/index.md(遗留) |
| Dumi 指南 | 3 | docs/index.md, getting-started.md, components/index.md |
| 逐组件 API 文档 | 0 | — |
| 设计规范 | 1 | DESIGN_SPEC.md(完整) |
| 开发约定 | 0 | CONVENTIONS.md 已删除 |
10.2 文档建设建议
短期(可脚本化):
- 为每个组件目录生成
index.md模板(frontmatter + 基础 demo + Props 表占位) - Dumi 配置
resolve.atomDirs指向src/components
中期:
- 从 TS 类型自动生成 Props 表(dumi-theme-Pika 或 typedoc)
- 每个组件至少 3 个 demo:基础用法、受控模式、禁用/错误态
长期:
- AI 示例库 — 与
DESIGN_SPEC.md的 AI-First 原则对齐,每组件提供「AI 友好」单行示例
10.3 开发者体验
| 改进项 | 说明 |
|---|---|
恢复 CONVENTIONS.md |
目录命名、export 规范、CSS 约定、测试要求 |
npm run lint:fix |
已有 lint-staged,可加 CI workflow |
| Storybook vs Dumi | 当前 Dumi 足够,保持单文档方案 |
| 组件生成 CLI | pnpm gen component Button --category common 脚手架 |
11. 构建与导出
11.1 当前构建链
father build → dist/cjs + dist/esm
src/index.ts → export * from './components' + tokens
11.2 问题
| 问题 | 详情 |
|---|---|
| Subpath exports 可能无效 | package.json 声明 "./*" → dist/esm/*/index.js,但源码无对应分包 |
| CSS 未作为包入口 | 消费者需自行引入 token CSS;global.css 未 export |
| global.css 耦合 dumi | @import './theme/dumi.css' 不应出现在库运行时入口 |
| shared 不对外 | 第三方无法复用 hooks/utils |
| 手动维护导出面 | 新组件需改 3 处 index |
11.3 建议
package.json exports 修正:
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./styles": "./dist/styles/global.css",
"./tokens": "./dist/esm/theme/tokens.js"
}
}
CSS 构建:
- 将
global.css拆为styles/tokens.css(纯 token)+styles/dumi.css(文档专用) - father 配置
extraBabelPlugins或 postcss 复制 CSS 到 dist
按需加载(可选):
- 若需要
@Pika/ui/button,需 father 多 entry 配置 + 每组件独立 package path
12. 优先级路线图
P0 — 基础一致性(1–2 周)
| # | 任务 | 影响 |
|---|---|---|
| 1 | 删除 src/components/Button/ 遗留,统一至 common/Button/ |
消除混淆 |
| 2 | 统一 common/ 目录为 kebab-case |
命名一致 |
| 3 | Props 诚实性清理:移除或标注未实现 prop | AI 生成准确 |
| 4 | global.css 与 dumi.css 解耦 |
库可独立使用 |
| 5 | 新增 shared/utils/classNames.ts |
减少重复 |
P1 — 架构增强(2–4 周)
| # | 任务 | 影响 |
|---|---|---|
| 6 | 抽取 shared/overlay/ primitives |
Tooltip/Popover 减 300+ 行 |
| 7 | 抽取 getSemantic → shared/utils/semantic.ts |
22 处重复消除 |
| 8 | ConfigProvider Phase 1 接线(zIndex、getPopupContainer) | 全局配置可用 |
| 9 | 统一 Size/Status 类型为 PikaSize / PikaStatus |
类型一致 |
| 10 | Cascader/DatePicker/TreeSelect 接入 useClickOutside |
交互完整 |
| 11 | 新增 useControllableState hook |
减少状态逻辑重复 |
P2 — 体验完善(4–8 周)
| # | 任务 | 影响 |
|---|---|---|
| 12 | Token 统一为 CSS --nv-* 单源 |
主题可运行时切换 |
| 13 | 补全 7 个子组件测试 | 测试完整 |
| 14 | 每组件 index.md + 基础 demo |
文档可用 |
| 15 | 恢复并完善 CONVENTIONS.md |
贡献者友好 |
| 16 | 实现高频未实现 prop(DatePicker.format、Slider.range) | API 完整 |
| 17 | package.json exports + CSS 入口修正 | 发布可用 |
P3 — 长期演进
| # | 任务 |
|---|---|
| 18 | 组件级 theme override(theme.components) |
| 19 | 按需 subpath import 分包构建 |
| 20 | 组件生成 CLI |
| 21 | a11y 审计与 WCAG 基线 |
| 22 | 可视化组件(AntV)与 UI 组件 Token 统一 |
13. 附录:文件清单速查
13.1 应删除/迁移
src/components/Button/ → 删除,保留 common/Button/
src/components/Button/index.md → 迁移至 docs 或 common/Button/
13.2 应统一命名(common/ PascalCase → kebab-case)
common/Button/ → common/button/
common/ConfigProvider/ → common/config-provider/
common/FloatButton/ → common/float-button/
common/BackTop/ → common/back-top/
… (共 10 个)
13.3 共享层待建设
shared/overlay/Overlay.tsx
shared/utils/classNames.ts
shared/utils/semantic.ts
shared/hooks/useControllableState.ts
shared/hooks/useBreakpoint.ts
shared/hooks/usePikaContext.ts
13.4 应对接 ConfigProvider 的组件(优先)
display/tooltip/Tooltip.tsx
display/popover/Popover.tsx
nav/dropdown/Dropdown.tsx
feedback/modal/Modal.tsx
feedback/drawer/Drawer.tsx
feedback/message/Message.tsx
feedback/notification/Notification.tsx
display/tour/Tour.tsx
entry/select/Select.tsx
entry/date-picker/DatePicker.tsx
13.5 统计
| 指标 | 数量 |
|---|---|
| 组件目录 | ~77 |
| 组件 TSX(不含测试) | ~80 |
| 测试文件 | ~74 |
| 无独立测试的子组件 | 7 |
| getSemantic 重复 | 22 处 |
| 未实现 prop 的组件 | ~12 |
| co-located 文档 | 1 |
总结
Pika 的 六大分类 + 单组件目录结构 已经是一个健康的基础,测试覆盖和 forwardRef 等实践也到位。当前主要短板集中在:
- 一致性 — 命名、导出、类型、token 三套并行
- 诚实性 — 接口超前于实现,对 AI-First 目标伤害最大
- 基础设施未接线 — ConfigProvider、shared utils 写了但没用起来
- 重复 — Overlay、getSemantic、className 拼接、受控状态
按 P0 → P1 → P2 推进,可以在不推翻现有结构的前提下,显著提升可维护性和 AI 代码生成准确率。