原子设计集成Skill atomic-design-integration

这个技能用于将原子设计方法论与现代前端框架如 React、Vue、Angular 集成,提供项目结构、组件模板、测试和配置的最佳实践,关键词包括:原子设计、前端开发、React、Vue、Angular、组件集成、项目结构、Storybook、测试。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: 原子设计集成 用户可调用: false 描述: 当将原子设计方法论与 React、Vue、Angular 或其他框架集成时使用。框架特定的实现模式。 允许工具:

  • Bash
  • Read
  • Write
  • Edit
  • Glob
  • Grep

原子设计:框架集成

掌握原子设计方法论与现代前端框架的集成。此技能涵盖 React、Vue、Angular 以及实现原子组件层次结构的一般模式。

React 集成

项目结构

src/
  components/
    atoms/
      Button/
        Button.tsx
        Button.module.css
        Button.test.tsx
        Button.stories.tsx
        index.ts
      index.ts                 # 桶导出
    molecules/
      FormField/
        FormField.tsx
        FormField.module.css
        FormField.test.tsx
        index.ts
      index.ts
    organisms/
      Header/
        Header.tsx
        Header.module.css
        useHeader.ts           # 自定义钩子
        index.ts
      index.ts
    templates/
      MainLayout/
        MainLayout.tsx
        MainLayout.module.css
        index.ts
      index.ts
    index.ts                   # 主桶导出
  pages/                       # Next.js 或页面组件
    HomePage/
      HomePage.tsx
      index.ts

桶导出模式

// components/atoms/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';

export { Input } from './Input';
export type { InputProps } from './Input';

export { Label } from './Label';
export type { LabelProps } from './Label';

export { Icon } from './Icon';
export type { IconProps } from './Icon';

// components/index.ts
export * from './atoms';
export * from './molecules';
export * from './organisms';
export * from './templates';

组件模板(React/TypeScript)

// components/atoms/Button/Button.tsx
import React, { forwardRef } from 'react';
import type { ButtonHTMLAttributes } from 'react';
import styles from './Button.module.css';
import { clsx } from 'clsx';

export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonSize = 'sm' | 'md' | 'lg';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
  fullWidth?: boolean;
  isLoading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      variant = 'primary',
      size = 'md',
      fullWidth = false,
      isLoading = false,
      leftIcon,
      rightIcon,
      disabled,
      children,
      className,
      ...props
    },
    ref
  ) => {
    return (
      <button
        ref={ref}
        className={clsx(
          styles.button,
          styles[variant],
          styles[size],
          fullWidth && styles.fullWidth,
          isLoading && styles.loading,
          className
        )}
        disabled={disabled || isLoading}
        aria-busy={isLoading}
        {...props}
      >
        {isLoading ? (
          <span className={styles.spinner} aria-hidden="true" />
        ) : (
          <>
            {leftIcon && <span className={styles.leftIcon}>{leftIcon}</span>}
            {children}
            {rightIcon && <span className={styles.rightIcon}>{rightIcon}</span>}
          </>
        )}
      </button>
    );
  }
);

Button.displayName = 'Button';

原子设计的自定义钩子

// organisms/Header/useHeader.ts
import { useState, useCallback } from 'react';

interface UseHeaderOptions {
  user?: { id: string; name: string };
  onLogout?: () => void;
}

export function useHeader({ user, onLogout }: UseHeaderOptions) {
  const [menuOpen, setMenuOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');

  const toggleMenu = useCallback(() => {
    setMenuOpen((prev) => !prev);
  }, []);

  const handleSearch = useCallback((query: string) => {
    setSearchQuery(query);
    // 可以在这里触发搜索操作
  }, []);

  const handleLogout = useCallback(() => {
    setMenuOpen(false);
    onLogout?.();
  }, [onLogout]);

  return {
    menuOpen,
    toggleMenu,
    searchQuery,
    handleSearch,
    handleLogout,
    isAuthenticated: !!user,
  };
}

设计令牌的上下文

// contexts/ThemeContext.tsx
import React, { createContext, useContext, useState } from 'react';

interface Theme {
  colors: {
    primary: string;
    secondary: string;
    background: string;
    text: string;
  };
  spacing: {
    xs: string;
    sm: string;
    md: string;
    lg: string;
  };
}

const defaultTheme: Theme = {
  colors: {
    primary: '#2196f3',
    secondary: '#f50057',
    background: '#ffffff',
    text: '#212121',
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
  },
};

const ThemeContext = createContext<{
  theme: Theme;
  setTheme: (theme: Theme) => void;
}>({
  theme: defaultTheme,
  setTheme: () => {},
});

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [theme, setTheme] = useState(defaultTheme);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => useContext(ThemeContext);

Vue 集成

项目结构(Vue 3)

src/
  components/
    atoms/
      VButton/
        VButton.vue
        VButton.spec.ts
        index.ts
      VInput/
      index.ts
    molecules/
      VFormField/
        VFormField.vue
        useFormField.ts
        index.ts
      index.ts
    organisms/
      VHeader/
        VHeader.vue
        index.ts
      index.ts
    templates/
      MainLayout/
        MainLayout.vue
        index.ts

Vue 组件模板

<!-- components/atoms/VButton/VButton.vue -->
<template>
  <button
    :class="buttonClasses"
    :disabled="disabled || loading"
    :aria-busy="loading"
    v-bind="$attrs"
  >
    <span v-if="loading" class="spinner" aria-hidden="true" />
    <template v-else>
      <span v-if="$slots.leftIcon" class="left-icon">
        <slot name="leftIcon" />
      </span>
      <slot />
      <span v-if="$slots.rightIcon" class="right-icon">
        <slot name="rightIcon" />
      </span>
    </template>
  </button>
</template>

<script setup lang="ts">
import { computed } from 'vue';

export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'tertiary';
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
  loading?: boolean;
  disabled?: boolean;
}

const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'md',
  fullWidth: false,
  loading: false,
  disabled: false,
});

const buttonClasses = computed(() => [
  'btn',
  `btn-${props.variant}`,
  `btn-${props.size}`,
  { 'btn-full': props.fullWidth, 'btn-loading': props.loading },
]);
</script>

<style scoped>
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: none;
  border-radius: 6px;
  font-weight: 500;
  cursor: pointer;
  transition: all 150ms ease;
}

.btn-primary {
  background-color: var(--color-primary);
  color: white;
}

.btn-sm {
  padding: 6px 12px;
  font-size: 14px;
}

.btn-md {
  padding: 8px 16px;
  font-size: 16px;
}

.btn-lg {
  padding: 12px 24px;
  font-size: 18px;
}

.btn-full {
  width: 100%;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

Vue 可组合函数(钩子)

// components/organisms/VHeader/useHeader.ts
import { ref, computed } from 'vue';
import type { Ref } from 'vue';

interface User {
  id: string;
  name: string;
}

export function useHeader(user: Ref<User | null>) {
  const menuOpen = ref(false);
  const searchQuery = ref('');

  const isAuthenticated = computed(() => !!user.value);

  const toggleMenu = () => {
    menuOpen.value = !menuOpen.value;
  };

  const handleSearch = (query: string) => {
    searchQuery.value = query;
  };

  return {
    menuOpen,
    searchQuery,
    isAuthenticated,
    toggleMenu,
    handleSearch,
  };
}

Angular 集成

项目结构(Angular)

src/
  app/
    components/
      atoms/
        button/
          button.component.ts
          button.component.html
          button.component.scss
          button.component.spec.ts
        input/
        atoms.module.ts
      molecules/
        form-field/
          form-field.component.ts
          form-field.component.html
        molecules.module.ts
      organisms/
        header/
          header.component.ts
          header.service.ts
        organisms.module.ts
      templates/
        main-layout/
          main-layout.component.ts
        templates.module.ts
    pages/
      home/
        home.component.ts

Angular 组件模板

// components/atoms/button/button.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

export type ButtonVariant = 'primary' | 'secondary' | 'tertiary';
export type ButtonSize = 'sm' | 'md' | 'lg';

@Component({
  selector: 'app-button',
  standalone: true,
  imports: [CommonModule],
  template: `
    <button
      [class]="buttonClasses"
      [disabled]="disabled || loading"
      [attr.aria-busy]="loading"
      (click)="onClick.emit($event)"
    >
      <span *ngIf="loading" class="spinner" aria-hidden="true"></span>
      <ng-container *ngIf="!loading">
        <ng-content select="[leftIcon]"></ng-content>
        <ng-content></ng-content>
        <ng-content select="[rightIcon]"></ng-content>
      </ng-container>
    </button>
  `,
  styleUrls: ['./button.component.scss'],
})
export class ButtonComponent {
  @Input() variant: ButtonVariant = 'primary';
  @Input() size: ButtonSize = 'md';
  @Input() fullWidth = false;
  @Input() loading = false;
  @Input() disabled = false;

  @Output() onClick = new EventEmitter<MouseEvent>();

  get buttonClasses(): string {
    return [
      'btn',
      `btn-${this.variant}`,
      `btn-${this.size}`,
      this.fullWidth ? 'btn-full' : '',
      this.loading ? 'btn-loading' : '',
    ]
      .filter(Boolean)
      .join(' ');
  }
}

Angular 模块组织

// components/atoms/atoms.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from './button/button.component';
import { InputComponent } from './input/input.component';
import { LabelComponent } from './label/label.component';
import { IconComponent } from './icon/icon.component';

@NgModule({
  imports: [CommonModule],
  declarations: [
    ButtonComponent,
    InputComponent,
    LabelComponent,
    IconComponent,
  ],
  exports: [ButtonComponent, InputComponent, LabelComponent, IconComponent],
})
export class AtomsModule {}

// components/molecules/molecules.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AtomsModule } from '../atoms/atoms.module';
import { FormFieldComponent } from './form-field/form-field.component';
import { SearchFormComponent } from './search-form/search-form.component';

@NgModule({
  imports: [CommonModule, AtomsModule],
  declarations: [FormFieldComponent, SearchFormComponent],
  exports: [FormFieldComponent, SearchFormComponent],
})
export class MoleculesModule {}

Storybook 集成

故事文件模板

// components/atoms/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { Icon } from '../Icon';

const meta: Meta<typeof Button> = {
  title: 'Atoms/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'tertiary'],
      description: '视觉样式变体',
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
      description: '按钮大小',
    },
    fullWidth: {
      control: 'boolean',
      description: '是否按钮占满宽度',
    },
    isLoading: {
      control: 'boolean',
      description: '加载状态',
    },
    disabled: {
      control: 'boolean',
      description: '禁用状态',
    },
  },
  args: {
    children: 'Button',
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
  },
};

export const WithIcons: Story = {
  args: {
    leftIcon: <Icon name="plus" size="sm" />,
    children: '添加项目',
  },
};

export const Loading: Story = {
  args: {
    isLoading: true,
    children: '提交中...',
  },
};

export const AllVariants: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap' }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="tertiary">Tertiary</Button>
    </div>
  ),
};

export const AllSizes: Story = {
  render: () => (
    <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
};

Storybook 配置

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/components/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
  docs: {
    autodocs: 'tag',
  },
};

export default config;

Storybook 层次结构

// 按原子级别组织故事
// 标题格式:"级别/组件名称"

// 原子
title: 'Atoms/Button'
title: 'Atoms/Input'
title: 'Atoms/Icon'

// 分子
title: 'Molecules/FormField'
title: 'Molecules/SearchForm'

// 有机体
title: 'Organisms/Header'
title: 'Organisms/Footer'

// 模板
title: 'Templates/MainLayout'
title: 'Templates/DashboardLayout'

// 页面(示例组合)
title: 'Pages/HomePage'

测试集成

单元测试模板(React/Vitest)

// components/atoms/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  describe('渲染', () => {
    it('正确渲染子元素', () => {
      render(<Button>点击我</Button>);
      expect(screen.getByRole('button')).toHaveTextContent('点击我');
    });

    it('正确应用变体类', () => {
      render(<Button variant="secondary">Button</Button>);
      expect(screen.getByRole('button')).toHaveClass('secondary');
    });

    it('正确应用大小类', () => {
      render(<Button size="lg">Button</Button>);
      expect(screen.getByRole('button')).toHaveClass('lg');
    });
  });

  describe('状态', () => {
    it('当 disabled prop 为 true 时禁用按钮', () => {
      render(<Button disabled>Button</Button>);
      expect(screen.getByRole('button')).toBeDisabled();
    });

    it('加载时禁用按钮', () => {
      render(<Button isLoading>Button</Button>);
      expect(screen.getByRole('button')).toBeDisabled();
      expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
    });

    it('加载时显示旋转器', () => {
      render(<Button isLoading>Button</Button>);
      expect(screen.getByRole('button').querySelector('.spinner')).toBeInTheDocument();
    });
  });

  describe('交互', () => {
    it('点击时调用 onClick 处理程序', () => {
      const handleClick = vi.fn();
      render(<Button onClick={handleClick}>Button</Button>);

      fireEvent.click(screen.getByRole('button'));

      expect(handleClick).toHaveBeenCalledTimes(1);
    });

    it('禁用时不调用 onClick', () => {
      const handleClick = vi.fn();
      render(<Button onClick={handleClick} disabled>Button</Button>);

      fireEvent.click(screen.getByRole('button'));

      expect(handleClick).not.toHaveBeenCalled();
    });
  });

  describe('可访问性', () => {
    it('没有可访问性违规', async () => {
      const { container } = render(<Button>可访问按钮</Button>);
      // 如果使用 axe-core
      // const results = await axe(container);
      // expect(results).toHaveNoViolations();
    });

    it('正确转发 ref', () => {
      const ref = { current: null };
      render(<Button ref={ref}>Button</Button>);
      expect(ref.current).toBeInstanceOf(HTMLButtonElement);
    });
  });
});

集成测试模板

// components/organisms/Header/Header.integration.test.tsx
import { render, screen, fireEvent, within } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Header } from './Header';

const mockNavigation = [
  { id: 'home', label: '首页', href: '/' },
  { id: 'products', label: '产品', href: '/products' },
  { id: 'about', label: '关于', href: '/about' },
];

const mockUser = {
  id: '1',
  name: '张三',
  email: 'zhangsan@example.com',
};

describe('Header 集成', () => {
  it('正确渲染导航项', () => {
    render(
      <Header
        logo={<span>Logo</span>}
        navigation={mockNavigation}
      />
    );

    const nav = screen.getByRole('navigation');
    expect(within(nav).getByText('首页')).toBeInTheDocument();
    expect(within(nav).getByText('产品')).toBeInTheDocument();
    expect(within(nav).getByText('关于')).toBeInTheDocument();
  });

  it('无用户时显示登录按钮', () => {
    render(
      <Header
        logo={<span>Logo</span>}
        navigation={mockNavigation}
        onLogin={vi.fn()}
      />
    );

    expect(screen.getByRole('button', { name: /登录/i })).toBeInTheDocument();
  });

  it('认证时显示用户菜单', () => {
    render(
      <Header
        logo={<span>Logo</span>}
        navigation={mockNavigation}
        user={mockUser}
      />
    );

    expect(screen.getByText('张三')).toBeInTheDocument();
  });

  it('点击注销时调用 onLogout', async () => {
    const handleLogout = vi.fn();

    render(
      <Header
        logo={<span>Logo</span>}
        navigation={mockNavigation}
        user={mockUser}
        onLogout={handleLogout}
      />
    );

    // 打开用户菜单
    fireEvent.click(screen.getByText('张三'));

    // 点击注销
    fireEvent.click(screen.getByText('注销'));

    expect(handleLogout).toHaveBeenCalledTimes(1);
  });
});

路径别名配置

TypeScript (tsconfig.json)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["src/components/*"],
      "@/components": ["src/components/index.ts"],
      "@/atoms/*": ["src/components/atoms/*"],
      "@/molecules/*": ["src/components/molecules/*"],
      "@/organisms/*": ["src/components/organisms/*"],
      "@/templates/*": ["src/components/templates/*"],
      "@/hooks/*": ["src/hooks/*"],
      "@/utils/*": ["src/utils/*"],
      "@/design-tokens/*": ["src/design-tokens/*"]
    }
  }
}

Vite 配置

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@/components': path.resolve(__dirname, './src/components'),
      '@/atoms': path.resolve(__dirname, './src/components/atoms'),
      '@/molecules': path.resolve(__dirname, './src/components/molecules'),
      '@/organisms': path.resolve(__dirname, './src/components/organisms'),
      '@/templates': path.resolve(__dirname, './src/components/templates'),
    },
  },
});

最佳实践

1. 一致的导出模式

// 每个组件文件夹应该有 index.ts
// atoms/Button/index.ts
export { Button } from './Button';
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';

2. 清晰的属性类型

// 始终导出用于组合的属性接口
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant;
  size?: ButtonSize;
}

3. 框架无关的逻辑

// 提取可跨框架共享的逻辑
// utils/formatPrice.ts
export function formatPrice(amount: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

何时使用此技能

  • 在新项目中设置原子设计
  • 将现有组件迁移到原子结构
  • 集成 Storybook 文档
  • 配置测试基础设施
  • 建立框架特定模式

相关技能

  • atomic-design-fundamentals - 核心方法论概述
  • atomic-design-atoms - 创建原子组件
  • atomic-design-molecules - 组合原子为分子
  • atomic-design-organisms - 构建复杂有机体
  • atomic-design-templates - 无内容的页面布局