本文へ移動
cccskills
無料GitHub で公開

component-architecture

设计组件结构、拆分粒度和复用策略时使用。覆盖 React / Vue / Angular / Svelte。融合 Atomic Design + Container/Presentation + Compound Components + Headless 模式。

インストール方法を見る

含まれるファイル(2)

  • SKILL.md14.1 KB
  • templates/component-design-template.md3.0 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

组件架构(Component Architecture)

参考来源:Brad Frost《Atomic Design》、Dan Abramov《Presentational and Container Components》、Kent C. Dodds《Compound Components》、Adam Wathan《Inversion of Control》。

适用场景

  • 设计组件树和拆分粒度
  • 复用率提升 / 设计系统建设
  • 组件 API 设计
  • 组件状态封装策略
  • React / Vue / Angular / Svelte 框架范式

核心原则

1. 单一职责
   一个组件做一件事

2. 显式 props,避免 prop drilling
   多于 3 层往下传 → 用 Context / 状态管理

3. Composition over Configuration
   `<Card><Card.Header /></Card>` 优于
   `<Card title="..." footer="..." actions={[...]} />`

4. 数据从上往下,事件从下往上
   单向数据流

5. 设计 API 比内部实现重要
   API 是契约,能改实现不能改 API

6. 受控 vs 非受控明确
   表单组件提供两种模式

7. 可访问性是默认
   不是事后补救(详见 a11y skill)

8. 状态最小化
   能从 props 派生就不存 state

Atomic Design 五层

┌─────────────────────────────────────┐
│ Pages(页面)                        │
│ 例:UserProfilePage / OrderListPage   │
└────────────────┬────────────────────┘
┌────────────────┴────────────────────┐
│ Templates(模板)                     │
│ 例:DashboardLayout / AuthLayout      │
└────────────────┬────────────────────┘
┌────────────────┴────────────────────┐
│ Organisms(生物体)                   │
│ 例:Header / Sidebar / OrderList     │
└────────────────┬────────────────────┘
┌────────────────┴────────────────────┐
│ Molecules(分子)                     │
│ 例:FormField / SearchBar / UserCard │
└────────────────┬────────────────────┘
┌────────────────┴────────────────────┐
│ Atoms(原子)                         │
│ 例:Button / Input / Icon / Label    │
└─────────────────────────────────────┘

Container / Presentation 模式

Container(容器):
  - 管理状态 / 调用 API / 业务逻辑
  - 不写 UI 细节
  - 命名:UserListContainer

Presentation(展示):
  - 纯 UI / 无状态(或仅 UI 状态)
  - 接收 props,emit 事件
  - 易测试 / 易复用 / Storybook 友好
  - 命名:UserList
// Presentation:纯 UI
interface UserListProps {
  users: User[];
  isLoading: boolean;
  onUserClick: (id: number) => void;
}

export function UserList({ users, isLoading, onUserClick }: UserListProps) {
  if (isLoading) return <Skeleton count={5} />;
  if (users.length === 0) return <EmptyState />;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id} onClick={() => onUserClick(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// Container:业务逻辑
export function UserListContainer() {
  const { data, isLoading } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const navigate = useNavigate();
  
  return (
    <UserList
      users={data ?? []}
      isLoading={isLoading}
      onUserClick={(id) => navigate(`/users/${id}`)}
    />
  );
}

Compound Components 模式

// 用法(用户写)
<Tabs defaultValue="orders">
  <Tabs.List>
    <Tabs.Trigger value="orders">订单</Tabs.Trigger>
    <Tabs.Trigger value="products">商品</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="orders"><OrderList /></Tabs.Panel>
  <Tabs.Panel value="products"><ProductList /></Tabs.Panel>
</Tabs>

// 实现:用 Context 共享状态
const TabsContext = createContext<TabsContextValue | null>(null);

export function Tabs({ children, defaultValue }: TabsProps) {
  const [value, setValue] = useState(defaultValue);
  return (
    <TabsContext.Provider value={{ value, setValue }}>
      <div data-tabs>{children}</div>
    </TabsContext.Provider>
  );
}

Tabs.List = function TabsList({ children }) {
  return <div role="tablist">{children}</div>;
};

Tabs.Trigger = function TabsTrigger({ value, children }) {
  const ctx = useContext(TabsContext);
  return (
    <button
      role="tab"
      aria-selected={ctx.value === value}
      onClick={() => ctx.setValue(value)}
    >
      {children}
    </button>
  );
};

Tabs.Panel = function TabsPanel({ value, children }) {
  const ctx = useContext(TabsContext);
  return ctx.value === value ? <div role="tabpanel">{children}</div> : null;
}

Headless 模式

不提供样式,只提供逻辑和无障碍:
  - Radix UI(React)
  - Headless UI(React/Vue)
  - React Aria
  - Melt UI(Svelte)
  - Bits UI(Svelte)
  - Kobalte(Solid)

优势:
  - 100% 自定义样式
  - 无障碍由库保证
  - 不与设计系统冲突

劣势:
  - 需要自己写样式
  - 学习曲线
// 用 Radix Dialog
import * as Dialog from '@radix-ui/react-dialog';

export function ConfirmDialog({ children, onConfirm }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>{children}</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="overlay" />
        <Dialog.Content className="content">
          <Dialog.Title>确认</Dialog.Title>
          <Dialog.Description>...</Dialog.Description>
          <button onClick={onConfirm}>确认</button>
          <Dialog.Close>取消</Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

React 组件范式

// 1. 函数组件(推荐)
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', isLoading, children, className, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={cn(buttonVariants({ variant, size }), className)}
        disabled={isLoading || props.disabled}
        {...props}
      >
        {isLoading && <Spinner />}
        {children}
      </button>
    );
  }
);
Button.displayName = 'Button';

// 2. 自定义 Hook 抽逻辑
function useUserList() {
  const [filter, setFilter] = useState('');
  const { data, isLoading } = useQuery({
    queryKey: ['users', filter],
    queryFn: () => fetchUsers({ filter }),
  });
  return { users: data ?? [], isLoading, filter, setFilter };
}

// 3. memo 避免不必要重渲染(仅当真有性能问题)
export const ExpensiveItem = memo(function ExpensiveItem({ item }) {
  return <div>...</div>;
});

Vue 3 组件范式

<!-- Button.vue -->
<script setup lang="ts">
interface Props {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  isLoading?: boolean;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
});

const emit = defineEmits<{
  click: [event: MouseEvent];
}>();
</script>

<template>
  <button
    :class="['btn', `btn-${variant}`, `btn-${size}`]"
    :disabled="isLoading || disabled"
    @click="emit('click', $event)"
  >
    <Spinner v-if="isLoading" />
    <slot />
  </button>
</template>
<!-- 组合式函数(Composables,Vue 的 Hook)-->
<script setup lang="ts">
import { useUserList } from '@/composables/useUserList';

const { users, isLoading, filter } = useUserList();
</script>

<!-- composables/useUserList.ts -->
<script lang="ts">
import { ref, computed } from 'vue';
import { useQuery } from '@tanstack/vue-query';

export function useUserList() {
  const filter = ref('');
  const { data, isLoading } = useQuery({
    queryKey: ['users', filter],
    queryFn: () => fetchUsers({ filter: filter.value }),
  });
  
  return {
    users: computed(() => data.value ?? []),
    isLoading,
    filter,
  };
}
</script>

Angular 组件范式

// button.component.ts
@Component({
  selector: 'app-button',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button
      [class]="'btn btn-' + variant() + ' btn-' + size()"
      [disabled]="isLoading() || disabled()"
      (click)="onClick.emit($event)"
    >
      <app-spinner *ngIf="isLoading()" />
      <ng-content />
    </button>
  `,
})
export class ButtonComponent {
  variant = input<'primary' | 'secondary'>('primary');
  size = input<'sm' | 'md' | 'lg'>('md');
  isLoading = input(false);
  disabled = input(false);
  onClick = output<MouseEvent>();
}

Svelte 5 组件范式

<!-- Button.svelte -->
<script lang="ts">
  interface Props {
    variant?: 'primary' | 'secondary' | 'danger';
    size?: 'sm' | 'md' | 'lg';
    isLoading?: boolean;
    onclick?: (e: MouseEvent) => void;
    children: any;
  }
  
  let {
    variant = 'primary',
    size = 'md',
    isLoading = false,
    onclick,
    children,
  }: Props = $props();
</script>

<button class="btn btn-{variant} btn-{size}" disabled={isLoading} {onclick}>
  {#if isLoading}<Spinner />{/if}
  {@render children()}
</button>

组件目录结构

推荐:feature-based + 共享组件分离

src/
├── components/              ← 跨业务的通用组件
│   ├── ui/                  ← 原子组件(Button, Input, Card)
│   ├── layout/              ← 布局组件
│   └── feedback/            ← Toast, Modal, Skeleton
├── features/                ← 业务模块(领域驱动)
│   ├── orders/
│   │   ├── components/      ← 仅订单用到的组件
│   │   ├── hooks/           ← useOrder, useOrderList
│   │   ├── api/             ← API 调用
│   │   ├── types.ts
│   │   └── OrderListPage.tsx
│   └── products/
│       └── ...
├── pages/                   ← 路由页面(薄)
├── lib/                     ← 工具函数
└── App.tsx

组件 API 设计

1. Props 命名
   - 布尔:is/has/can 前缀
   - 事件:on 前缀(onClick / onSubmit)
   - 字符串枚举:明确选项(variant: 'primary' | 'secondary')

2. 默认值
   - 提供合理默认
   - 不要全部 required

3. 受控 vs 非受控
   提供两种模式:
   <Input value={x} onChange={fn} />     // 受控
   <Input defaultValue="x" />            // 非受控

4. forwardRef
   原子组件应该转发 ref

5. ...rest props
   原子组件应允许传递 HTML 属性
   <Button {...props} />

状态封装原则

状态从上往下放:
  局部 state(一个组件用) → useState 在该组件
  父子共享 → 提到父组件
  跨组件 → Context / 状态管理库
  服务端数据 → TanStack Query / SWR

不要:
  - useState 然后只 setState 一次(用 props)
  - useEffect 同步 props 到 state(直接用 props)
  - 把 server state 放 Redux(用 React Query)

工作流程

1. 看设计稿 → 拆分 Atomic Design 五层
   ↓
2. 列原子组件清单(Button / Input / Icon ...)
   ↓
3. 看是否复用现有组件库(不要重造轮子)
   ↓
4. 组件 API 设计(先 README 再写代码)
   ↓
5. 实现 + Storybook story
   ↓
6. 单元测试 + 集成测试 + 视觉测试
   ↓
7. 检查 a11y(键盘 + 屏幕阅读器)
   ↓
8. 性能(memo 必要时 / 懒加载)
   ↓
9. 文档(API + 用法示例)

配套模板

  • templates/component-design-template.md — 组件设计文档(API + 状态 + 复用 + 测试 + a11y)

质量自检

□ 单一职责(一个组件做一件事)
□ Props 类型完整(TypeScript)
□ 默认值合理
□ 受控 / 非受控明确
□ forwardRef(原子组件)
□ ...rest 透传
□ a11y 完整(角色 / 键盘 / 屏幕阅读器)
□ 状态最小化
□ 命名清晰
□ Storybook story 覆盖主要状态
□ 测试覆盖
□ 不深嵌套(< 3 层 prop drilling)
□ 不滥用 memo / useCallback
□ Loading / Empty / Error 状态完整

常见坑

  1. prop drilling 5+ 层——用 Context / 状态库
  2. God Component——一个组件 1000 行
  3. 状态在错误层级——子组件用却放在最顶层
  4. useState 初始化是同步函数——重复执行
  5. useEffect 同步 props 到 state——直接用 props
  6. server state 放 Redux——用 TanStack Query
  7. 没有 Storybook——视觉回归无门
  8. 直接修改组件库源码——升级地狱
  9. 滥用 forwardRef / memo——过度优化
  10. 不区分受控 / 非受控——使用混乱
  11. Container 写 UI 细节——破坏分层
  12. Atomic Design 教条化——硬塞五层不实际
  13. a11y 后置——重构成本巨大
  14. 组件不文档化——团队各自重造

与其他 skill 的协作

上游:
  ui-ux-designer 工作流 → 设计稿 / 状态 / 流程
  api-designer → API 契约(影响数据组件)

下游:
  state-management → 全局状态
  styling-system → 样式
  forms-validation → 表单组件
  data-fetching → 数据组件
  testing-frontend → 测试覆盖
  accessibility-implementation → a11y 实现

相关参考

  • 项目根 references/frontend-component-libraries.md — 组件库选型
  • 项目根 references/frontend-frameworks-2026.md — 框架对比
  • ui-ux-designer 工作流 atomic-design / component-states skill

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

A/B 测试设计与分析时使用。适用于产品实验、功能验证、转化率优化。融合假设驱动 + 样本量计算 + 置信度判断。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

上线前业务方/产品方验收时使用。适用于功能交付确认、UAT、发布会签。融合 BDD Given-When-Then、Specification by Example、ATDD。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

设计无障碍可访问的界面时使用。适用于满足 WCAG 标准、键盘导航、屏幕阅读器支持。优先使用 WCAG 2.2 AA 级标准 + 键盘 + 焦点 + 对比度 + ARIA。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

实现可访问性(无障碍)时使用。覆盖 WCAG 2.2 AA / ARIA / 键盘导航 / 屏幕阅读器 / 颜色对比 / 焦点管理。融合 React Aria / Radix UI / Headless UI 实践 + axe-core 自动化测试。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

AI 模型部署时使用。适用于推理服务 / GPU / TensorRT / 监控 / 扩缩容。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

数据分析报告输出时使用。适用于业务分析报告、周报、专项分析、决策支持。融合洞察 + 建议 + 行动项。

日本語の概要は準備中です。原文の説明を表示しています。

zhaoxuya520/AI-Fullstack-Delivery-Workflow532026年5月19日 更新

zhaoxuya520 のスキルをすべて見る

このスキルの問題を報告する