Vue3组件库设计模式与响应式布局工程化实践:构建企业级UI组件体系

企业级Vue3组件库的架构分层设计

Vue3组件库不是把组件写完丢进一个仓库就完事了。一个可持续维护的企业级组件库,至少需要三层架构:基础层、业务层和文档层。基础层提供原子化组件和设计Token,业务层基于基础层组合出业务组件,文档层提供可交互的组件示例和API文档。

目录结构设计:

packages/
  components/          # 基础组件
    button/
    input/
    select/
    ...
  composables/        # 组合式函数
    useResponsive/
    useBreakpoint/
  theme/              # 设计Token
    tokens.css
    dark.css
  utils/              # 工具函数
  business/            # 业务组件
    search-form/
    data-table/
  docs/                # 文档站点
playground/           # 开发调试
internal/             # 构建/发布工具

每层独立打包,按需引用。不要把所有组件打进一个bundle,用户只引入Button就不该加载Table的代码。

组件设计与TypeScript类型约束

组件的Props定义是组件库的API契约,必须用TypeScript严格约束。一个Button组件的完整类型定义:

// types.ts
export type ButtonSize = 'small' | 'medium' | 'large'
export type ButtonType = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
export type ButtonHTMLType = 'button' | 'submit' | 'reset'

export interface ButtonProps {
  type?: ButtonType
  size?: ButtonSize
  disabled?: boolean
  loading?: boolean
  htmlType?: ButtonHTMLType
  block?: boolean
  icon?: Component
  href?: string  // 渲染为a标签时使用
  target?: string
}

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

组件实现中用defineProps和defineEmits约束输入输出:

<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps, ButtonEmits } from './types'
import { useNamespace } from '@lib/composables/useNamespace'

const props = withDefaults(defineProps<ButtonProps>(), {
  type: 'secondary',
  size: 'medium',
  disabled: false,
  loading: false,
  block: false,
})

const emit = defineEmits<ButtonEmits>()

const ns = useNamespace('button')
const classes = computed(() => [
  ns.b(),
  ns.m(props.type),
  ns.m(props.size),
  ns.is('disabled', props.disabled),
  ns.is('loading', props.loading),
  ns.is('block', props.block),
])

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

useNamespace是BEM命名规范的封装,生成规范的CSS类名:

// composables/useNamespace.ts
export function useNamespace(block: string) {
  const prefix = 'my'
  const b = () => `${prefix}-${block}`
  const m = (modifier: string) => `${prefix}-${block}--${modifier}`
  const e = (element: string) => `${prefix}-${block}__${element}`
  const is = (state: string, active: boolean) =>
    active ? `${prefix}-${block}-is-${state}` : ''
  return { b, m, e, is }
}

响应式布局的工程化方案

响应式布局的核心不是写几条media query,而是建立一套从设计到代码的完整映射体系。

断点系统:用CSS自定义属性定义断点,配合Vue3组合式函数在JS中同步使用:

// composables/useBreakpoint.ts
import { ref, computed, onMounted, onUnmounted } from 'vue'

const breakpoints = {
  xs: 0,
  sm: 576,
  md: 768,
  lg: 992,
  xl: 1200,
  xxl: 1600,
} as const

export type Breakpoint = keyof typeof breakpoints

export function useBreakpoint() {
  const current = ref<Breakpoint>('md')
  const width = ref(window.innerWidth)

  const update = () => {
    width.value = window.innerWidth
    const w = width.value
    if (w >= breakpoints.xxl) current.value = 'xxl'
    else if (w >= breakpoints.xl) current.value = 'xl'
    else if (w >= breakpoints.lg) current.value = 'lg'
    else if (w >= breakpoints.md) current.value = 'md'
    else if (w >= breakpoints.sm) current.value = 'sm'
    else current.value = 'xs'
  }

  onMounted(() => {
    update()
    window.addEventListener('resize', update)
  })
  onUnmounted(() => window.removeEventListener('resize', update))

  const isMobile = computed(() => ['xs', 'sm'].includes(current.value))
  const isDesktop = computed(() => !isMobile.value)

  return { current, width, isMobile, isDesktop }
}

栅格组件:Row和Col组件是响应式布局的基础设施。Col组件需要支持不同断点下的span值:

<Row :gutter="{ xs: 8, sm: 16, md: 24 }">
  <Col :xs="24" :sm="12" :md="8" :lg="6">内容A</Col>
  <Col :xs="24" :sm="12" :md="8" :lg="6">内容B</Col>
  <Col :xs="24" :sm="12" :md="8" :lg="6">内容C</Col>
  <Col :xs="24" :sm="12" :md="8" :lg="6">内容D</Col>
</Row>

Col组件的CSS使用media query动态计算宽度:

.my-col {
  box-sizing: border-box;
  width: 100%;
}

/* 24栅格系统,每个span = 100/24 = 4.167% */
@each $span, $i in (1..24) {
  .my-col--span-$(i) {
    flex: 0 0 percentage($i / 24);
    max-width: percentage($i / 24);
  }
}

/* 响应式覆盖 */
@media (min-width: 576px) {
  .my-col-sm--span-12 { flex: 0 0 50%; max-width: 50%; }
}
@media (min-width: 768px) {
  .my-col-md--span-8 { flex: 0 0 33.333%; max-width: 33.333%; }
}

组件库的按需加载与Tree-Shaking配置

组件库必须支持按需引用,否则一个只用了3个组件的项目也会加载全部代码。

Vite环境下的按需引用配置:

// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { MyLibResolver } from 'my-lib/resolver'

export default defineConfig({
  plugins: [
    Components({
      resolvers: [MyLibResolver()],
    }),
  ],
})

Resolver的实现核心是约定组件的导入路径格式:

// resolver.ts
export function MyLibResolver(): ComponentResolver {
  return {
    type: 'component',
    resolve: (name: string) => {
      if (name.startsWith('My')) {
        const partial = name.slice(2).replace(/[A-Z]/g, (c, i) =>
          i ? '-' + c.toLowerCase() : c.toLowerCase()
        )
        return {
          name: 'default',
          from: 'my-lib',
          sideEffects: `my-lib/es/${partial}/style/index`,
        }
      }
    },
  }
}

组件库打包配置中,每个组件需要独立入口:

// build.config.ts
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
  entries: [
    'src/index.ts',           // 全量入口
    'src/components/button/index.ts',
    'src/components/input/index.ts',
    'src/components/select/index.ts',
    // ...每个组件一个入口
  ],
  externals: ['vue'],
  rollup: {
    emitCJS: true,
  },
})

暗色主题的Token化实现

暗色模式不是简单的背景色反转,而是需要完整的设计Token体系支撑:

// theme/tokens.css
:root {
  /* 语义化颜色Token */
  --my-color-bg-primary: #ffffff;
  --my-color-bg-secondary: #f5f5f5;
  --my-color-text-primary: #1a1a1a;
  --my-color-text-secondary: #666666;
  --my-color-border: #e5e5e5;
  --my-color-primary: #1677ff;
  --my-color-primary-hover: #4096ff;
  --my-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
  /* 间距Token */
  --my-spacing-xs: 4px;
  --my-spacing-sm: 8px;
  --my-spacing-md: 16px;
  --my-spacing-lg: 24px;
  /* 圆角Token */
  --my-radius-sm: 2px;
  --my-radius-md: 6px;
  --my-radius-lg: 12px;
}

/* 暗色主题覆盖 */
:root[data-theme='dark'] {
  --my-color-bg-primary: #141414;
  --my-color-bg-secondary: #1f1f1f;
  --my-color-text-primary: #ffffffd9;
  --my-color-text-secondary: #ffffff73;
  --my-color-border: #424242;
  --my-color-primary: #1668dc;
  --my-color-primary-hover: #3c89e8;
  --my-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.36);
}

主题切换通过修改根元素的data-theme属性实现,无需重新加载CSS。组件库所有样式必须引用Token变量,禁止使用硬编码颜色值。这一约束通过Stylelint规则强制执行:

// .stylelintrc.js
module.exports = {
  rules: {
    'color-no-hex': true,
    'color-named': 'never',
    'declaration-property-value-disallowed-list': {
      '/.*/': ['/#[0-9a-fA-F]{3,8}/'],
    },
  },
}

这套方案确保暗色主题切换时,所有组件自动适配,无需为每个组件单独写暗色样式覆盖。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-ku-she-ji-mo-shi-yu-xiang-ying-shi-bu-ju-gong/

(0)
小编小编
上一篇 56分钟前
下一篇 55分钟前

相关推荐