组件库落地实践

42 阅读7分钟

组件库名称: GloryCloud UI
当前版本: 0.0.1
一句话简介: 基于 Nuxt3 + Vue3 + Element Plus 的企业级组件库,专注于提供统一、高效、可维护的 UI 解决方案 技术栈: Nuxt3 + Vue3 + TypeScript + TailwindCSS + Element Plus + Storybook


背景 & 痛点

我们为什么要做这个组件库?

在项目发展过程中,我们遇到了以下痛点:

  • 重复造轮子:各个页面重复实现相似的组件,代码冗余严重
  • 样式不统一:缺乏统一的设计规范,UI 风格不一致
  • 维护成本高:组件散落在各处,修改样式需要改动多个文件
  • 开发效率低:缺乏标准化的组件文档和使用规范
  • 类型安全不足:缺乏完整的 TypeScript 类型定义

为什么不直接用 Element Plus?

虽然 Element Plus 是优秀的组件库,但在实际业务中存在以下局限:

  1. 设计风格固化:难以深度定制符合企业品牌的设计风格
  2. 功能局限性:某些业务场景需要更复杂的组件功能
  3. 样式覆盖困难:深度定制样式时优先级冲突频繁
  4. 主题系统不够灵活:暗色模式和主题切换支持有限

设计原则

可组合性(Composability)

基于 Vue 3 Composition API 设计,每个组件都是独立、可复用的功能单元。

// 组件内部使用 Composition API
export default defineComponent({
  setup(props, { emit }) {
    const { formData, handleSearch, handleReset } = useTableSearch(props, emit);
    return { formData, handleSearch, handleReset };
  },
});

类型安全与开发者体验

全面的 TypeScript 支持,提供完整的类型定义和智能提示。

interface TabItem {
  name: string;
  label: string;
  icon?: string;
  count?: number;
}

interface XxTabsProps {
  type: "card" | "nav" | "toggle" | "line";
  tabs: TabItem[];
  modelValue: string | number;
}

无运行时样式污染(Runtime-style isolation)

采用 CSS-in-JS 和 Scoped CSS 相结合的方式,确保组件样式不会相互污染。

.xx-tabs {
  // 组件样式完全封装
  :deep(.el-tabs__item) {
    // 使用 :deep() 穿透样式,避免全局污染
    padding: 0 16px !important;
  }
}

主题化与暗色模式

基于 CSS 变量的主题。

:root {
  --xx-color-primary: #1cb1c8;
  --xx-color-primary-hover: #40bdd1;
  --xx-bg-color: #ffffff;
}

无障碍访问(a11y)

遵循 WCAG 2.1 标准,提供完整的键盘导航、屏幕阅读器支持和焦点管理。

<template>
  <button
    :aria-label="buttonLabel"
    :aria-pressed="isActive"
    @keydown.enter="handleClick"
    @keydown.space="handleClick"
  >
    <slot />
  </button>
</template>

架构总览

graph TB
    A[GloryCloud UI 组件库] --> B[核心包结构]

    B --> C[storybook-repo<br/>📚 文档与演示]
    B --> D[components/<br/>🧩 组件源码]
    B --> E[assets/<br/>🎨 样式资源]

    C --> C1[stories/ - 组件故事]
    C --> C2[.storybook/ - 配置]
    C --> C3[docs/ - 文档]

    D --> D1[global/Xx/ - 基础组件]
    D --> D2[global/ - 业务组件]

    E --> E1[scss/ - 样式文件]
    E --> E2[fonts/ - 字体资源]

    F[构建工具链] --> G[Vite + Vue3]
    F --> H[Storybook 8.x]
    F --> I[TypeScript]
    F --> J[TailwindCSS]

    K[开发流程] --> L[Git Submodule]
    K --> M[独立仓库管理]
    K --> N[CI/CD 自动化]

    style A fill:#667eea,stroke:#333,stroke-width:3px,color:#fff
    style F fill:#764ba2,stroke:#333,stroke-width:2px,color:#fff
    style K fill:#f093fb,stroke:#333,stroke-width:2px,color:#fff

Monorepo 结构说明

  • 独立仓库管理:Storybook 采用独立仓库,通过 Git Submodule 集成
  • 组件源码分离:业务代码与组件文档完全分离,保持代码仓库整洁
  • 构建工具统一:Vite + TypeScript + TailwindCSS 提供现代化构建体验
  • 文档自动化:Storybook + Chromatic 实现组件文档的自动构建和部署

核心技术方案

TailwindCSS + clsx + cva 实现变体

使用 class-variance-authority 实现类型安全的样式变体管理:

import { cva, type VariantProps } from "class-variance-authority";
import { clsx } from "clsx";

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-md font-medium transition-colors",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input hover:bg-accent hover:text-accent-foreground",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps extends VariantProps<typeof buttonVariants> {
  // 其他 props
}

主题系统(CSS 变量 + Tailwind plugin)

基于 CSS 变量的动态主题系统:

// tailwind.config.js
module.exports = {
  darkMode: "class",
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: "var(--xx-color-primary)",
          hover: "var(--xx-color-primary-hover)",
          active: "var(--xx-color-primary-active)",
        },
        background: "var(--xx-bg-color)",
        foreground: "var(--xx-text-color)",
      },
    },
  },
  plugins: [
    function ({ addBase }) {
      addBase({
        ":root": {
          "--xx-color-primary": "#1cb1c8",
          "--xx-color-primary-hover": "#40bdd1",
          "--xx-bg-color": "#ffffff",
          "--xx-text-color": "#000000",
        },
        '[data-theme="dark"]': {
          "--xx-bg-color": "#1a1a1a",
          "--xx-text-color": "#ffffff",
        },
      });
    },
  ],
};

暗色模式自动探测与切换

// composables/useTheme.ts
export const useTheme = () => {
  const theme = ref<"light" | "dark" | "auto">("auto");

  const applyTheme = (newTheme: string) => {
    const root = document.documentElement;

    if (newTheme === "auto") {
      const prefersDark = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches;
      root.setAttribute("data-theme", prefersDark ? "dark" : "light");
    } else {
      root.setAttribute("data-theme", newTheme);
    }
  };

  const toggleTheme = () => {
    theme.value = theme.value === "light" ? "dark" : "light";
    applyTheme(theme.value);
  };

  // 监听系统主题变化
  watchEffect(() => {
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    const handleChange = () => {
      if (theme.value === "auto") {
        applyTheme("auto");
      }
    };

    mediaQuery.addEventListener("change", handleChange);
    return () => mediaQuery.removeEventListener("change", handleChange);
  });

  return { theme, toggleTheme, applyTheme };
};

按需加载与 Tree-shaking 方案

// vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue(),
    // 自动导入组件
    Components({
      resolvers: [
        // 自定义解析器
        (componentName) => {
          if (componentName.startsWith("Xx")) {
            return {
              name: componentName,
              from: `@/components/global/Xx/${componentName.slice(2)}/index.vue`,
            };
          }
        },
      ],
    }),
  ],
  build: {
    lib: {
      entry: "src/index.ts",
      formats: ["es", "cjs"],
    },
    rollupOptions: {
      external: ["vue", "element-plus"],
      output: {
        globals: {
          vue: "Vue",
          "element-plus": "ElementPlus",
        },
      },
    },
  },
});

组件总览

组件名状态Storybook 链接备注
XxButton✅ Completed查看文档基础按钮组件
XxInput✅ Completed查看文档输入框组件
XxSelect✅ Completed查看文档下拉选择器
XxTabs✅ Completed查看文档多样式标签页
XxTable✅ Completed查看文档数据表格
XxTableSearchForm✅ Completed查看文档表格搜索表单
XxDialog✅ Completed查看文档对话框组件
XxForm✅ Completed查看文档表单组件
XxTag✅ Completed查看文档标签组件
XxAlert✅ Completed查看文档警告提示
XxBreadcrumb✅ Completed查看文档面包屑导航
XxCascader✅ Completed查看文档级联选择器
XxInputNumber✅ Completed查看文档数字输入框
XxRadio✅ Completed查看文档单选框组件
XxSlider✅ Completed查看文档滑块组件
XxText✅ Completed查看文档文本组件
AvifPicture✅ Completed查看文档现代图片格式组件
PayMethod✅ Completed查看文档支付方式选择
CommonTip✅ Completed查看文档通用提示组件
SvgIcon✅ Completed查看文档SVG 图标组件
XxLink✅ Completed查看文档链接组件
XxCheckbox📋 Planned查看文档复选框组件
XxViewToggle📋 Planned查看文档视图切换组件
XxMobileDrawer📋 Planned查看文档移动端抽屉

拿手好戏:3 个最得意的组件深度拆解

1. XxTabs - 多样式标签页组件

设计思路

XxTabs 是我们最引以为豪的组件之一,它解决了不同场景下标签页样式需求的问题。通过一个组件支持四种不同的视觉风格:

  • Card 卡片式:适合桌面端,有明显的卡片边框
  • Line 线条式:适合移动端,支持数量显示
  • Nav 导航式:适合页面导航,简洁的分隔线设计
  • Toggle 切换式:适合工具栏,紧凑的按钮组样式

变体实现

<template>
  <div class="xx-tabs" :class="tabsClass">
    <component
      :is="tabComponent"
      v-model="currentValue"
      :tabs="tabs"
      @change="handleChange"
    />
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import XxCardTabs from "./CardTabs.vue";
import XxLineTabs from "./LineTabs.vue";
import XxNavTabs from "./NavTabs.vue";
import XxToggleTabs from "./ToggleTabs.vue";

interface TabItem {
  name: string;
  label: string;
  icon?: string;
  count?: number;
}

interface Props {
  type: "card" | "nav" | "toggle" | "line";
  tabs: TabItem[];
  modelValue: string | number;
}

const props = withDefaults(defineProps<Props>(), {
  type: "card",
});

// 动态组件映射
const tabComponent = computed(() => {
  const componentMap = {
    card: XxCardTabs,
    line: XxLineTabs,
    nav: XxNavTabs,
    toggle: XxToggleTabs,
  };
  return componentMap[props.type];
});

// 样式类计算
const tabsClass = computed(() => [
  `xx-tabs--${props.type}`,
  {
    "xx-tabs--mobile": isMobile.value,
  },
]);
</script>

性能优化点

  1. 动态组件加载:根据 type 动态加载对应的子组件,减少初始包体积
  2. 样式按需加载:每种类型的样式独立,避免样式冗余
  3. 响应式优化:使用 computed 缓存计算结果,避免重复计算

2. XxTableSearchForm - 配置化搜索表单

设计思路

XxTableSearchForm 解决了表格页面搜索表单的标准化问题。通过配置化的方式,开发者只需要定义字段配置,就能快速生成功能完整的搜索表单。

核心实现

<template>
  <el-form
    ref="formRef"
    :model="formData"
    class="xx-table-search-form"
    :class="formClass"
  >
    <el-row :gutter="16">
      <el-col
        v-for="field in visibleFields"
        :key="field.prop"
        :span="field.span || 6"
      >
        <el-form-item :label="field.label" :prop="field.prop">
          <!-- 动态渲染不同类型的表单控件 -->
          <component
            :is="getFieldComponent(field.type)"
            v-model="formData[field.prop]"
            v-bind="getFieldProps(field)"
            @change="handleFieldChange(field, $event)"
          />
        </el-form-item>
      </el-col>

      <!-- 操作按钮 -->
      <el-col :span="actionSpan">
        <el-form-item>
          <el-button type="primary" :loading="loading" @click="handleSearch">
            {{ searchText }}
          </el-button>
          <el-button @click="handleReset">
            {{ resetText }}
          </el-button>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script setup lang="ts">
interface FieldConfig {
  prop: string;
  label: string;
  type: "input" | "select" | "date-range" | "cascader";
  placeholder?: string;
  options?: Array<{ label: string; value: any }>;
  span?: number;
  [key: string]: any;
}

// 动态组件映射
const getFieldComponent = (type: string) => {
  const componentMap = {
    input: "el-input",
    select: "el-select",
    "date-range": "el-date-picker",
    cascader: "el-cascader",
  };
  return componentMap[type] || "el-input";
};

// 动态属性计算
const getFieldProps = (field: FieldConfig) => {
  const baseProps = {
    placeholder: field.placeholder,
    clearable: true,
  };

  // 根据类型添加特定属性
  switch (field.type) {
    case "select":
      return {
        ...baseProps,
        options: field.options,
      };
    case "date-range":
      return {
        ...baseProps,
        type: "daterange",
        rangeSeparator: "至",
        startPlaceholder: "开始日期",
        endPlaceholder: "结束日期",
      };
    default:
      return baseProps;
  }
};
</script>

性能优化点

  1. 虚拟滚动支持:当字段数量过多时,支持虚拟滚动渲染
  2. 防抖搜索:搜索操作自动防抖,避免频繁请求
  3. 缓存机制:表单数据支持本地缓存,提升用户体验

3. AvifPicture - 现代图片格式组件

设计思路

AvifPicture 组件解决了现代 Web 应用中图片格式优化的问题。它支持 AVIF、WebP、JPG 三种格式的智能回退,在保证兼容性的同时最大化性能优势。

核心实现

<template>
  <picture :class="pictureClass">
    <!-- AVIF 格式 - 最优压缩比 -->
    <source v-if="avifSrc" :srcset="avifSrc" type="image/avif" />

    <!-- WebP 格式 - 广泛支持 -->
    <source v-if="webpSrc" :srcset="webpSrc" type="image/webp" />

    <!-- 回退格式 - 兼容性保证 -->
    <img
      :src="fallbackSrc"
      :alt="alt"
      :loading="loading"
      :class="imgClass"
      @load="handleLoad"
      @error="handleError"
    />
  </picture>
</template>

<script setup lang="ts">
interface Props {
  avifSrc?: string;
  webpSrc?: string;
  fallbackSrc: string;
  alt: string;
  loading?: "lazy" | "eager" | "auto";
  pictureClass?: string;
  imgClass?: string;
}

const props = withDefaults(defineProps<Props>(), {
  loading: "lazy",
});

// 智能格式推断
const generateSources = () => {
  if (!props.avifSrc && !props.webpSrc) {
    // 自动生成现代格式路径
    const basePath = props.fallbackSrc.replace(/\.(jpg|jpeg|png)$/i, "");
    return {
      avif: `${basePath}.avif`,
      webp: `${basePath}.webp`,
    };
  }
  return {
    avif: props.avifSrc,
    webp: props.webpSrc,
  };
};

// 加载状态管理
const loadingState = ref<"loading" | "loaded" | "error">("loading");

const handleLoad = () => {
  loadingState.value = "loaded";
  emit("load");
};

const handleError = () => {
  loadingState.value = "error";
  emit("error");
};
</script>

性能优化点

  1. 格式智能选择:浏览器自动选择支持的最优格式
  2. 懒加载支持:默认启用懒加载,提升页面加载速度
  3. 错误处理:完善的错误处理和回退机制
  4. 尺寸优化:支持响应式图片,根据屏幕尺寸加载合适的图片

开发与贡献指南

本地开发流程

# 1. 克隆项目
git clone [项目地址]
cd glorycloud-nuxt-web

# 2. 初始化 submodule
git submodule update --init --recursive

# 3. 安装依赖
pnpm install

# 4. 进入 storybook 仓库安装依赖
cd storybook-repo
pnpm install
cd ..

# 5. 启动开发环境
pnpm run storybook:dev

Storybook 地址

如何添加新组件

1. 创建组件文件

# 在 components/global/Xx/ 目录下创建新组件
mkdir components/global/Xx/YourComponent
cd components/global/Xx/YourComponent

2. 组件模板代码

<!-- index.vue -->
<template>
  <div class="xx-your-component" :class="componentClass">
    <slot />
  </div>
</template>

<script setup lang="ts">
interface Props {
  variant?: "default" | "primary" | "secondary";
  size?: "small" | "medium" | "large";
}

const props = withDefaults(defineProps<Props>(), {
  variant: "default",
  size: "medium",
});

const componentClass = computed(() => [
  `xx-your-component--${props.variant}`,
  `xx-your-component--${props.size}`,
]);
</script>

<style lang="scss" scoped>
.xx-your-component {
  // 组件样式
  &--default {
    // 默认变体样式
  }

  &--primary {
    // 主要变体样式
  }

  &--small {
    // 小尺寸样式
  }
}
</style>

3. 创建 Storybook 故事

// storybook-repo/stories/components/YourComponent.stories.ts
import type { Meta, StoryObj } from "@storybook/vue3";
import YourComponent from "@/components/global/Xx/YourComponent/index.vue";

const meta: Meta<typeof YourComponent> = {
  title: "基础组件/你的组件 YourComponent",
  component: YourComponent,
  parameters: {
    layout: "padded",
    docs: {
      description: {
        component: "组件描述和使用说明",
      },
    },
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["default", "primary", "secondary"],
      description: "组件变体",
    },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const 默认用法: Story = {
  args: {
    variant: "default",
  },
};

发版流程

我们使用 Git Submodule + 独立仓库的方式管理版本:

# 1. 开发完成后,提交 Storybook 仓库
cd storybook-repo
git add .
git commit -m "feat: 添加新组件 YourComponent"
git push origin main

# 2. 回到主项目,更新 submodule 引用
cd ..
git add storybook-repo
git commit -m "docs: 更新 storybook 文档"
git push origin main

# 3. 自动部署
# GitLab CI/CD 会自动构建和部署 Storybook

未来规划(Roadmap)

timeline
    title GloryCloud UI 发展规划

    section 2024 Q1
        组件库基础建设 : 完成核心组件开发
                      : 建立 Storybook 文档体系
                      : 实现主题系统

    section 2024 Q2
        功能完善阶段 : 新增 15+ 业务组件
                   : 完善 TypeScript 类型定义
                   : 优化构建和打包流程

    section 2024 Q3
        生态建设 : 发布 npm 包
               : 提供 CLI 工具
               : 建立组件市场

    section 2024 Q4
        性能优化 : 实现按需加载
               : 优化包体积
               : 提升运行时性能

    section 2025 Q1
        移动端适配 : 完善响应式设计
                 : 新增移动端专用组件
                 : 支持触摸手势

    section 2025 Q2
        国际化支持 : 多语言支持
                 : 全球化主题
                 : 文档国际化

近期目标(Q1 2024)

  • ✅ 完成 25+ 核心组件开发
  • ✅ 建立完整的 Storybook 文档体系
  • ✅ 实现主题系统和暗色模式
  • 🔄 优化组件 API 设计
  • 📋 完善单元测试覆盖

中期目标(Q2-Q3 2024)

  • 📋 发布正式版本到 npm
  • 📋 提供 Vue DevTools 插件
  • 📋 建立组件使用统计和反馈机制
  • 📋 开发 CLI 工具简化组件使用
  • 📋 建立设计系统文档

长期目标(Q4 2024 - Q2 2025)

  • 📋 支持 React 版本组件库
  • 📋 提供 Figma 设计资源
  • 📋 建立组件市场和插件生态
  • 📋 支持低代码平台集成
  • 📋 提供企业级定制服务

技术亮点总结

🎨 设计系统完整性

  • 统一的设计语言和视觉规范
  • 完整的主题系统
  • 响应式设计和移动端适配

⚡ 开发体验优化

  • 完整的 TypeScript 类型支持
  • 丰富的 Storybook 文档和示例
  • 热重载和快速开发反馈

🔧 工程化水平

  • 独立仓库管理和版本控制
  • 自动化构建和部署流程
  • 完善的代码规范和质量保证

📈 性能表现

  • 按需加载和 Tree-shaking 支持
  • 现代图片格式优化
  • 运行时性能优化