Skip to content
第 129 / 250 章前端⏱ 10 分钟阅读

第 129 章:组件库设计

学习目标

  • 掌握组件库的设计原则与 API 规范
  • 学会封装可复用的基础组件
  • 理解主题系统、样式隔离与按需加载
  • 能从零搭建一个组件库脚手架

一、什么是组件库

一组通用、可复用、有设计规范的 UI 组件集合,为多个项目提供一致的视觉与交互。

二、组件库设计原则

2.1 单一职责

typescript
// ✅ 每个组件只做一件事
MyButton    // 按钮
MyInput     // 输入
MyModal     // 弹窗

2.2 命名规范

类型规范示例
组件PascalCase + 前缀MyButton
文件.vueMyButton.vue
目录kebab-casecomponents/button/
PropscamelCasemodelValue
事件kebab-caseupdate:modelValue

2.3 目录结构

my-ui/
├── packages/
│   ├── components/
│   │   ├── button/
│   │   │   ├── Button.vue
│   │   │   ├── button.css
│   │   │   └── index.ts
│   │   ├── input/
│   │   └── index.ts
│   ├── styles/
│   │   ├── base.css
│   │   └── theme.css
│   └── index.ts
├── play/              # 测试/演示
│   └── App.vue
└── package.json

三、组件 API 设计

3.1 Button 示例

vue
<!-- Button.vue -->
<script setup lang="ts">
interface Props {
  type?: 'primary' | 'success' | 'warning' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  loading?: boolean;
  block?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'primary',
  size: 'medium',
  disabled: false,
  loading: false,
  block: false
});

const emit = defineEmits<{
  click: [event: MouseEvent];
}>();

const handleClick = (e: MouseEvent) => {
  if (!props.disabled && !props.loading) {
    emit('click', e);
  }
};
</script>

<template>
  <button
    :class="[
      'my-button',
      `my-button--${type}`,
      `my-button--${size}`,
      { 'is-disabled': disabled, 'is-loading': loading, 'is-block': block }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="my-button__loader" />
    <slot />
  </button>
</template>

3.2 双向绑定(v-model)

vue
<!-- Input.vue -->
<script setup lang="ts">
interface Props {
  modelValue: string;
  type?: string;
  placeholder?: string;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  type: 'text',
  disabled: false
});

const emit = defineEmits<{
  'update:modelValue': [value: string];
  change: [value: string];
}>();

const handleInput = (e: Event) => {
  const val = (e.target as HTMLInputElement).value;
  emit('update:modelValue', val);
};
</script>

<template>
  <input
    :type="type"
    :value="modelValue"
    :placeholder="placeholder"
    :disabled="disabled"
    class="my-input"
    @input="handleInput"
  />
</template>

四、样式隔离

4.1 Scoped CSS

vue
<style scoped>
.my-button {
  /* 只对当前组件生效 */
  padding: 8px 16px;
}
</style>

4.2 CSS 变量主题

css
/* theme.css */
:root {
  --my-primary: #42b883;
  --my-danger: #f56c6c;
  --my-border-radius: 4px;
  --my-font-size: 14px;
}

/* 暗色主题 */
[data-theme="dark"] {
  --my-primary: #5fb889;
}
vue
<style scoped>
.my-button {
  background: var(--my-primary);
  border-radius: var(--my-border-radius);
}
</style>

4.3 BEM 命名

vue
<template>
  <div class="my-modal">
    <header class="my-modal__header">{{ title }}</header>
    <div class="my-modal__body">
      <slot />
    </div>
    <footer class="my-modal__footer">
      <slot name="footer" />
    </footer>
  </div>
</template>

五、组件分类

5.1 基础组件

typescript
// Button, Input, Select, Checkbox, Radio, Switch, Slider

5.2 反馈组件

typescript
// Toast, Modal, Drawer, Alert, Notification, Message

5.3 导航组件

typescript
// Menu, Tabs, Breadcrumb, Pagination, Steps

5.4 数据展示

typescript
// Table, List, Card, Tag, Avatar, Badge

5.5 业务组件

typescript
// UserCard, SearchBar, FileUploader, RichEditor

六、组件通讯

6.1 props/emits

vue
<script setup lang="ts">
const props = defineProps<{ items: Item[] }>();
const emit = defineEmits<{ select: [item: Item] }>();
</script>

6.2 v-model

vue
<!-- 支持多个 v-model -->
defineProps<{ modelValue: string; visible: boolean }>();
defineEmits<{
  'update:modelValue': [v: string];
  'update:visible': [v: boolean];
}>();

6.3 provide/inject

typescript
// Carousel 组件向 Slide 注入
provide('carousel', {
  current: currentIndex,
  goto: (i: number) => { /* ... */ }
});

七、插槽设计

7.1 默认插槽

vue
<template>
  <div class="my-card">
    <slot />
  </div>
</template>

7.2 具名插槽

vue
<template>
  <div class="my-modal">
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" /></footer>
  </div>
</template>

7.3 作用域插槽

vue
<script setup lang="ts">
interface Item { id: number; name: string }
defineProps<{ items: Item[] }>();
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id" />
    </li>
  </ul>
</template>
vue
<MyList :items="users">
  <template #default="{ item, index }">
    {{ index }} - {{ item.name }}
  </template>
</MyList>

八、组件库入口

8.1 统一导出

typescript
// packages/index.ts
import type { App } from 'vue';
import MyButton from './components/button';
import MyInput from './components/input';
import MyModal from './components/modal';

const components = [MyButton, MyInput, MyModal];

export {
  MyButton,
  MyInput,
  MyModal
};

export default {
  install(app: App) {
    components.forEach((comp) => {
      app.use(comp);
    });
  }
};

8.2 单组件注册

typescript
// components/button/index.ts
import Button from './Button.vue';

Button.install = (app: App) => {
  app.component('MyButton', Button);
};

export default Button;

8.3 类型导出

typescript
// types.ts
export interface ButtonProps {
  type?: 'primary' | 'success' | 'warning' | 'danger';
  size?: 'small' | 'medium' | 'large';
}

export type ButtonInstance = InstanceType<typeof import('./Button.vue').default>;

九、Monorepo 搭建

9.1 pnpm workspace

yaml
# pnpm-workspace.yaml
packages:
  - 'packages/*'
  - 'play'

9.2 项目结构

my-ui/
├── packages/
│   ├── components/
│   ├── theme/
│   └── utils/
├── play/
├── docs/
├── pnpm-workspace.yaml
└── package.json

9.3 内部依赖

json
{
  "dependencies": {
    "@my-ui/components": "workspace:*",
    "@my-ui/theme": "workspace:*"
  }
}

十、按需加载

10.1 tree-shaking

typescript
// ✅ ES Module,自动按需
import { MyButton } from 'my-ui';

10.2 unplugin-vue-components

bash
pnpm add -D unplugin-vue-components
typescript
// vite.config.ts
import Components from 'unplugin-vue-components/vite';
import { MyUiResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    Components({
      resolvers: [MyUiResolver()]
    })
  ]
});

10.3 手动按需

typescript
// 注册全部
import MyUI from 'my-ui';
app.use(MyUI);

// 按需
import { MyButton, MyInput } from 'my-ui';
app.component('MyButton', MyButton);

十一、文档与示例

11.1 VitePress 文档

markdown
# Button 按钮

## 基础用法

<demo-block>
  <my-button>默认</my-button>
  <my-button type="primary">主要</my-button>
</demo-block>

## API

| 参数 | 说明 | 类型 | 默认值 |
|------|------|------|--------|
| type | 类型 | string | primary |

11.2 自动生成文档

typescript
// vite-plugin-md
import Markdown from 'unplugin-vue-markdown/vite';

export default defineConfig({
  plugins: [
    Markdown({
      headmatter: {
        title: 'title'
      }
    })
  ]
});

十二、版本管理

12.1 语义化版本

主版本.次版本.修订号
  ↓      ↓      ↓
  1 .   2  .   3
  • 主版本:不兼容 API 变更
  • 次版本:向后兼容的功能新增
  • 修订号:向后兼容的 bug 修复

12.2 Changesets

bash
pnpm add -D @changesets/cli
markdown
# .changeset/xxx.md
---
'my-ui': patch
---

修复 Button 在 disabled 状态下的样式问题

十三、测试

13.1 单元测试

typescript
import { mount } from '@vue/test-utils';
import MyButton from './Button.vue';

describe('MyButton', () => {
  it('renders', () => {
    const wrapper = mount(MyButton, {
      props: { type: 'primary' },
      slots: { default: 'Click' }
    });
    expect(wrapper.text()).toBe('Click');
  });

  it('emits click', async () => {
    const wrapper = mount(MyButton);
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
  });

  it('disabled', async () => {
    const wrapper = mount(MyButton, { props: { disabled: true } });
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeFalsy();
  });
});

13.2 视觉测试

typescript
// chromatic / Percy
pnpm add -D chromatic

十四、推荐组件库

特点
Element Plus桌面端,功能丰富
Ant Design Vue阿里出品,企业级
Naive UI简洁,Tailwind 风格
VuetifyMaterial Design
PrimeVue主题丰富
Quasar多端

十五、本章小结

维度关键
命名PascalCase + 统一前缀
APIprops + emits + slots
样式Scoped + CSS 变量
主题CSS 变量 + 暗色模式
目录components/ + 单组件目录
注册install + 全局注册
文档VitePress + 演示
版本语义化 + Changesets

动手练习

  1. Button 组件:实现支持 type/size/loading/disabled 的按钮
  2. Modal 组件:实现支持 v-model 控制的弹窗
  3. Table 组件:实现支持列定义和数据分页的表格
  4. 主题系统:实现 CSS 变量切换深浅色
  5. 文档站:用 VitePress 搭建组件库文档

推荐阅读


下一章第 130 章:动画与过渡

本站基于 VitePress 构建 · 由 Codebook 团队维护