关于 component-architecture
面向 React、Vue 和 Svelte 的 UI 组件架构设计、实现与审查技能,涵盖代码探索、组合与状态策略、性能、无障碍性及验证。
主要功能
- 01梳理现有组件结构与模式
- 02指导选择组合方式和状态管理
- 03涵盖组件组织与实现
- 04处理渲染性能和无障碍性
- 05提供组件验证步骤
适用场景
- 01构建可复用 UI 组件
- 02重构组件层级结构
- 03选择状态范围与组合模式
- 04审查性能和无障碍性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill component-architecture安装后在 Claude Code 中输入/component-architecture确认已加载。
附带文件references/component-patterns.mdscripts/validate-components.sh
详细介绍
资源
scripts/
validate-components.sh
references/
component-patterns.md
组件架构
本技能将指导你使用 GoodVibes 精准分析工具设计和实现 UI 组件。在构建 React、Vue 或 Svelte 组件,并确保合理组合、状态管理和性能优化时,可使用此工作流。
何时使用此技能
在以下情况下加载此技能:
- 构建新的 UI 组件或组件库
- 重构组件层级或组合模式
- 优化组件渲染性能
- 组织组件文件结构
- 实现设计系统或原子设计模式
- 在组件框架之间迁移
- 审查组件架构的可维护性
触发短语:“构建组件”“创建 UI”“组件结构”“渲染优化”“状态提升”“组件组合”。
核心工作流
阶段 1:探索
构建组件之前,先了解代码库中现有的模式。
步骤 1.1:梳理组件结构
使用 discover 查找所有组件文件并了解组织模式。
discover:
queries:
- id: react_components
type: glob
patterns: ["**/*.tsx", "**/*.jsx"]
- id: vue_components
type: glob
patterns: ["**/*.vue"]
- id: svelte_components
type: glob
patterns: ["**/*.svelte"]
verbosity: files_only
可了解的信息:
- 使用的框架(React、Vue、Svelte 或混合框架)
- 组件文件组织方式(按功能组织、原子化、扁平)
- 命名约定(PascalCase、kebab-case)
- 文件共置模式(组件与测试、样式放在一起)
步骤 1.2:分析组件模式
使用 discover 查找组合模式、状态管理和样式方案。
discover:
queries:
- id: composition_patterns
type: grep
pattern: "(children|render|slot|as\\s*=)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: state_management
type: grep
pattern: "(useState|useReducer|reactive|writable|createSignal)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
- id: styling_approach
type: grep
pattern: "(className|styled|css|tw`|@apply)"
glob: "**/*.{tsx,jsx,vue,svelte}"
- id: performance_hooks
type: grep
pattern: "(useMemo|useCallback|memo|computed|\\$:)"
glob: "**/*.{ts,tsx,js,jsx,vue,svelte}"
verbosity: files_only
可了解的信息:
- 组合策略(children 属性、render props、插槽)
- 状态管理模式(hooks、stores、signals)
- 样式方案(CSS modules、Tailwind、styled-components)
- 正在使用的性能优化技术
步骤 1.3:提取组件符号
使用 precision_read 提取符号,以了解组件导出情况。
precision_read:
files:
- path: "src/components/Button/index.tsx" # Example component
extract: symbols
symbol_filter: ["function", "class", "interface", "type"]
verbosity: standard
可了解的信息:
- 组件导出模式(具名导出或默认导出)
- Props 接口定义
- 辅助函数和 hooks
- 类型定义和泛型
步骤 1.4:阅读有代表性的组件
阅读 2-3 个结构良好的组件,了解实现模式。
precision_read:
files:
- path: "src/components/Button/Button.tsx"
extract: content
- path: "src/components/Form/Form.tsx"
extract: content
output:
max_per_item: 100
verbosity: standard
阶段 2:决策
步骤 2.1:选择组件组织模式
参阅 references/component-patterns.md 中的组织方式决策树。
常见模式:
- 原子设计 - 原子、分子、有机体、模板、页面
- 按功能组织 - 按功能或领域分组
- 扁平 - 所有组件放在同一个目录中
- 混合 - 共享组件 + 功能组件
决策因素:
- 团队规模(团队越大 -> 越需要结构化)
- 应用复杂度(越复杂 -> 越适合按功能组织)
- 是否有设计系统(有 -> 采用原子设计)
- 组件可复用性(高 -> 使用 shared/ui 目录)
步骤 2.2:选择组合模式
详细比较请参阅 references/component-patterns.md。
模式选择指南:
| 模式 | 适用情况 | 框架支持 |
|---|---|---|
| Children 属性 | 简单的包装组件 | React、Vue(插槽)、Svelte |
| Render props | 动态渲染逻辑 | React(旧版) |
| 复合组件 | 相关组件共享状态 | React、Vue、Svelte |
| 高阶组件 | 横切关注点 | React(旧版) |
| Hooks/Composables | 逻辑复用 | React、Vue 3、Svelte |
| 插槽 | 基于模板的组合 | Vue、Svelte |
现代建议:
- React:Hooks + children 属性(避免使用 HOC/render props)
- Vue:Composition API + 插槽
- Svelte:Actions + 插槽
步骤 2.3:选择状态管理策略
组件级状态:
- UI 专属需求(模态框、表单、切换控件)使用本地状态
- 仅在同级组件需要共享状态时才提升状态
- 使用派生状态,而不是重复存储状态
应用级状态:
- React:Context + useReducer、Zustand、Jotai
- Vue:Pinia(Vue 3)、Vuex(Vue 2)
- Svelte:Stores、Context API
状态管理决策树请参阅 references/component-patterns.md。
阶段 3:实现
步骤 3.1:定义组件接口
从定义 props 的 TypeScript 接口开始。
React 示例:
import { ReactNode } from 'react';
interface ButtonProps {
/** Button content */
children: ReactNode;
/** Visual style variant */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** Size preset */
size?: 'sm' | 'md' | 'lg';
/** Loading state */
isLoading?: boolean;
/** Disabled state */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
/** Additional CSS classes */
className?: string;
}
最佳实践:
- 使用 JSDoc 注释说明所有 props
- 使用可辨识联合类型定义变体 props
- 使用
?明确标记可选 props - 提供合理的默认值
- 避免使用
any类型
Vue 3 示例:
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {
variant: {
type: String as PropType<'primary' | 'secondary' | 'ghost' | 'danger'>,
default: 'primary',
},
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
default: 'md',
},
isLoading: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
});
Svelte 示例:
// Button.svelte (Svelte 4)
<script lang="ts">
export let variant: 'primary' | 'secondary' | 'ghost' | 'danger' = 'primary';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let isLoading = false;
export let disabled = false;
</script>
// Button.svelte (Svelte 5 - using $props rune)
<script lang="ts">
let { variant = 'primary', size = 'md', isLoading = false, disabled = false }: {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
disabled?: boolean;
} = $props();
</script>
步骤 3.2:实现组件逻辑
遵循特定框架的组件实现模式。
使用组合的 React:
import { forwardRef } from 'react';
import { cn } from '@/lib/utils';
import { Spinner } from './Spinner';
const buttonVariants = {
variant: {
primary: 'bg-blue-600 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
ghost: 'hover:bg-gray-100 text-gray-700',
danger: 'bg-red-600 hover:bg-red-700 text-white',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
children,
variant = 'primary',
size = 'md',
isLoading = false,
disabled = false,
className,
...props
},
ref
) => {
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
'transition-colors focus-visible:outline-none focus-visible:ring-2',
'disabled:pointer-events-none disabled:opacity-50',
buttonVariants.variant[variant],
buttonVariants.size[size],
className
)}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<>
<Spinner className="mr-2 h-4 w-4" aria-hidden />
<span>Loading...</span>
</>
) : (
children
)}
</button>
);
}
);
Button.displayName = 'Button';
关键模式:
- 使用
forwardRef暴露 DOM 引用 - 展开
...props,提高灵活性 - 使用工具函数组合 classNames
- 处理加载和禁用状态
- 添加 ARIA 属性以确保无障碍访问
步骤 3.3:组织组件文件
创建组件目录,并采用合适的文件结构。
标准结构:
components/
Button/
Button.tsx # Component implementation
Button.test.tsx # Unit tests
Button.stories.tsx # Storybook stories (if using)
index.tsx # Barrel export
types.ts # Type definitions (if complex)
使用精确工具实现:
precision_write:
files:
- path: "src/components/Button/Button.tsx"
content: |
import { forwardRef } from 'react';
// ... [full implementation]
- path: "src/components/Button/index.tsx"
content: |
export { Button } from './Button';
export type { ButtonProps } from './Button';
- path: "src/components/Button/Button.test.tsx"
content: |
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
// ... [test cases]
verbosity: count_only
阶段 4:状态管理
步骤 4.1:确定状态作用范围
本地状态(仅组件内):
- 表单输入
- 模态框打开/关闭
- 下拉菜单展开/折叠
- 加载状态
提升的状态(由父组件管理):
- 跨字段的表单验证
- 多步骤向导状态
- 具有单项展开行为的手风琴组件
全局状态(应用级):
- 用户身份验证
- 主题偏好
- 购物车
- 通知
步骤 4.2:实现状态模式
React - 本地状态:
import { useState } from 'react';
interface SearchResult {
id: string;
title: string;
description: string;
}
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const handleSearch = async () => {
const data = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
setResults(await data.json());
};
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</>
);
}
React - 提升的状态:
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
return (
<>
<SearchInput query={query} onQueryChange={setQuery} />
<SearchResults results={results} />
</>
);
}
React - 派生状态:
interface Item {
id: string;
category: string;
name: string;
}
interface FilteredListProps {
items: Item[];
filter: string;
}
function FilteredList({ items, filter }: FilteredListProps) {
// Don't store filtered items in state - derive them
const filteredItems = items.filter(item => item.category === filter);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}
步骤 4.3:避免逐层传递 props
使用 Context 传递深层嵌套的 props:
import { createContext, useContext } from 'react';
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
阶段 5:性能优化
步骤 5.1:识别渲染问题
使用分析工具检测性能问题。
mcp__plugin_goodvibes_frontend-engine__trace_component_state:
component_file: "src/components/Dashboard.tsx"
target_component: "Dashboard"
mcp__plugin_goodvibes_frontend-engine__analyze_render_triggers:
component_file: "src/components/ExpensiveList.tsx"
这些工具可以揭示:
- 组件不必要地重新渲染
- 状态更新引发级联渲染
- 每次父组件渲染时 props 都发生变化
步骤 5.2:应用记忆化
记忆化开销较大的计算:
import { useMemo } from 'react';
interface DataItem {
id: string;
[key: string]: unknown;
}
interface DataTableProps {
data: DataItem[];
filters: Record<string, any>;
}
function DataTable({ data, filters }: DataTableProps) {
const filteredData = useMemo(() => {
return data.filter(item => matchesFilters(item, filters));
}, [data, filters]);
return <table>{/* render filteredData */}</table>;
}
记忆化回调函数:
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Prevent Child re-render when count changes
const handleClick = useCallback(() => {
onClick();
}, []);
return <Child onClick={handleClick} />;
}
记忆化组件:
import { memo } from 'react';
interface ExpensiveChildProps {
data: DataItem[];
}
const ExpensiveChild = memo(function ExpensiveChild({ data }: ExpensiveChildProps) {
// Only re-renders if data changes
return <div>{/* complex rendering */}</div>;
});
步骤 5.3:实现虚拟化
对于大型列表,请使用虚拟化库。
import { useVirtualizer } from '@tanstack/react-virtual';
interface VirtualListProps<T = unknown> {
items: T[];
}
function VirtualList({ items }: VirtualListProps) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
// Note: Inline styles acceptable here for virtualization positioning
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);
}
步骤 5.4:延迟加载组件
React 延迟加载:
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
阶段 6:无障碍访问
步骤 6.1:语义化 HTML
使用合适的 HTML 元素,而不是 div。
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
步骤 6.2:ARIA 属性
添加 ARIA 标签,方便屏幕阅读器使用。
interface DialogProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
}
function Dialog({ isOpen, onClose, children }: DialogProps) {
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<div id="dialog-description">{children}</div>
<button onClick={onClose} aria-label="Close dialog">
X
</button>
</div>
);
}
步骤 6.3:键盘导航
确保所有交互元素都可通过键盘操作。
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
if (e.key === 'Escape') setIsOpen(false);
if (e.key === 'Enter' || e.key === ' ') setIsOpen(!isOpen);
};
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
onKeyDown={handleKeyDown}
aria-expanded={isOpen}
aria-haspopup="true"
>
Menu
</button>
{isOpen && <div role="menu">{/* menu items */}</div>}
</div>
);
}
步骤 6.4:验证无障碍访问
使用前端分析工具检查无障碍访问情况。
mcp__plugin_goodvibes_frontend-engine__get_accessibility_tree:
component_file: "src/components/Dialog.tsx"
阶段 7:验证
步骤 7.1:类型检查
验证 TypeScript 编译结果。
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
步骤 7.2:运行组件验证脚本
使用验证脚本确保质量。
bash plugins/goodvibes/skills/outcome/component-architecture/scripts/validate-components.sh .
完整的验证套件请参见 scripts/validate-components.sh。
步骤 7.3:视觉回归测试
如果使用 Storybook 或类似工具,请运行视觉测试。
precision_exec:
commands:
- cmd: "npm run test:visual"
expect:
exit_code: 0
verbosity: minimal
常见反模式
不要:
- 存储派生状态(从现有状态计算)
- 直接修改 props 或状态
- 将索引用作动态列表的 key
- 在渲染过程中创建新对象或函数
- 跳过 props 验证或 TypeScript 类型定义
- 将
any类型用于组件 props - 混合表现层与业务逻辑
- 忽略无障碍访问(ARIA、键盘导航)
- 过度优化(过早进行记忆化)
应该:
- 尽可能根据 props 派生状态
- 将 props 和状态视为不可变数据
- 为列表项使用稳定且唯一的 key
- 在渲染函数外定义函数,或使用
useCallback - 使用 TypeScript 定义严格的 props 类型
- 将容器组件(逻辑)与表现组件分开
- 为自定义组件添加 ARIA 属性
- 优化前先进行测量(React DevTools Profiler)
快速参考
发现阶段:
discover: { queries: [components, patterns, state, styling], verbosity: files_only }
precision_read: { files: [example components], extract: symbols }
实现阶段:
precision_write: { files: [Component.tsx, index.tsx, types.ts], verbosity: count_only }
性能分析:
trace_component_state: { component_file: "src/...", target_component: "Name" }
analyze_render_triggers: { component_file: "src/..." }
无障碍检查:
get_accessibility_tree: { component_file: "src/..." }
验证阶段:
precision_exec: { commands: [{ cmd: "npm run typecheck" }] }
实施后:
bash scripts/validate-components.sh .
如需了解详细模式、框架对比和决策树,请参阅 references/component-patterns.md。
相关技能
可考虑使用以下互补的 GoodVibes 技能:
- styling-system - 设计系统令牌、主题架构和 CSS-in-JS 模式
- state-management - 全局状态模式、存储架构和数据流
- testing-strategy - 组件测试、视觉回归和无障碍测试
- performance-audit - 包分析、渲染性能分析和优化策略
- accessibility-audit - WCAG 合规性、屏幕阅读器测试和 ARIA 模式