Vue3组件库设计实战:从架构搭建到Tree-shaking优化的完整方案

组件库项目架构怎么搭

Vue3组件库不是简单的组件堆叠,从目录结构到构建工具链再到发布流程,每一步都影响后续维护和接入成本。一个合理的项目架构需要同时满足开发体验和生产性能——开发时热更新快、类型提示全,生产时Tree-shaking干净、产物体积小。

推荐的Monorepo结构:

my-ui/
├── packages/
│   ├── components/        # 组件源码
│   │   ├── button/
│   │   │   ├── src/
│   │   │   │   ├── Button.vue
│   │   │   │   └── Button.ts    # 组件入口
│   │   │   └── index.ts
│   │   ├── input/
│   │   └── index.ts           # 组件统一导出
│   ├── theme/               # 样式系统
│   │   ├── src/
│   │   │   ├── var.scss       # CSS变量
│   │   │   └── button.scss
│   │   └── index.scss
│   └── utils/               # 工具函数
├── docs/                    # 文档站
├── playground/              # 调试沙盒
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json
└── vite.config.ts

pnpm-workspace.yaml:

packages:
  - 'packages/*'
  - 'docs'
  - 'playground'

组件设计与TypeScript类型定义

组件设计的第一步是定义Props类型。以Button组件为例,用TypeScript的类型系统把API约束清楚:

// packages/components/button/src/Button.ts
import type { ExtractPropTypes, PropType } from 'vue'

export type ButtonType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
export type ButtonSize = 'large' | 'default' | 'small'

export const buttonProps = {
  type: {
    type: String as PropType<ButtonType>,
    default: 'primary',
  },
  size: {
    type: String as PropType<ButtonSize>,
    default: 'default',
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  loading: {
    type: Boolean,
    default: false,
  },
  icon: {
    type: String,
    default: '',
  },
} as const

export type ButtonProps = ExtractPropTypes<typeof buttonProps>

组件实现:

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

<script setup lang="ts">
import { buttonProps } from './Button'
defineOptions({ name: 'MyButton' })
const props = defineProps(buttonProps)
const emit = defineEmits<{ click: [e: MouseEvent] }>()

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

组件注册与按需导出

每个组件单独导出,同时提供全量注册的插件入口。这是Tree-shaking的基础:

// packages/components/button/index.ts
import { withInstall } from '@my-ui/utils'
import Button from './src/Button.vue'

export const MyButton = withInstall(Button)
export default MyButton

// packages/components/index.ts
export { MyButton } from './button'
export { MyInput } from './input'
// ...其他组件

withInstall工具函数给组件添加install方法:

// packages/utils/with-install.ts
import type { Plugin, App, Component } from 'vue'

type SFCWithInstall<T> = T & Plugin

export function withInstall<T extends Component>(component: T): SFCWithInstall<T> {
  const comp = component as SFCWithInstall<T>
  comp.install = (app: App) => {
    app.component(comp.name!, comp)
  }
  return comp
}

Vite构建配置与Tree-shaking优化

构建产物需要同时支持ESM和UMD两种格式。ESM供现代打包工具做Tree-shaking,UMD供CDN直接引入。Vite的rollup配置:

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

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: resolve(__dirname, 'packages/components/index.ts'),
      name: 'MyUI',
      formats: ['es', 'umd'],
      fileName: (format) => `my-ui.${format}.js`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        // ESM格式保留模块结构,支持Tree-shaking
        exports: 'named',
        // 手动拆分每个组件为独立chunk
        manualChunks(id) {
          if (id.includes('packages/components')) {
            const match = id.match(/components\/([^/]+)/)
            if (match) return `my-ui-${match[1]}`
          }
        },
      },
    },
  },
})

关键点:manualChunks把每个组件拆成独立文件,用ESM格式的用户只需要导入的组件代码,不会把整个库拉进来。

CSS变量主题系统

样式隔离用CSS变量方案,不依赖运行时JS切换,性能最好:

/* packages/theme/src/var.scss */
:root {
  --my-color-primary: #409eff;
  --my-color-success: #67c23a;
  --my-color-warning: #e6a23c;
  --my-color-danger: #f56c6c;
  --my-color-info: #909399;
  --my-font-size-large: 16px;
  --my-font-size-default: 14px;
  --my-font-size-small: 12px;
  --my-border-radius: 4px;
}

/* 暗色主题 */
:root[data-theme='dark'] {
  --my-color-primary: #3a8ee6;
  --my-color-success: #5daf34;
  --my-color-warning: #cf9236;
  --my-color-danger: #dd5a5a;
}

Button样式使用变量:

/* packages/theme/src/button.scss */
.my-button {
  display: inline-flex;
  align-items: center;
  padding: 8px 16px;
  font-size: var(--my-font-size-default);
  border-radius: var(--my-border-radius);
  border: 1px solid var(--my-color-primary);
  background: var(--my-color-primary);
  color: #fff;
  cursor: pointer;
  transition: all 0.2s;

  &--large { padding: 10px 20px; font-size: var(--my-font-size-large); }
  &--small { padding: 6px 12px; font-size: var(--my-font-size-small); }
  &.is-disabled { opacity: 0.6; cursor: not-allowed; }
  &.is-loading { opacity: 0.7; cursor: wait; }
}

组件库发布与版本管理

发布到npm前,package.json的入口配置必须正确:

{
  "name": "@my-ui/components",
  "version": "1.0.0",
  "main": "./dist/my-ui.umd.js",
  "module": "./dist/my-ui.es.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-ui.es.js",
      "require": "./dist/my-ui.umd.js",
      "types": "./dist/types/index.d.ts"
    },
    "./button": {
      "import": "./dist/my-ui-button.es.js",
      "types": "./dist/types/button/index.d.ts"
    }
  },
  "sideEffects": ["**/*.scss", "**/*.css"]
}

sideEffects字段告诉打包工具:除了样式文件外,其他模块都是纯函数,可以安全Tree-shaking。exports字段支持import { MyButton } from '@my-ui/components/button'这样的按需导入,Vite/Webpack会自动处理。

类型声明生成用vue-tsc

vue-tsc --declaration --emitDeclarationOnly --outDir dist/types

这样用户在TS项目中使用组件库就能获得完整的类型提示和自动补全,开发体验和用Element Plus一样顺滑。

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

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

相关推荐