📖 核心概念
- 组件库架构设计
- 复合组件模式(Compound Components)
- Headless UI理念
- 样式方案:CSS Modules / Tailwind / CSS-in-JS
- 文档与Storybook
- 发布与版本管理
💻 代码实现
复合组件与Headless
✅ tsx
// 复合组件模式 - Tabs
import { createContext, useContext, useState } from 'react';
interface TabsContextValue {
activeTab: string;
setActiveTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextValue>({} as TabsContextValue);
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: ReactNode }) {
return <div role="tablist" className="tab-list">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
className={activeTab === id ? 'active' : ''}
>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return <div role="tabpanel" className="tab-panel">{children}</div>;
}
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// 使用
<Tabs defaultTab="overview">
<Tabs.List>
<Tabs.Tab id="overview">概览</Tabs.Tab>
<Tabs.Tab id="details">详情</Tabs.Tab>
<Tabs.Tab id="reviews">评价</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="overview">概览内容</Tabs.Panel>
<Tabs.Panel id="details">详情内容</Tabs.Panel>
<Tabs.Panel id="reviews">评价内容</Tabs.Panel>
</Tabs>
// Headless Hook - useToggle
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
return {
on,
off: !on,
toggle: () => setOn(p => !p),
setOn: () => setOn(true),
setOff: () => setOn(false),
getToggleProps: () => ({
role: 'switch' as const,
'aria-checked': on,
onClick: () => setOn(p => !p),
}),
};
}
// Headless Component
function Switch({ children }: { children: (props: ReturnType<typeof useToggle>) => ReactNode }) {
const toggle = useToggle();
return <>{children(toggle)}</>;
}
<Switch>
{({ on, getToggleProps }) => (
<button {...getToggleProps()} className={\`switch \${on ? 'on' : ''}\`}>
<span className="switch-thumb" />
</button>
)}
</Switch>
组件库发布
✅ bash
# 组件库项目结构
# my-ui/
# ├── src/
# │ ├── components/
# │ │ ├── Button/
# │ │ │ ├── Button.tsx
# │ │ │ ├── Button.module.css
# │ │ │ ├── Button.test.tsx
# │ │ │ └── index.ts
# │ │ ├── Input/
# │ │ ├── Modal/
# │ │ └── index.ts # 统一导出
# │ └── styles/
# │ ├── tokens.css
# │ └── reset.css
# ├── .storybook/
# ├── package.json
# ├── tsconfig.json
# └── vite.config.ts
# package.json
# {
# "name": "@myorg/ui",
# "version": "1.0.0",
# "type": "module",
# "main": "./dist/index.cjs",
# "module": "./dist/index.js",
# "types": "./dist/index.d.ts",
# "exports": {
# ".": { "import": "./dist/index.js", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" },
# "./styles.css": "./dist/styles.css"
# },
# "files": ["dist"],
# "sideEffects": ["**/*.css"],
# "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" }
# }
# 构建配置
# vite.config.ts
# export default defineConfig({
# build: {
# lib: {
# entry: resolve(__dirname, 'src/components/index.ts'),
# formats: ['es', 'cjs'],
# fileName: (format) => \`index.\${format === 'es' ? 'js' : 'cjs'}\`,
# },
# rollupOptions: {
# external: ['react', 'react-dom', 'react/jsx-runtime'],
# output: { globals: { react: 'React', 'react-dom': 'ReactDOM' } },
# },
# },
# });
# 发布流程
pnpm build
pnpm test
npm version patch # 或 minor, major
npm publish --access public
🎯 练习任务
- 1 实现一个复合组件模式的Dropdown
- 2 创建一个Headless的useDisclosure Hook
- 3 搭建Storybook,为组件编写文档和交互示例
- 4 发布一个组件库到npm
🏆 成就解锁
组件库工程师 — 掌握组件库开发全流程,打造可复用的UI组件库