Vue3 + TypeScript企业级组件库设计模式:从设计令牌到构建发布

企业级Vue3组件库的设计令牌体系、泛型Props接口契约、BEM+CSS Modules样式隔离、Vite库模式构建和VitePress文档生成完整闭环。

企业级组件库需要解决哪些问题

前端项目从单体走向多团队协作时,组件库是基础设施。但很多团队搭建组件库的思路停留在”封装Element Plus”层面,忽略了类型安全、样式隔离、版本管理、文档生成这些工程化问题。一个真正能支撑业务长期迭代的企业级组件库,需要从设计令牌(Design Token)、组件接口契约、样式方案到构建发布形成完整闭环。Vue3 + TypeScript的组合在类型推导和响应式系统上有天然优势,合理利用后可以大幅减少运行时错误。

设计令牌与主题系统搭建

设计令牌是组件库的底层基础设施。它把颜色、间距、字体、圆角、阴影等设计决策抽象为可命名的变量,确保组件库中所有组件的视觉一致性。

// src/tokens/index.ts
// 设计令牌定义——所有视觉决策从这里产出
export const tokens = {
  color: {
    primary: '#1677ff',
    primaryHover: '#4096ff',
    primaryActive: '#0958d9',
    success: '#52c41a',
    warning: '#faad14',
    danger: '#ff4d4f',
    textPrimary: 'rgba(0, 0, 0, 0.88)',
    textSecondary: 'rgba(0, 0, 0, 0.65)',
    border: '#d9d9d9',
    bgBase: '#ffffff',
    bgContainer: '#ffffff',
    bgLayout: '#f5f5f5',
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 12,
    lg: 16,
    xl: 24,
    xxl: 32,
  },
  fontSize: {
    xs: 12,
    sm: 14,
    md: 16,
    lg: 20,
    xl: 24,
  },
  borderRadius: {
    sm: 2,
    md: 4,
    lg: 8,
    round: 9999,
  },
  shadow: {
    sm: '0 1px 2px rgba(0, 0, 0, 0.06)',
    md: '0 4px 12px rgba(0, 0, 0, 0.08)',
    lg: '0 8px 24px rgba(0, 0, 0, 0.12)',
  },
} as const;

// 类型导出——用于组件Props的类型约束
export type ColorToken = keyof typeof tokens.color;
export type SpacingToken = keyof typeof tokens.spacing;

令牌定义好之后,通过CSS变量注入到文档根节点,实现运行时主题切换:

// src/tokens/provider.ts
import { tokens } from './index';
import type { App } from 'vue';

// 将设计令牌转换为CSS变量
function tokensToCSSVars(tk: Record<string, any>, prefix = '--ui'): Record<string, string> {
  const result: Record<string, string> = {};
  const walk = (obj: Record<string, any>, path: string) => {
    for (const [key, value] of Object.entries(obj)) {
      const varName = `${path}-${key}`;
      if (typeof value === 'object' && value !== null) {
        walk(value, varName);
      } else {
        result[varName] = String(value);
      }
    }
  };
  walk(tk, prefix);
  return result;
}

export const ThemeProvider = {
  install(app: App) {
    const cssVars = tokensToCSSVars(tokens);
    const root = document.documentElement;
    for (const [key, value] of Object.entries(cssVars)) {
      root.style.setProperty(key, value);
    }
    // 提供运行时主题切换能力
    app.config.globalProperties.$switchTheme = (newTokens: Partial<typeof tokens>) => {
      const merged = deepMerge(tokens, newTokens);
      const vars = tokensToCSSVars(merged);
      for (const [k, v] of Object.entries(vars)) {
        root.style.setProperty(k, v);
      }
    };
  }
};

组件接口契约与泛型Props设计

组件的Props类型定义就是接口契约。好的契约让调用者不需要看文档就能知道怎么用,且编译器能在写代码时就捕获错误。以Table组件为例,它需要支持泛型行数据类型:

// src/components/Table/types.ts
import type { ExtractPropTypes, PropType } from 'vue';

// 核心类型定义
export interface TableColumn<T> {
  key: keyof T & string;
  title: string;
  width?: number;
  fixed?: 'left' | 'right';
  sortable?: boolean;
  render?: (value: T[keyof T], row: T, index: number) => VNode;
}

export interface TableProps<T extends Record<string, any>> {
  data: T[];
  columns: TableColumn<T>[];
  loading?: boolean;
  bordered?: boolean;
  rowKey: keyof T & string;
  selectedKeys?: Array<T[keyof T]>;
  pagination?: { page: number; pageSize: number; total: number };
}

// 运行时Props定义——Vue3要求运行时和类型双重声明
export const tableProps = {
  data: { type: Array as PropType<Record<string, any>[]>, required: true },
  columns: { type: Array as PropType<TableColumn<any>[]>, required: true },
  loading: { type: Boolean, default: false },
  bordered: { type: Boolean, default: false },
  rowKey: { type: String, required: true },
  selectedKeys: { type: Array as PropType<any[]>, default: () => [] },
  pagination: { type: Object as PropType<{ page: number; pageSize: number; total: number }> },
} as const;

export type TablePropsType = ExtractPropTypes<typeof tableProps>;
// src/components/Table/Table.vue
<script setup lang="ts" generic="T extends Record<string, any>">
import { tableProps } from './types';
import { usePagination } from './composables/usePagination';
import { useSelection } from './composables/useSelection';

const props = defineProps(tableProps);
const emit = defineEmits<{
  'update:selectedKeys': [keys: Array<T[keyof T]>];
  'page-change': [page: number, pageSize: number];
  'sort-change': [key: keyof T, order: 'asc' | 'desc' | null];
}>();

const { currentPage, pageSize, paginatedData, totalPages } = usePagination(
  () => props.data,
  () => props.pagination
);

const { isSelected, toggleSelect, selectAll, selectedCount } = useSelection(
  () => props.data,
  () => props.rowKey,
  (keys) => emit('update:selectedKeys', keys)
);
</script>

组合式API下的逻辑复用模式

Vue3的组合式API让逻辑复用不再依赖Mixins。把每个可复用的逻辑块封装为composable函数,通过参数传入响应式数据,返回响应式状态和方法。这是组件库中最核心的复用模式。

// src/composables/useValidation.ts
import { ref, computed, type Ref } from 'vue';

export interface ValidationRule<T> {
  validator: (value: T) => boolean | string;
  trigger?: 'blur' | 'change';
}

export function useValidation<T>(value: Ref<T>, rules: ValidationRule<T>[]) {
  const errors = ref<string[]>([]);
  const isValidating = ref(false);

  const validate = async (): Promise<boolean> => {
    errors.value = [];
    isValidating.value = true;
    for (const rule of rules) {
      const result = rule.validator(value.value);
      if (typeof result === 'string') {
        errors.value.push(result);
      } else if (!result) {
        errors.value.push('Validation failed');
      }
    }
    isValidating.value = false;
    return errors.value.length === 0;
  };

  const resetValidation = () => {
    errors.value = [];
  };

  const hasError = computed(() => errors.value.length > 0);

  return { errors, isValidating, hasError, validate, resetValidation };
}

// 使用示例
const email = ref('');
const { errors, validate, hasError } = useValidation(email, [
  {
    validator: (v) => v.length > 0 ? true : 'Email is required',
    trigger: 'blur'
  },
  {
    validator: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)
      ? true : 'Invalid email format',
    trigger: 'blur'
  }
]);

样式隔离方案:BEM + CSS Modules

组件库的样式必须与宿主应用完全隔离。BEM命名约定 + CSS Modules的组合既保持了样式可读性,又避免了全局污染。

// src/components/Button/Button.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useNamespace } from '../../composables/useNamespace';

interface ButtonProps {
  type?: 'primary' | 'secondary' | 'danger' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  disabled?: boolean;
  block?: boolean;
}

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

const { ns, b, e, m, is } = useNamespace('button');

const classes = computed(() => [
  b(),                           // .ui-button
  m(props.type),                 // .ui-button--primary
  m(props.size),                 // .ui-button--md
  { [is('loading')]: props.loading },  // .ui-button.is-loading
  { [is('disabled')]: props.disabled },
  { [is('block')]: props.block },
]);
</script>

<template>
  <button :class="classes" :disabled="disabled || loading">
    <span v-if="loading" :class="e('icon')">
      <LoadingIcon />
    </span>
    <span :class="e('content')">
      <slot />
    </span>
  </button>
</template>
// src/composables/useNamespace.ts
// BEM命名空间工具
export function useNamespace(block: string, prefix = 'ui') {
  const ns = `${prefix}-${block}`;

  const b = () => ns;                              // .ui-button
  const e = (element: string) => `${ns}__${element}`; // .ui-button__icon
  const m = (modifier: string) => `${ns}--${modifier}`; // .ui-button--primary
  const is = (state: string) => `is-${state}`;         // .is-loading

  return { ns, b, e, m, is };
}

组件库构建与发布流程

组件库的构建产物需要同时支持ESM和CJS两种模块格式,并输出独立的CSS文件和类型声明文件。Vite库模式是目前最高效的方案:

// 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({
      insertTypesEntry: true,
      outDir: 'dist/types',
      tsconfigPath: './tsconfig.build.json',
    }),
  ],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'UIKit',
      formats: ['es', 'cjs'],
      fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: { vue: 'Vue' },
        // 每个组件独立打包,支持tree-shaking
        preserveModules: true,
        preserveModulesRoot: 'src',
      },
    },
    cssCodeSplit: true, // 每个组件CSS独立输出
  },
});

发布时确保package.json的exports字段正确定义了模块路径映射:

// package.json关键配置
{
  "name": "@your-org/ui-kit",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "types": "./dist/types/index.d.ts"
    },
    "./style.css": "./dist/style.css",
    "./components/*": {
      "import": "./dist/components/*/index.mjs",
      "types": "./dist/types/components/*/index.d.ts"
    }
  },
  "sideEffects": ["**/*.css"]
}

自动化文档生成与组件调试

组件库没有文档就等于不存在。VitePress + 自动Props解析是最轻量的方案:

// docs/.vitepress/components-demo/Button.vue
// 在VitePress中直接使用组件编写交互式文档
<script setup>
import { ref } from 'vue';
import { Button } from '@your-org/ui-kit';

const loading = ref(false);
const handleClick = () => {
  loading.value = true;
  setTimeout(() => { loading.value = false; }, 2000);
};
</script>

<template>
  <Button type="primary" :loading="loading" @click="handleClick">
    Submit
  </Button>
</template>

:::details 查看源码
```vue
<Button type="primary" :loading="loading" @click="handleClick">
  Submit
</Button>
```
:::

组件库的工程化闭环是:设计令牌保证视觉一致性→TypeScript接口约束组件契约→BEM+CSS Modules实现样式隔离→Vite库模式输出多种格式→VitePress自动生成文档。每个环节缺一不可,跳过任何一个都会在长期迭代中积累技术债务。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3typescript-qi-ye-ji-zu-jian-ku-she-ji-mo-shi-cong-she/

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

相关推荐