Web组件设计Skill web-component-design

这个技能专注于使用React、Vue和Svelte等现代前端框架设计和实现可重用的UI组件,涵盖CSS-in-JS样式方法、组件组合模式(如复合组件、渲染属性)、API设计等,适用于构建组件库、前端设计系统和提升代码可维护性。关键词:React, Vue, Svelte, CSS-in-JS, 组件设计, 前端开发, UI组件, 可重用架构, 组合模式, 样式管理。

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

name: web-component-design description: 掌握React、Vue和Svelte组件模式,包括CSS-in-JS、组合策略和可重用组件架构。适用于构建UI组件库、设计组件API或实现前端设计系统时使用。

Web组件设计

使用现代框架和清晰的组合模式及样式方法构建可重用、可维护的UI组件。

何时使用此技能

  • 设计可重用组件库或设计系统
  • 实现复杂组件组合模式
  • 选择并应用CSS-in-JS解决方案
  • 构建可访问、响应式的UI组件
  • 在代码库中创建一致的组件API
  • 将遗留组件重构为现代模式
  • 实现复合组件或渲染属性

核心概念

1. 组件组合模式

复合组件:相关组件协同工作

// 用法
<Select value={value} onChange={setValue}>
  <Select.Trigger>选择选项</Select.Trigger>
  <Select.Options>
    <Select.Option value="a">选项A</Select.Option>
    <Select.Option value="b">选项B</Select.Option>
  </Select.Options>
</Select>

渲染属性:将渲染委托给父组件

<DataFetcher url="/api/users">
  {({ data, loading, error }) =>
    loading ? <Spinner /> : <UserList users={data} />
  }
</DataFetcher>

插槽(Vue/Svelte):命名的内容注入点

<template>
  <Card>
    <template #header>标题</template>
    <template #content>正文</template>
    <template #footer><Button>操作</Button></template>
  </Card>
</template>

2. CSS-in-JS方法

解决方案 方法 最佳适用场景
Tailwind CSS 实用工具类 快速原型设计、设计系统
CSS Modules 作用域CSS文件 现有CSS、逐步采用
styled-components 模板字符串 React、动态样式
Emotion 对象/模板样式 灵活、SSR友好
Vanilla Extract 零运行时 性能关键应用

3. 组件API设计

interface ButtonProps {
  variant?: "primary" | "secondary" | "ghost";
  size?: "sm" | "md" | "lg";
  isLoading?: boolean;
  isDisabled?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  children: React.ReactNode;
  onClick?: () => void;
}

原则

  • 使用语义化的属性名(isLoading 而非 loading
  • 提供合理的默认值
  • 通过 children 支持组合
  • 允许通过 classNamestyle 覆盖样式

快速入门:使用Tailwind的React组件

import { forwardRef, type ComponentPropsWithoutRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "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",
  {
    variants: {
      variant: {
        primary: "bg-blue-600 text-white hover:bg-blue-700",
        secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
        ghost: "hover:bg-gray-100 hover:text-gray-900",
      },
      size: {
        sm: "h-8 px-3 text-sm",
        md: "h-10 px-4 text-sm",
        lg: "h-12 px-6 text-base",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  },
);

interface ButtonProps
  extends
    ComponentPropsWithoutRef<"button">,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, isLoading, children, ...props }, ref) => (
    <button
      ref={ref}
      className={cn(buttonVariants({ variant, size }), className)}
      disabled={isLoading || props.disabled}
      {...props}
    >
      {isLoading && <Spinner className="mr-2 h-4 w-4" />}
      {children}
    </button>
  ),
);
Button.displayName = "Button";

框架模式

React:复合组件

import { createContext, useContext, useState, type ReactNode } from "react";

interface AccordionContextValue {
  openItems: Set<string>;
  toggle: (id: string) => void;
}

const AccordionContext = createContext<AccordionContextValue | null>(null);

function useAccordion() {
  const context = useContext(AccordionContext);
  if (!context) throw new Error("必须在Accordion内使用");
  return context;
}

export function Accordion({ children }: { children: ReactNode }) {
  const [openItems, setOpenItems] = useState<Set<string>>(new Set());

  const toggle = (id: string) => {
    setOpenItems((prev) => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  };

  return (
    <AccordionContext.Provider value={{ openItems, toggle }}>
      <div className="divide-y">{children}</div>
    </AccordionContext.Provider>
  );
}

Accordion.Item = function AccordionItem({
  id,
  title,
  children,
}: {
  id: string;
  title: string;
  children: ReactNode;
}) {
  const { openItems, toggle } = useAccordion();
  const isOpen = openItems.has(id);

  return (
    <div>
      <button onClick={() => toggle(id)} className="w-full text-left py-3">
        {title}
      </button>
      {isOpen && <div className="pb-3">{children}</div>}
    </div>
  );
};

Vue 3:组合式API

<script setup lang="ts">
import { ref, computed, provide, inject, type InjectionKey } from "vue";

interface TabsContext {
  activeTab: Ref<string>;
  setActive: (id: string) => void;
}

const TabsKey: InjectionKey<TabsContext> = Symbol("tabs");

// 父组件
const activeTab = ref("tab-1");
provide(TabsKey, {
  activeTab,
  setActive: (id: string) => {
    activeTab.value = id;
  },
});

// 子组件使用
const tabs = inject(TabsKey);
const isActive = computed(() => tabs?.activeTab.value === props.id);
</script>

Svelte 5:运行

<script lang="ts">
  interface Props {
    variant?: 'primary' | 'secondary';
    size?: 'sm' | 'md' | 'lg';
    onclick?: () => void;
    children: import('svelte').Snippet;
  }

  let { variant = 'primary', size = 'md', onclick, children }: Props = $props();

  const classes = $derived(
    `btn btn-${variant} btn-${size}`
  );
</script>

<button class={classes} {onclick}>
  {@render children()}
</button>

最佳实践

  1. 单一职责:每个组件专注于一件事
  2. 避免属性透传:使用上下文处理深层嵌套数据
  3. 默认可访问:包含ARIA属性、键盘支持
  4. 受控与非受控:在适当时支持两种模式
  5. 转发引用:允许父组件访问DOM节点
  6. 记忆化:使用React.memouseMemo处理昂贵渲染
  7. 错误边界:包装可能失败的组件

常见问题

  • 属性爆炸:属性过多 - 考虑使用组合替代
  • 样式冲突:使用作用域样式或CSS Modules
  • 重渲染级联:使用React DevTools分析,适当记忆化
  • 可访问性差距:用屏幕阅读器和键盘导航测试
  • 包大小:摇树优化未使用的组件变体

资源