Vue3组件库设计实战:从零搭建响应式布局业务组件体系

Vue3生态组件库设计前端工程化的核心环节。一套设计良好的业务组件库能提升团队开发效率、保证UI一致性、降低维护成本。本文从组件架构设计到工程化构建,完整演示Vue3组件库的搭建流程,涵盖Composables复用、响应式布局方案和构建发布配置。

组件库架构设计:分层设计与目录结构规划

组件库采用三层架构:基础组件层(Base Components)、业务组件层(Business Components)、布局组件层(Layout Components)。各层职责清晰,依赖方向单一:

# 项目目录结构
vue3-ui-library/
├── packages/
│   ├── base/              # 基础组件
│   │   ├── button/
│   │   │   ├── src/Button.vue
│   │   │   ├── style/index.scss
│   │   │   └── index.ts
│   │   └── input/
│   ├── business/          # 业务组件
│   │   ├── search-form/
│   │   └── data-table/
│   ├── layout/            # 布局组件
│   │   ├── grid/
│   │   └── container/
│   └── shared/            # 共享工具
│       ├── composables/
│       │   ├── useResponsive.ts
│       │   ├── useTheme.ts
│       │   └── useForm.ts
│       └── utils/
├── play/                  # 组件调试环境
├── docs/                  # 文档站点
└── vite.config.ts

基础组件实现:Button组件的设计模式

以Button组件为例,展示Vue3组件的标准实现模式。利用definePropsdefineEmits和CSS变量实现主题定制:

<!-- packages/base/button/src/Button.vue -->
<template>
  <button
    :class="[ns.b(), ns.m(type), ns.m(size),
             ns.is('disabled', disabled), ns.is('loading', loading)]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <span v-if="loading" class="btn-loading-icon" />
    <slot name="icon" />
    <span class="btn-content"><slot /></span>
  </button>
</template>

<script setup lang="ts">
import { useNamespace } from '../../../shared/composables/useNamespace'

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

const props = withDefaults(defineProps<{
  type?: ButtonType
  size?: ButtonSize
  disabled?: boolean
  loading?: boolean
}>(), {
  type: 'default',
  size: 'medium',
  disabled: false,
  loading: false,
})

const emit = defineEmits<{
  (e: 'click', event: MouseEvent): void
}>()

const ns = useNamespace('btn')

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

<style scoped>
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid var(--btn-border-color, #dcdfe6);
  border-radius: var(--btn-border-radius, 4px);
  cursor: pointer;
  transition: all 0.2s ease;
  padding: 8px 16px;
}
.btn--primary {
  background: var(--color-primary, #409eff);
  border-color: var(--color-primary, #409eff);
  color: #fff;
}
.btn--small { padding: 5px 11px; font-size: 12px; }
.btn--large { padding: 12px 22px; font-size: 16px; }
.btn.is-disabled, .btn.is-loading { cursor: not-allowed; opacity: 0.5; }
</style>

useNamespace是BEM命名空间的Composable,统一管理组件class命名规范:

// packages/shared/composables/useNamespace.ts
const statePrefix = 'is-'

const _bem = (namespace, block, blockSuffix, element, modifier) => {
  let cls = `${namespace}-${block}`
  if (blockSuffix) cls += `-${blockSuffix}`
  if (element) cls += `__${element}`
  if (modifier) cls += `--${modifier}`
  return cls
}

export const useNamespace = (block, namespaceOverride) => {
  const namespace = namespaceOverride || 'vue3-ui'
  const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '')
  const e = (element) => element ? _bem(namespace, block, '', element, '') : ''
  const m = (modifier) => modifier ? _bem(namespace, block, '', '', modifier) : ''
  const is = (name, state) => name && state ? `${statePrefix}${name}` : ''
  return { b, e, m, is }
}

响应式布局方案:CSS Grid与断点系统设计

组件库需要内置响应式布局能力。通过CSS Grid和媒体查询断点系统,实现自适应栅格布局:

<!-- packages/layout/grid/src/Row.vue -->
<template>
  <div :class="rowClass" :style="rowStyle">
    <slot />
  </div>
</template>

<script setup lang="ts">
import { computed, provide, reactive } from 'vue'

const props = withDefaults(defineProps<{
  gutter?: number
  justify?: 'start' | 'end' | 'center' | 'space-around' | 'space-between'
  align?: 'top' | 'middle' | 'bottom'
}>(), { gutter: 0, justify: 'start', align: 'top' })

provide('rowContext', reactive({
  gutter: computed(() => props.gutter),
}))

const rowClass = computed(() => [
  'vue3-ui-row', `is-justify-${props.justify}`, `is-align-${props.align}`
])
const rowStyle = computed(() => ({
  marginLeft: `-${props.gutter / 2}px`,
  marginRight: `-${props.gutter / 2}px`,
}))
</script>

<!-- packages/layout/grid/src/Col.vue -->
<template>
  <div :class="colClass" :style="colStyle">
    <slot />
  </div>
</template>

<script setup lang="ts">
import { computed, inject } from 'vue'

const props = withDefaults(defineProps<{
  span?: number
  offset?: number
  xs?: number | { span?: number; offset?: number }
  sm?: number | { span?: number; offset?: number }
  md?: number | { span?: number; offset?: number }
  lg?: number | { span?: number; offset?: number }
  xl?: number | { span?: number; offset?: number }
}>(), { span: 24, offset: 0 })

const rowContext = inject('rowContext', { gutter: computed(() => 0) })

const colClass = computed(() => {
  const classes = ['vue3-ui-col', `vue3-ui-col-${props.span}`]
  if (props.offset) classes.push(`vue3-ui-col-offset-${props.offset}`)
  const bps = ['xs', 'sm', 'md', 'lg', 'xl']
  bps.forEach(bp => {
    const val = props[bp]
    if (typeof val === 'number') classes.push(`vue3-ui-col-${bp}-${val}`)
    else if (val && typeof val === 'object') {
      if (val.span) classes.push(`vue3-ui-col-${bp}-${val.span}`)
      if (val.offset) classes.push(`vue3-ui-col-${bp}-offset-${val.offset}`)
    }
  })
  return classes
})

const colStyle = computed(() => ({
  paddingLeft: `${rowContext.gutter.value / 2}px`,
  paddingRight: `${rowContext.gutter.value / 2}px`,
}))
</script>

对应的SCSS断点定义生成24栅格响应式类:

// packages/layout/grid/style/index.scss
$breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);

@mixin respond-to($bp) {
  $value: map-get($breakpoints, $bp);
  @if $value == 0 { @content; }
  @else { @media (min-width: $value) { @content; } }
}

@each $bp, $value in $breakpoints {
  @for $i from 0 through 24 {
    @include respond-to($bp) {
      .vue3-ui-col-#{$bp}-#{$i} {
        max-width: percentage($i / 24);
        flex: 0 0 percentage($i / 24);
      }
    }
  }
}

Composables复用:表单逻辑抽象与状态管理

Vue3的Composition API让逻辑复用变得简单。表单验证是高频场景,抽象为useFormComposable:

// packages/shared/composables/useForm.ts
import { reactive, computed } from 'vue'

export function useForm(initialValues, rules) {
  const values = reactive({ ...initialValues })
  const errors = reactive({})

  const validateField = async (field) => {
    const fieldRules = rules[field]
    if (!fieldRules) return true
    const value = values[field]
    for (const rule of fieldRules) {
      if (rule.required && !value) {
        errors[field] = rule.message
        return false
      }
      if (rule.min && value.length < rule.min) {
        errors[field] = rule.message
        return false
      }
      if (rule.pattern && !rule.pattern.test(value)) {
        errors[field] = rule.message
        return false
      }
      if (rule.validator) {
        const result = rule.validator(value)
        if (result !== true) {
          errors[field] = typeof result === 'string' ? result : rule.message
          return false
        }
      }
    }
    delete errors[field]
    return true
  }

  const validate = async () => {
    let valid = true
    for (const field in rules) {
      if (!await validateField(field)) valid = false
    }
    return valid
  }

  const isValid = computed(() => Object.keys(errors).length === 0)
  return { values, errors, validateField, validate, isValid }
}

// 使用示例
const { values, errors, validate } = useForm(
  { username: '', email: '', password: '' },
  {
    username: [
      { required: true, message: '用户名不能为空' },
      { min: 3, max: 20, message: '用户名长度3-20位' },
    ],
    email: [
      { required: true, message: '邮箱不能为空' },
      { pattern: /^\S+@\S+\.\S+$/, message: '邮箱格式不正确' },
    ],
  }
)

工程化构建配置:Vite打包与按需引入

组件库需要支持全量引入和按需引入两种模式。Vite的库模式配合preserveModules可以实现按需加载:

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

export default defineConfig({
  plugins: [vue(), dts({ entryRoot: 'packages', outDir: 'dist/types' })],
  build: {
    lib: {
      entry: resolve(__dirname, 'packages/index.ts'),
      name: 'Vue3UI',
      fileName: (format) => `vue3-ui.${format}.js`,
      formats: ['es', 'cjs', 'umd'],
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        preserveModules: true,
        preserveModulesRoot: 'packages',
        exports: 'named',
        globals: { vue: 'Vue' },
      },
    },
    cssCodeSplit: true,
  },
})

// packages/index.ts
export * from './base/button'
export * from './base/input'
export * from './layout/grid'
export * from './shared/composables/useForm'

import type { App } from 'vue'
import Button from './base/button'
import Input from './base/input'

export default {
  install(app: App) {
    [Button, Input].forEach(c => app.component(c.name, c))
  },
}

构建完成后,消费端可以通过unplugin-vue-components实现自动按需导入,无需手动import。配合TypeScript类型声明文件,组件库的使用体验与原生Vue组件无异。跨端小程序开发场景下,通过条件编译可以复用大部分组件逻辑层代码,仅替换渲染层实现即可。TypeScript实战中,组件Props类型应导出为独立接口,便于消费端进行类型推断和扩展。Web性能优化方面,组件库支持Tree-Shaking,按需引入的产物体积可控制在合理范围内。

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

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

相关推荐