从 0 搭建现代前端组件库:2026年完整实战指南

前言

很多前端工程师用过 Element Plus、Ant Design,但自己动手搭建组件库时却无从下手。本文从零开始,带你搭建一个生产级组件库,包含设计系统、工程化、文档、发布全流程。

正文

一、组件库架构设计

1.1 整体架构
复制代码
my-ui/
├── packages/
│   ├── components/          # 组件源码
│   ├── theme/               # 样式主题
│   └── utils/               # 工具函数
├── docs/                    # 文档站点
├── play/                    # 调试 playground
├── scripts/                 # 构建脚本
└── package.json
1.2 技术选型(2026年标准)
环节 工具 说明
构建 Rollup + Vite 支持 ESM/CJS/UMD
测试 Vitest 单元测试
文档 Storybook 组件展示
类型 TypeScript 类型定义
样式 SCSS + CSS Vars 主题定制
发布 Changeset 版本管理

二、组件设计与实现

2.1 Button 组件示例
typescript 复制代码
// packages/components/button/src/button.tsx
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  name: 'MyButton',
  props: {
    type: {
      type: String as PropType<'primary' | 'success' | 'warning' | 'danger'>,
      default: 'primary'
    },
    size: {
      type: String as PropType<'small' | 'medium' | 'large'>,
      default: 'medium'
    },
    disabled: Boolean,
    loading: Boolean
  },
  setup(props, { slots, emit }) {
    return () => (
      <button
        class={[
          'my-button',
          `my-button--${props.type}`,
          `my-button--${props.size}`,
          {
            'is-disabled': props.disabled,
            'is-loading': props.loading
          }
        ]}
        disabled={props.disabled || props.loading}
        onClick={() => emit('click')}
      >
        {props.loading && <span class="my-button__loading" />}
        {slots.default?.()}
      </button>
    )
  }
})
2.2 样式设计(CSS 变量方案)
scss 复制代码
// packages/theme/src/button.scss
:root {
  --my-button-primary-bg: #409eff;
  --my-button-primary-text: #ffffff;
  --my-button-success-bg: #67c23a;
  --my-button-danger-bg: #f56c6c;
  
  --my-button-small-padding: 8px 12px;
  --my-button-medium-padding: 12px 20px;
  --my-button-large-padding: 16px 24px;
}

.my-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
  
  &--primary {
    background: var(--my-button-primary-bg);
    color: var(--my-button-primary-text);
    
    &:hover {
      opacity: 0.8;
    }
  }
  
  &--small {
    padding: var(--my-button-small-padding);
    font-size: 12px;
  }
  
  &--medium {
    padding: var(--my-button-medium-padding);
    font-size: 14px;
  }
}
2.3 类型定义文件
typescript 复制代码
// packages/components/button/src/button.d.ts
export type ButtonType = 'primary' | 'success' | 'warning' | 'danger'
export type ButtonSize = 'small' | 'medium' | 'large'

export interface ButtonProps {
  type?: ButtonType
  size?: ButtonSize
  disabled?: boolean
  loading?: boolean
}

export interface ButtonEmits {
  (e: 'click'): void
}

三、工程化配置

3.1 Rollup 构建配置
javascript 复制代码
// scripts/rollup.config.js
import { defineConfig } from 'rollup'
import vue from '@vitejs/plugin-vue'
import typescript from '@rollup/plugin-typescript'
import postcss from 'rollup-plugin-postcss'
import { terser } from 'rollup-plugin-terser'

export default defineConfig([
  // ESM 构建
  {
    input: 'packages/components/index.ts',
    output: {
      file: 'dist/es/index.js',
      format: 'es'
    },
    plugins: [
      vue(),
      typescript(),
      postcss({
        extract: true,
        minimize: true
      })
    ],
    external: ['vue']
  },
  // CJS 构建
  {
    input: 'packages/components/index.ts',
    output: {
      file: 'dist/lib/index.js',
      format: 'cjs'
    },
    plugins: [vue(), typescript()],
    external: ['vue']
  },
  // UMD 构建(浏览器直接用)
  {
    input: 'packages/components/index.ts',
    output: {
      file: 'dist/dist/index.full.js',
      format: 'umd',
      name: 'MyUI',
      globals: {
        vue: 'Vue'
      }
    },
    plugins: [vue(), typescript(), terser()],
    external: ['vue']
  }
])
3.2 package.json 配置
json 复制代码
{
  "name": "@my-org/ui",
  "version": "0.1.0",
  "type": "module",
  "main": "dist/lib/index.js",
  "module": "dist/es/index.js",
  "types": "dist/types/index.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "build": "rollup -c scripts/rollup.config.js",
    "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist/types",
    "test": "vitest",
    "docs:dev": "storybook dev -p 6006",
    "docs:build": "storybook build -o docs-dist",
    "release": "changeset publish"
  },
  "peerDependencies": {
    "vue": "^3.3.0"
  }
}

四、测试策略

4.1 单元测试(Vitest)
typescript 复制代码
// packages/components/button/__tests__/button.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from '../src/button'

describe('Button', () => {
  it('renders correctly', () => {
    const wrapper = mount(Button, {
      slots: { default: 'Click me' }
    })
    expect(wrapper.text()).toBe('Click me')
    expect(wrapper.classes()).toContain('my-button')
  })

  it('handles different types', () => {
    const wrapper = mount(Button, {
      props: { type: 'success' }
    })
    expect(wrapper.classes()).toContain('my-button--success')
  })

  it('emits click event', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('click')
  })

  it('is disabled when loading', () => {
    const wrapper = mount(Button, {
      props: { loading: true }
    })
    expect(wrapper.attributes('disabled')).toBeDefined()
  })
})
4.2 视觉回归测试(Storybook + Chromatic)
typescript 复制代码
// packages/components/button/src/button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import Button from './button'

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    type: { control: 'select', options: ['primary', 'success', 'warning', 'danger'] },
    size: { control: 'select', options: ['small', 'medium', 'large'] }
  }
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: {
    type: 'primary',
    default: 'Primary Button'
  }
}

export const Loading: Story = {
  args: {
    loading: true,
    default: 'Loading...'
  }
}

五、文档站点搭建

5.1 VitePress 配置
javascript 复制代码
// docs/.vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'My UI',
  description: 'A Vue 3 Component Library',
  themeConfig: {
    nav: [
      { text: 'Guide', link: '/guide/' },
      { text: 'Components', link: '/components/' }
    ],
    sidebar: {
      '/components/': [
        {
          text: 'Basic',
          items: [
            { text: 'Button', link: '/components/button' },
            { text: 'Icon', link: '/components/icon' }
          ]
        },
        {
          text: 'Form',
          items: [
            { text: 'Input', link: '/components/input' },
            { text: 'Select', link: '/components/select' }
          ]
        }
      ]
    }
  }
})

六、发布与版本管理

6.1 Changeset 配置
json 复制代码
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}
6.2 发布流程
bash 复制代码
# 1. 添加 changeset
npx changeset

# 2. 提升版本
npx changeset version

# 3. 构建
npm run build

# 4. 发布
npx changeset publish

七、设计系统对接

7.1 Figma Tokens 同步
javascript 复制代码
// scripts/sync-figma-tokens.js
import { getFigmaFile } from 'figma-api'

async function syncTokens() {
  const file = await getFigmaFile('FILE_KEY')
  const tokens = extractTokens(file)
  
  // 生成 CSS 变量
  generateCSSVariables(tokens)
  
  // 生成 TypeScript 类型
  generateTSTypes(tokens)
}

八、总结

搭建组件库不是简单的代码堆砌,而是系统工程:

  1. 设计先行 - 有完整的设计系统和规范
  2. 质量保障 - 测试覆盖、类型完整
  3. 开发体验 - 文档完善、调试方便
  4. 发布流程 - 自动化、版本管理

2026年组件库新趋势:

  • AI 辅助设计(Figma AI 生成组件)
  • Headless UI 模式(逻辑与样式分离)
  • 微前端适配(Module Federation 支持)
相关推荐
凌冰_1 小时前
Thymeleaf 核心语法详解
java·前端·javascript
AIBox3651 小时前
claude 镜像 api 使用指南(2026 年4 月更新)
java·服务器·前端·人工智能·gpt·前端框架
SuperEugene1 小时前
Vue3 配置文件管理:按模块拆分配置,提升配置可维护性|配置驱动开发实战篇
前端·javascript·vue.js·驱动开发
阿凤211 小时前
后端返回文件二进制流
开发语言·前端·javascript·uniapp
落魄江湖行1 小时前
进阶篇四 Nuxt4 Server Routes:写后端 API
前端·vue.js·typescript·nuxt4
萧行之2 小时前
解决Microsoft Edge/Hotmail登录报错(15/25/2603、0x80190001)
前端·microsoft·edge
Eiceblue2 小时前
C# 删除 PDF 页面:单页 / 多页批量删除技巧
前端·pdf·c#
悟空瞎说2 小时前
从isMounted到跨页面状态:高级前端如何优雅解决订单场景的“幽灵陷阱”(附React/Vue完整代码)
前端·javascript
C_fashionCat2 小时前
【2026面试题】前端实际场景去考察原理
前端·vue.js·面试