Vue3组件库工程化实践:从Monorepo架构到npm发布的完整流程

Vue3组件库工程化的技术选型与项目初始化

前端开发中,Vue3生态的组件库工程化是提升团队研发效率的基础设施。一套成熟的组件库需要解决开发、构建、测试、文档、发布五个环节的工程化问题。技术选型阶段需要确定构建工具、测试框架、文档方案和发布策略四个维度。

构建工具选择Vite,开发阶段HMR速度远超Webpack;测试框架使用Vitest(兼容Jest API)+ @vue/test-utils;文档方案选择VitePress,天然支持Vue组件预览;包管理使用pnpm的Monorepo模式管理组件包、工具包和文档包。

项目结构初始化:

vue3-ui-library/
├── packages/
│   ├── components/       # 组件源码
│   │   ├── button/
│   │   │   ├── src/
│   │   │   │   ├── button.vue
│   │   │   │   └── button.ts
│   │   │   ├── style/
│   │   │   │   └── index.css
│   │   │   └── index.ts
│   │   ├── input/
│   │   └── index.ts      # 统一导出
│   ├── utils/            # 工具函数
│   ├── hooks/            # 组合式函数
│   └── theme/            # 主题变量
├── docs/                 # VitePress文档
├── playground/           # 调试环境
├── vite.config.ts
├── tsconfig.json
└── pnpm-workspace.yaml

pnpm-workspace.yaml配置:

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

组件设计规范:Props、Slots与TypeScript类型定义

Vue3组件库设计的核心在于API一致性。每个组件遵循统一的Props命名规范、事件命名规范和Slot命名规范。TypeScript实战中,使用defineComponent和泛型约束确保类型安全。

以Button组件为例,完整的类型定义与组件实现:

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

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

export const buttonProps = {
  type: {
    type: String as PropType<ButtonType>,
    default: 'default',
    validator: (val: string) => ['default', 'primary', 'success', 'warning', 'danger'].includes(val)
  },
  size: {
    type: String as PropType<ButtonSize>,
    default: 'default'
  },
  disabled: {
    type: Boolean,
    default: false
  },
  loading: {
    type: Boolean,
    default: false
  },
  round: {
    type: Boolean,
    default: false
  },
  nativeType: {
    type: String as PropType<'button' | 'submit' | 'reset'>,
    default: 'button'
  }
} as const

export type ButtonProps = ExtractPropTypes<typeof buttonProps>

export const buttonEmits = {
  click: (e: MouseEvent) => e instanceof MouseEvent
}

export type ButtonEmits = typeof buttonEmits
<!-- packages/components/button/src/button.vue -->
<template>
  <button
    class="ui-button"
    :class="[
      `ui-button--${type}`,
      `ui-button--${size}`,
      { 'is-disabled': disabled, 'is-loading': loading, 'is-round': round }
    ]"
    :type="nativeType"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="ui-button__loading-icon">
      <ui-icon name="loading" />
    </span>
    <slot name="icon" />
    <span class="ui-button__content"><slot /></span>
  </button>
</template>

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

defineOptions({ name: 'UiButton' })

const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)

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

CSS变量与BEM命名:响应式布局与主题定制

组件样式采用CSS自定义属性(CSS Variables)实现主题切换,配合BEM命名规范保证样式隔离。所有设计token通过CSS变量定义,主题切换只需修改变量值。

/* packages/theme/variables.css */
:root {
  /* 颜色系统 */
  --ui-color-primary: #409eff;
  --ui-color-success: #67c23a;
  --ui-color-warning: #e6a23c;
  --ui-color-danger: #f56c6c;
  --ui-color-text: #303133;
  --ui-color-text-secondary: #909399;
  --ui-color-border: #dcdfe6;
  --ui-color-bg: #ffffff;

  /* 字号 */
  --ui-font-size-lg: 18px;
  --ui-font-size-base: 14px;
  --ui-font-size-sm: 12px;

  /* 圆角 */
  --ui-border-radius-base: 4px;
  --ui-border-radius-round: 20px;

  /* 间距 */
  --ui-spacing-base: 12px;
}

/* 暗色主题 */
.dark {
  --ui-color-text: #e5eaf3;
  --ui-color-text-secondary: #a3a6ad;
  --ui-color-border: #4c4d4f;
  --ui-color-bg: #141414;
}
/* packages/components/button/style/index.css */
.ui-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--ui-spacing-base) 20px;
  font-size: var(--ui-font-size-base);
  border: 1px solid var(--ui-color-border);
  border-radius: var(--ui-border-radius-base);
  cursor: pointer;
  transition: all 0.2s ease;
}

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

.ui-button--primary:hover {
  opacity: 0.85;
}

.ui-button--small {
  padding: 8px 16px;
  font-size: var(--ui-font-size-sm);
}

.ui-button.is-disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

前端工程化:Vite构建配置与按需引入

Vite构建组件库需要输出ES Module和CommonJS两种格式,同时生成类型声明文件。使用@vitejs/plugin-vue处理SFC,vite-plugin-dts生成.d.ts文件。

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

export default defineConfig({
  plugins: [
    vue(),
    dts({
      include: ['packages/components/**/*.ts', 'packages/components/**/*.vue'],
      outputDir: 'dist/types',
      tsconfigPath: './tsconfig.json'
    })
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'packages/components/index.ts'),
      name: 'UiLib',
      fileName: (format) => `index.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        exports: 'named',
        assetFileNames: (assetInfo) => {
          if (assetInfo.name === 'style.css') return 'index.css'
          return assetInfo.name || '[name][extname]'
        }
      }
    },
    cssCodeSplit: false
  }
})

按需引入插件实现,消费方使用unplugin-vue-components自动导入:

// 消费方 vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { UiLibResolver } from 'vue3-ui-library/resolver'

export default {
  plugins: [
    Components({
      resolvers: [UiLibResolver()]
    })
  ]
}

组件测试与CI/CD流水线集成

Vitest单元测试覆盖组件渲染、Props传递、事件触发和Slot内容。测试文件与组件同级,命名__tests__/button.test.ts

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

describe('Button', () => {
  it('renders default button', () => {
    const wrapper = mount(Button)
    expect(wrapper.classes()).toContain('ui-button')
    expect(wrapper.classes()).toContain('ui-button--default')
  })

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

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

  it('renders slot content', () => {
    const wrapper = mount(Button, {
      slots: { default: 'Submit' }
    })
    expect(wrapper.text()).toContain('Submit')
  })
})

npm发布前配置package.json的exports字段,支持子路径导入:

{
  "name": "vue3-ui-library",
  "version": "1.0.0",
  "exports": {
    ".": {
      "import": "./dist/index.es.js",
      "require": "./dist/index.cjs.js"
    },
    "./style": "./dist/index.css",
    "./resolver": "./dist/resolver.js"
  },
  "types": "./dist/types/index.d.ts",
  "files": ["dist"],
  "sideEffects": ["**/*.css"]
}

CI/CD流水线使用GitHub Actions,在push到main分支时自动执行类型检查、单元测试、构建和发布。跨端小程序开发场景中,组件库可通过条件编译适配多端,CSS使用rpx单位兼容小程序布局。Flutter移动端虽然技术栈不同,但组件库的API设计思路(Props/Events/Slots模式)可以复用借鉴。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-ku-gong-cheng-hua-shi-jian-cong-monorepo-jia/

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

相关推荐