📚 现代前端开发 目录
阶段4:UI与体验
第 23 / 35 课

组件库开发

打造自己的React组件库

📖 核心概念

💻 代码实现

复合组件与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

🎯 练习任务

🏆 成就解锁
组件库工程师 — 掌握组件库开发全流程,打造可复用的UI组件库