component-library
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseComponent Library Building Skill
组件库构建技能
Overview
概述
Learn to build reusable, accessible, and well-documented component libraries for React applications using modern design patterns and tools.
学习使用现代设计模式和工具为React应用构建可复用、可访问且文档完善的组件库。
Learning Objectives
学习目标
- Design reusable component APIs
- Implement accessible components (WCAG 2.1)
- Create component variants and compositions
- Build documentation with Storybook
- Publish and maintain npm packages
- 设计可复用的组件API
- 实现符合WCAG 2.1标准的可访问组件
- 创建组件变体和组合
- 使用Storybook构建文档
- 发布并维护npm包
Component Design Principles
组件设计原则
1. Flexible and Composable
1. 灵活且可组合
jsx
// Good: Flexible API
<Card>
<Card.Header>
<Card.Title>Title</Card.Title>
</Card.Header>
<Card.Body>Content</Card.Body>
<Card.Footer>
<Button>Action</Button>
</Card.Footer>
</Card>
// Bad: Rigid API
<Card title="Title" content="Content" action="Action" />jsx
// 推荐:灵活的API
<Card>
<Card.Header>
<Card.Title>Title</Card.Title>
</Card.Header>
<Card.Body>Content</Card.Body>
<Card.Footer>
<Button>Action</Button>
</Card.Footer>
</Card>
// 不推荐:僵化的API
<Card title="Title" content="Content" action="Action" />2. Accessible by Default
2. 默认支持可访问性
jsx
function Button({ children, onClick, disabled, ...props }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
{...props}
>
{children}
</button>
);
}jsx
function Button({ children, onClick, disabled, ...props }) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
{...props}
>
{children}
</button>
);
}Essential Components
核心组件
Button Component
Button组件
jsx
const Button = forwardRef(({
children,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
leftIcon,
rightIcon,
...props
}, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant} btn-${size}`}
disabled={disabled || loading}
aria-busy={loading}
{...props}
>
{leftIcon && <span className="btn-icon-left">{leftIcon}</span>}
{loading ? <Spinner size="sm" /> : children}
{rightIcon && <span className="btn-icon-right">{rightIcon}</span>}
</button>
);
});
Button.displayName = 'Button';jsx
const Button = forwardRef(({
children,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
leftIcon,
rightIcon,
...props
}, ref) => {
return (
<button
ref={ref}
className={`btn btn-${variant} btn-${size}`}
disabled={disabled || loading}
aria-busy={loading}
{...props}
>
{leftIcon && <span className="btn-icon-left">{leftIcon}</span>}
{loading ? <Spinner size="sm" /> : children}
{rightIcon && <span className="btn-icon-right">{rightIcon}</span>}
</button>
);
});
Button.displayName = 'Button';Input Component
Input组件
jsx
const Input = forwardRef(({
label,
error,
helper,
required,
...props
}, ref) => {
const id = useId();
return (
<div className="input-wrapper">
{label && (
<label htmlFor={id}>
{label}
{required && <span aria-label="required">*</span>}
</label>
)}
<input
ref={ref}
id={id}
aria-invalid={!!error}
aria-describedby={error ? `${id}-error` : helper ? `${id}-helper` : undefined}
{...props}
/>
{helper && <span id={`${id}-helper`} className="input-helper">{helper}</span>}
{error && <span id={`${id}-error`} className="input-error" role="alert">{error}</span>}
</div>
);
});jsx
const Input = forwardRef(({
label,
error,
helper,
required,
...props
}, ref) => {
const id = useId();
return (
<div className="input-wrapper">
{label && (
<label htmlFor={id}>
{label}
{required && <span aria-label="required">*</span>}
</label>
)}
<input
ref={ref}
id={id}
aria-invalid={!!error}
aria-describedby={error ? `${id}-error` : helper ? `${id}-helper` : undefined}
{...props}
/>
{helper && <span id={`${id}-helper`} className="input-helper">{helper}</span>}
{error && <span id={`${id}-error`} className="input-error" role="alert">{error}</span>}
</div>
);
});Modal Component
Modal组件
jsx
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
const previousActiveElement = document.activeElement;
modalRef.current?.focus();
return () => {
previousActiveElement?.focus();
};
}
}, [isOpen]);
useOnClickOutside(modalRef, onClose);
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div ref={modalRef} className="modal" tabIndex={-1}>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Close modal">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.body
);
}jsx
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen) {
const previousActiveElement = document.activeElement;
modalRef.current?.focus();
return () => {
previousActiveElement?.focus();
};
}
}, [isOpen]);
useOnClickOutside(modalRef, onClose);
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div ref={modalRef} className="modal" tabIndex={-1}>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button onClick={onClose} aria-label="Close modal">×</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>,
document.body
);
}Dropdown Component
Dropdown组件
jsx
function Dropdown({ trigger, children }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef} className="dropdown">
<div onClick={() => setIsOpen(!isOpen)} role="button" aria-expanded={isOpen}>
{trigger}
</div>
{isOpen && (
<div className="dropdown-menu" role="menu">
{children}
</div>
)}
</div>
);
}
function DropdownItem({ onClick, children }) {
return (
<div className="dropdown-item" role="menuitem" onClick={onClick} tabIndex={0}>
{children}
</div>
);
}
Dropdown.Item = DropdownItem;jsx
function Dropdown({ trigger, children }) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef} className="dropdown">
<div onClick={() => setIsOpen(!isOpen)} role="button" aria-expanded={isOpen}>
{trigger}
</div>
{isOpen && (
<div className="dropdown-menu" role="menu">
{children}
</div>
)}
</div>
);
}
function DropdownItem({ onClick, children }) {
return (
<div className="dropdown-item" role="menuitem" onClick={onClick} tabIndex={0}>
{children}
</div>
);
}
Dropdown.Item = DropdownItem;Compound Components Pattern
复合组件模式
Tabs Component
Tabs组件
jsx
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
className={activeTab === id ? 'active' : ''}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ id, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;jsx
const TabsContext = createContext();
function Tabs({ children, defaultTab }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list" role="tablist">{children}</div>;
}
function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
className={activeTab === id ? 'active' : ''}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ id, children }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;
Tabs.Panel = TabPanel;Storybook Integration
Storybook集成
Installation
安装
bash
npx storybook@latest initbash
npx storybook@latest initButton Stories
Button组件示例
jsx
// Button.stories.jsx
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger']
},
size: {
control: 'select',
options: ['sm', 'md', 'lg']
}
}
};
export const Primary = {
args: {
variant: 'primary',
children: 'Button'
}
};
export const WithIcons = {
args: {
leftIcon: <Icon name="star" />,
children: 'Button'
}
};
export const Loading = {
args: {
loading: true,
children: 'Loading...'
}
};jsx
// Button.stories.jsx
import { Button } from './Button';
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger']
},
size: {
control: 'select',
options: ['sm', 'md', 'lg']
}
}
};
export const Primary = {
args: {
variant: 'primary',
children: 'Button'
}
};
export const WithIcons = {
args: {
leftIcon: <Icon name="star" />,
children: 'Button'
}
};
export const Loading = {
args: {
loading: true,
children: 'Loading...'
}
};TypeScript Support
TypeScript支持
typescript
// Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, variant = 'primary', size = 'md', ...props }, ref) => {
return (
<button ref={ref} className={`btn btn-${variant} btn-${size}`} {...props}>
{children}
</button>
);
}
);typescript
// Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ children, variant = 'primary', size = 'md', ...props }, ref) => {
return (
<button ref={ref} className={`btn btn-${variant} btn-${size}`} {...props}>
{children}
</button>
);
}
);Publishing to npm
发布至npm
package.json Setup
package.json配置
json
{
"name": "@yourname/component-library",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}json
{
"name": "@yourname/component-library",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}Practice Projects
实践项目
- Build a complete Button component with variants
- Create accessible Form components (Input, Select, Checkbox)
- Implement Modal with focus trap
- Build Dropdown with keyboard navigation
- Create Tabs compound component
- Implement Toast notification system
- Build Tooltip component
- 构建一个包含多种变体的完整Button组件
- 创建支持可访问性的表单组件(Input、Select、Checkbox)
- 实现带有焦点陷阱的Modal组件
- 构建支持键盘导航的Dropdown组件
- 创建Tabs复合组件
- 实现Toast通知系统
- 构建Tooltip组件
Resources
参考资源
- Radix UI - Headless UI components
- Headless UI - Unstyled components
- Storybook - Component documentation
- React Aria - Accessibility
Difficulty: Intermediate to Advanced
Estimated Time: 3-4 weeks
Prerequisites: React Fundamentals, Component Architecture
- Radix UI - 无头UI组件库
- Headless UI - 无样式组件库
- Storybook - 组件文档工具
- React Aria - 可访问性工具库
难度:中高级
预计耗时:3-4周
前置要求:React基础、组件架构知识