Vue3 组合式 API 组件库设计:从架构规划到发布的工程化实践

企业级 Vue3 组件库为什么必须从架构设计开始

组件库不是几个 UI 组件的简单堆砌。一个在多个业务线使用的组件库,必须解决类型安全、样式隔离、按需加载、主题定制、版本管理等工程问题。跳过架构设计直接写组件,后期重构成本远大于前期规划成本。Vue3 的组合式 API(Composition API)为组件库设计提供了更强的逻辑复用能力,但也需要更规范的目录结构和构建策略。

组件库项目结构设计

推荐的项目目录结构:

my-ui/
├── packages/
│   ├── components/
│   │   ├── button/
│   │   │   ├── src/
│   │   │   │   ├── Button.vue
│   │   │   │   ├── useButton.ts
│   │   │   │   └── button.props.ts
│   │   │   ├── index.ts
│   │   │   └── __tests__/
│   │   │       └── button.test.ts
│   │   ├── input/
│   │   └── ...
│   ├── hooks/
│   │   ├── use-click-outside.ts
│   │   ├── use-intersection-observer.ts
│   │   └── index.ts
│   ├── theme/
│   │   ├── src/
│   │   │   ├── variables.css
│   │   │   ├── dark.css
│   │   │   └── light.css
│   │   └── index.ts
│   └── utils/
│       ├── props.ts
│       └── install.ts
├── scripts/
│   ├── build.ts
│   └── publish.sh
├── package.json
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts

每个组件一个目录,内部包含实现、composable 逻辑抽取、props 类型定义和单测。hooks 目录存放跨组件复用的组合式函数,theme 目录管理样式变量和主题切换。

组合式 API 组件封装模式

以 Button 组件为例,展示组合式 API 的封装标准:

Props 类型定义button.props.ts):

import type { PropType } from 'vue'

export const buttonProps = {
  type: {
    type: String as PropType<'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'>,
    default: 'default',
  },
  size: {
    type: String as PropType<'large' | 'default' | 'small'>,
    default: 'default',
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  loading: {
    type: Boolean,
    default: false,
  },
  icon: {
    type: [String, Object] as PropType<string | Component>,
    default: '',
  },
} as const

Props 独立定义文件,方便 SFC 组件和 TSX 组件复用,也能自动生成文档。

Composable 逻辑抽取useButton.ts):

import type { SetupContext } from 'vue'
import { buttonProps } from './button.props'

export function useButton(props: typeof buttonProps, emit: SetupContext['emit']) {
  const handleClick = (e: MouseEvent) => {
    if (props.disabled || props.loading) return
    emit('click', e)
  }

  const buttonClass = computed(() => [
    'my-button',
    `my-button--${props.type}`,
    `my-button--${props.size}`,
    {
      'is-disabled': props.disabled,
      'is-loading': props.loading,
    },
  ])

  return { handleClick, buttonClass }
}

逻辑与模板分离,composable 可以在不同组件间复用。如果后续要实现 LinkButton,只需新写一个模板文件引用同一个 useButton。

组件模板Button.vue):

<template>
  <button :class="buttonClass" :disabled="disabled || loading" @click="handleClick">
    <span v-if="loading" class="my-button__loading">
      <svg class="my-button__spinner" viewBox="0 0 1024 1024">...</svg>
    </span>
    <slot />
  </button>
</template>

<script setup lang="ts">
import { buttonProps } from './button.props'
import { useButton } from './useButton'

const props = defineProps(buttonProps)
const emit = defineEmits(['click'])
const { handleClick, buttonClass } = useButton(props, emit)
</script>

CSS 变量与主题系统设计

样式隔离是组件库的基础要求。使用 CSS 变量实现主题定制:

/* packages/theme/src/variables.css */
:root {
  --my-color-primary: #409eff;
  --my-color-success: #67c23a;
  --my-color-warning: #e6a23c;
  --my-color-danger: #f56c6c;
  --my-color-info: #909399;

  --my-font-size-base: 14px;
  --my-font-size-small: 12px;
  --my-font-size-large: 16px;

  --my-border-radius-base: 4px;
  --my-border-color: #dcdfe6;

  --my-button-height-default: 32px;
  --my-button-height-large: 40px;
  --my-button-height-small: 24px;
}

/* packages/theme/src/dark.css */
html.dark {
  --my-color-primary: #3a8ee6;
  --my-border-color: #4c4d4f;
  --my-bg-color: #1d1e1f;
}

组件样式使用变量而非硬编码颜色值:

.my-button--primary {
  background-color: var(--my-color-primary);
  border-color: var(--my-color-primary);
  color: #fff;
}

.my-button--small {
  height: var(--my-button-height-small);
  font-size: var(--my-font-size-small);
}

业务方只需覆盖 CSS 变量就能实现品牌定制,不需要改组件源码。

按需加载与 Tree-shaking 配置

组件库全量引入会显著增加打包体积。按需加载通过 ES Module 的导出结构实现:

每个组件的 index.ts

import { withInstall } from '@my-ui/utils'
import Button from './src/Button.vue'

export const MyButton = withInstall(Button)
export default MyButton
export * from './button.props'

总入口 packages/components/index.ts

export { MyButton } from './button'
export { MyInput } from './input'
// ... 逐个导出

构建配置使用 Vite 的 library 模式:

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'packages/components/index.ts'),
      formats: ['es'],
      fileName: () => 'index.mjs',
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        preserveModules: true,
        preserveModulesRoot: 'packages',
      },
    },
  },
})

preserveModules: true 让 Rollup 保留原始模块结构,业务项目 import 某个组件时,打包工具可以只打包该组件及其依赖。

package.json 的 exports 字段支持子路径导入:

{
  "exports": {
    ".": { "import": "./dist/index.mjs" },
    "./button": { "import": "./dist/components/button/index.mjs" },
    "./input": { "import": "./dist/components/input/index.mjs" },
    "./theme": { "import": "./dist/theme/index.mjs" }
  }
}

业务项目直接 import { MyButton } from '@my-ui/button',只引入 Button 相关代码。

自动化测试策略

组件库测试分两层:单元测试覆盖逻辑,组件测试覆盖渲染和交互:

// button.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { MyButton } from '../index'

describe('MyButton', () => {
  it('emits click event when not disabled', async () => {
    const wrapper = mount(MyButton)
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toHaveLength(1)
  })

  it('does not emit click when disabled', async () => {
    const wrapper = mount(MyButton, { props: { disabled: true } })
    await wrapper.trigger('click')
    expect(wrapper.emitted('click')).toBeUndefined()
  })

  it('applies size class correctly', () => {
    const wrapper = mount(MyButton, { props: { size: 'small' } })
    expect(wrapper.classes()).toContain('my-button--small')
  })
})

CI 流水线中配置 Vitest 跑全量测试,覆盖率门禁 80%。

发布与版本管理

使用 changesets 管理版本和 changelog:

# 安装
pnpm add -D @changesets/cli

# 初始化
pnpm changeset init

# 每次发版前记录变更
pnpm changeset

# 自动更新版本号和 changelog
pnpm changeset version

# 发布
pnpm changeset publish

changesets 的优势在于支持 monorepo 中多包联动版本,修改了 Button 组件后可以只发 Button 的补丁版本,不需要全量发版。

Vue3 组合式 API 组件库的核心设计思路是逻辑与模板分离、样式与变量分离、导出与构建分离。这三个分离让组件库在多业务线复用时具备了按需加载、主题定制和独立发版的能力,是前端工程化体系中基础设施级别的工作。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zu-jian-ku-she-ji-cong-jia-gou-gui-hua/

(0)
小编小编
上一篇 4小时前
下一篇 4小时前

相关推荐