mcpField
C
component-architecture

component-architecture

github.com/mgd34msu开发660 次安装10 周前更新
代码仓库

关于 component-architecture

面向 React、Vue 和 Svelte 的 UI 组件架构设计、实现与审查技能,涵盖代码探索、组合与状态策略、性能、无障碍性及验证。

主要功能

  1. 01梳理现有组件结构与模式
  2. 02指导选择组合方式和状态管理
  3. 03涵盖组件组织与实现
  4. 04处理渲染性能和无障碍性
  5. 05提供组件验证步骤

适用场景

  1. 01构建可复用 UI 组件
  2. 02重构组件层级结构
  3. 03选择状态范围与组合模式
  4. 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 模式

评论

支持 Markdown