uni-app 组件库开发

章节介绍

组件库是现代前端开发的重要组成部分,它可以提高开发效率、保证代码质量、统一设计风格。在 uni-app 开发中,构建一套符合企业标准的组件库尤为重要,因为它需要兼顾多端适配、性能优化和开发体验。本章节将详细介绍 uni-app 组件库开发的完整流程,从设计规范到最终发布,帮助开发者掌握组件库开发的系统方法。

核心知识点

1. 组件库设计

组件库设计是组件库开发的基础,它决定了组件库的整体架构和使用体验。在设计组件库时,需要考虑以下几个方面:

1.1 设计系统

设计系统是组件库的灵魂,它包括:

  • 设计语言:颜色、字体、间距、阴影等设计元素的规范
  • 组件体系:组件的分类、层级关系和使用场景
  • 交互规范:组件的交互方式、动画效果和反馈机制

1.2 技术架构

技术架构是组件库的骨架,它包括:

  • 目录结构:组件库的文件组织方式
  • 构建工具:使用何种工具进行开发、构建和发布
  • 依赖管理:组件库的依赖项和版本控制
  • 多端适配:如何处理不同平台的差异

1.3 组件规划

组件规划是组件库的蓝图,它包括:

  • 基础组件:按钮、输入框、标签等基础 UI 元素
  • 布局组件:网格、面板、卡片等布局结构
  • 功能组件:日历、表单、表格等具有特定功能的组件
  • 业务组件:与具体业务相关的复合组件

2. 统一规范

统一规范是组件库质量的保证,它确保了组件的一致性和可维护性。在制定统一规范时,需要考虑以下几个方面:

2.1 命名规范

命名规范包括:

  • 组件命名:使用 PascalCase 命名组件,如 ButtonInput
  • 文件命名:组件文件与组件名称保持一致
  • 变量命名:使用 camelCase 命名变量和属性
  • 常量命名:使用 UPPER_CASE 命名常量

2.2 代码规范

代码规范包括:

  • 缩进:使用 2 或 4 个空格进行缩进
  • 换行:合理的换行和空行,提高代码可读性
  • 注释:为组件添加详细的注释,包括功能说明、参数说明和使用示例
  • 类型定义:使用 TypeScript 或 JSDoc 为组件添加类型定义

2.3 样式规范

样式规范包括:

  • 样式命名:使用 BEM 或其他命名方法,如 component__element--modifier
  • 样式管理:使用 CSS 变量、SCSS 或 Less 管理样式
  • 响应式设计:考虑不同屏幕尺寸的适配
  • 多端样式:处理不同平台的样式差异

3. 组件开发

组件开发是组件库的核心,它需要遵循一定的开发流程和最佳实践。在开发组件时,需要考虑以下几个方面:

3.1 开发流程

开发流程包括:

  • 需求分析:明确组件的功能和使用场景
  • 设计阶段:设计组件的 UI 和交互
  • 开发阶段:实现组件的功能和样式
  • 测试阶段:测试组件的功能和兼容性
  • 文档编写:编写组件的使用文档

3.2 最佳实践

最佳实践包括:

  • 单一职责:每个组件只负责一个功能
  • 可配置性:通过 props 实现组件的灵活配置
  • 事件处理:使用自定义事件实现组件与父组件的通信
  • 插槽使用:合理使用插槽提高组件的灵活性
  • 性能优化:避免不必要的渲染和计算

3.3 多端适配

多端适配是 uni-app 组件库的重要特点,它包括:

  • 条件编译:使用条件编译处理平台差异
  • 平台 API:合理使用平台特定的 API
  • 样式适配:处理不同平台的样式差异
  • 兼容性测试:在不同平台上测试组件的表现

4. 文档生成

文档是组件库的重要组成部分,它帮助开发者理解和使用组件库。在生成文档时,需要考虑以下几个方面:

4.1 文档结构

文档结构包括:

  • 快速开始:如何安装和使用组件库
  • 组件文档:每个组件的详细说明和使用示例
  • API 参考:组件的属性、事件和方法的详细说明
  • 主题定制:如何定制组件库的主题
  • 常见问题:使用组件库时常见问题的解答

4.2 文档工具

文档工具包括:

  • 静态站点生成器:如 VuePress、VitePress 等
  • 组件演示:如何在文档中展示组件的使用方法
  • 代码高亮:使用代码高亮提高代码示例的可读性
  • 响应式设计:确保文档在不同设备上的良好体验

4.3 文档维护

文档维护包括:

  • 版本管理:确保文档与组件库版本保持一致
  • 实时更新:当组件库更新时,及时更新文档
  • 反馈机制:收集用户对文档的反馈,不断改进文档质量
  • 多语言支持:为文档提供多语言版本,扩大组件库的使用范围

实用案例:开发企业级组件库

1. 项目初始化

首先,我们需要初始化一个 uni-app 组件库项目。这里我们使用 Vue CLI 来创建项目:

# 安装 Vue CLI
npm install -g @vue/cli

# 创建组件库项目
vue create uni-component-library

# 选择手动配置
# 选择 TypeScript、Babel、ESLint 等选项

2. 项目结构

创建合理的项目结构,便于组件的开发和管理:

├── packages/                  # 组件库包
│   ├── components/            # 组件目录
│   │   ├── button/            # 按钮组件
│   │   ├── input/             # 输入框组件
│   │   ├── card/              # 卡片组件
│   │   └── index.js           # 组件导出
│   ├── themes/                # 主题配置
│   │   ├── default.scss       # 默认主题
│   │   └── index.js           # 主题导出
│   ├── utils/                 # 工具函数
│   │   ├── index.js           # 工具函数导出
│   │   └── platform.js        # 平台判断工具
│   ├── index.js               # 组件库入口
│   └── package.json           # 组件库包配置
├── docs/                      # 文档目录
│   ├── src/                   # 文档源码
│   ├── public/                # 文档静态资源
│   └── package.json           # 文档包配置
├── examples/                  # 示例目录
│   └── pages/                 # 示例页面
├── build/                     # 构建脚本
│   ├── build.js               # 构建脚本
│   └── utils.js               # 构建工具函数
├── test/                      # 测试目录
│   ├── unit/                  # 单元测试
│   └── e2e/                   # 端到端测试
├── .eslintrc.js               # ESLint 配置
├── .prettierrc.js             # Prettier 配置
├── tsconfig.json              # TypeScript 配置
└── package.json               # 项目根配置

3. 基础配置

3.1 package.json

配置组件库的基本信息和依赖:

packages/package.json

{
  "name": "uni-component-library",
  "version": "1.0.0",
  "description": "企业级 uni-app 组件库",
  "main": "lib/index.js",
  "module": "es/index.js",
  "unpkg": "dist/uni-component-library.min.js",
  "types": "types/index.d.ts",
  "scripts": {
    "build": "npm run build:lib && npm run build:es && npm run build:dist",
    "build:lib": "vue-cli-service build --target lib --name uni-component-library --dest lib packages/index.js",
    "build:es": "vue-cli-service build --target lib --name uni-component-library --dest es packages/index.js",
    "build:dist": "vue-cli-service build --target lib --name uni-component-library --dest dist packages/index.js",
    "test": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "vue": "^2.6.14"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.15",
    "@vue/cli-plugin-eslint": "~4.5.15",
    "@vue/cli-plugin-typescript": "~4.5.15",
    "@vue/cli-plugin-unit-jest": "~4.5.15",
    "@vue/cli-service": "~4.5.15",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^5.0.2",
    "@vue/test-utils": "1.0.3",
    "eslint": "^6.7.2",
    "eslint-plugin-prettier": "^3.3.1",
    "eslint-plugin-vue": "^6.2.2",
    "prettier": "^1.19.1",
    "sass": "^1.26.5",
    "sass-loader": "^8.0.2",
    "typescript": "~4.1.5",
    "vue-template-compiler": "^2.6.11"
  },
  "peerDependencies": {
    "vue": "^2.6.14"
  },
  "keywords": [
    "uni-app",
    "component",
    "library",
    "ui"
  ],
  "author": "Your Name",
  "license": "MIT"
}

3.2 组件库入口

创建组件库的入口文件,导出所有组件:

packages/index.js

import Button from './components/button/index.vue';
import Input from './components/input/index.vue';
import Card from './components/card/index.vue';

// 导入主题样式
import './themes/default.scss';

// 组件列表
const components = [
  Button,
  Input,
  Card
];

// 安装函数
const install = (Vue) => {
  components.forEach(component => {
    Vue.component(component.name, component);
  });
};

// 导出组件
export {
  Button,
  Input,
  Card
};

// 导出默认安装函数
export default {
  install,
  version: '1.0.0'
};

4. 开发基础组件

4.1 Button 组件

开发一个基础的 Button 组件,支持不同的类型、尺寸和状态:

packages/components/button/index.vue

<template>
  <button
    class="uni-button"
    :class="[
      `uni-button--${type}`,
      `uni-button--${size}`,
      { 'uni-button--disabled': disabled },
      { 'uni-button--loading': loading }
    ]"
    :disabled="disabled || loading"
    @click="handleClick"
  >
    <uni-loading v-if="loading" class="uni-button__loading" :size="size === 'large' ? '20px' : '16px'" />
    <slot></slot>
  </button>
</template>

<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator';
import UniLoading from '../loading/index.vue';

@Component({
  name: 'UniButton',
  components: {
    UniLoading
  }
})
export default class Button extends Vue {
  @Prop({ type: String, default: 'default' })
  private type!: 'primary' | 'default' | 'success' | 'warning' | 'error';

  @Prop({ type: String, default: 'medium' })
  private size!: 'large' | 'medium' | 'small';

  @Prop({ type: Boolean, default: false })
  private disabled!: boolean;

  @Prop({ type: Boolean, default: false })
  private loading!: boolean;

  @Emit('click')
  private handleClick(event: MouseEvent) {
    if (!this.disabled && !this.loading) {
      return event;
    }
  }
}
</script>

<style scoped lang="scss">
@import '../../themes/default.scss';

.uni-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 16px;
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  font-size: var(--font-size-base);
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  transition: all 0.3s ease;
  outline: none;
  white-space: nowrap;

  &:active {
    opacity: 0.8;
  }

  &:disabled {
    cursor: not-allowed;
    opacity: 0.6;
  }

  &--primary {
    background-color: var(--primary-color);
    border-color: var(--primary-color);
    color: white;
  }

  &--default {
    background-color: white;
    border-color: var(--border-color);
    color: var(--text-color);
  }

  &--success {
    background-color: var(--success-color);
    border-color: var(--success-color);
    color: white;
  }

  &--warning {
    background-color: var(--warning-color);
    border-color: var(--warning-color);
    color: white;
  }

  &--error {
    background-color: var(--error-color);
    border-color: var(--error-color);
    color: white;
  }

  &--large {
    height: 48px;
    font-size: var(--font-size-lg);
    padding: 0 20px;
  }

  &--medium {
    height: 40px;
    font-size: var(--font-size-base);
    padding: 0 16px;
  }

  &--small {
    height: 32px;
    font-size: var(--font-size-sm);
    padding: 0 12px;
  }

  &__loading {
    margin-right: 8px;
  }
}
</style>

4.2 Input 组件

开发一个基础的 Input 组件,支持不同的类型、尺寸和状态:

packages/components/input/index.vue

<template>
  <div class="uni-input-wrapper">
    <slot name="prefix" v-if="$slots.prefix"></slot>
    <input
      v-if="type !== 'textarea'"
      class="uni-input"
      :class="[
        `uni-input--${size}`,
        { 'uni-input--disabled': disabled },
        { 'uni-input--error': error }
      ]"
      :type="type"
      :value="value"
      :placeholder="placeholder"
      :disabled="disabled"
      :maxlength="maxlength"
      @input="handleInput"
      @focus="handleFocus"
      @blur="handleBlur"
    />
    <textarea
      v-else
      class="uni-input uni-input--textarea"
      :class="[
        `uni-input--${size}`,
        { 'uni-input--disabled': disabled },
        { 'uni-input--error': error }
      ]"
      :value="value"
      :placeholder="placeholder"
      :disabled="disabled"
      :maxlength="maxlength"
      :rows="rows"
      @input="handleInput"
      @focus="handleFocus"
      @blur="handleBlur"
    ></textarea>
    <slot name="suffix" v-if="$slots.suffix"></slot>
    <span class="uni-input__clear" v-if="clearable && value && !disabled" @click="handleClear"></span>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Emit, Watch } from 'vue-property-decorator';

@Component({
  name: 'UniInput'
})
export default class Input extends Vue {
  @Prop({ type: String, default: '' })
  private value!: string;

  @Prop({ type: String, default: 'text' })
  private type!: 'text' | 'password' | 'number' | 'email' | 'tel' | 'textarea';

  @Prop({ type: String, default: 'medium' })
  private size!: 'large' | 'medium' | 'small';

  @Prop({ type: String, default: '' })
  private placeholder!: string;

  @Prop({ type: Boolean, default: false })
  private disabled!: boolean;

  @Prop({ type: Boolean, default: false })
  private error!: boolean;

  @Prop({ type: Boolean, default: false })
  private clearable!: boolean;

  @Prop({ type: Number })
  private maxlength!: number;

  @Prop({ type: Number, default: 2 })
  private rows!: number;

  @Emit('input')
  private handleInput(event: Event) {
    const target = event.target as HTMLInputElement | HTMLTextAreaElement;
    return target.value;
  }

  @Emit('focus')
  private handleFocus(event: Event) {
    return event;
  }

  @Emit('blur')
  private handleBlur(event: Event) {
    return event;
  }

  @Emit('clear')
  private handleClear() {
    this.$emit('input', '');
    return true;
  }
}
</script>

<style scoped lang="scss">
@import '../../themes/default.scss';

.uni-input-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: 100%;
}

.uni-input {
  flex: 1;
  padding: 0 12px;
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
  font-size: var(--font-size-base);
  color: var(--text-color);
  background-color: white;
  transition: all 0.3s ease;
  outline: none;

  &:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.1);
  }

  &:disabled {
    background-color: var(--bg-color-light);
    border-color: var(--border-color-light);
    color: var(--text-color-light);
    cursor: not-allowed;
  }

  &--error {
    border-color: var(--error-color) !important;

    &:focus {
      box-shadow: 0 0 0 2px rgba(255, 59, 48, 0.1) !important;
    }
  }

  &--large {
    height: 48px;
    font-size: var(--font-size-lg);
  }

  &--medium {
    height: 40px;
    font-size: var(--font-size-base);
  }

  &--small {
    height: 32px;
    font-size: var(--font-size-sm);
  }

  &--textarea {
    resize: vertical;
    padding: 10px 12px;
    height: auto;
    min-height: 80px;
    line-height: 1.5;
  }
}

.uni-input__clear {
  position: absolute;
  right: 12px;
  width: 16px;
  height: 16px;
  cursor: pointer;
  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="%23999" d="M8 12c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/><path fill="%23999" d="M12 10.5l-1-1L9.5 12l-1-1-1 1-1-1-1 1-1-1L2 9.5l1-1-1-1 1-1 1 1 1-1 1 1 1-1 1 1 1-1 1 1-1 1 1 1z"/></svg>') no-repeat center;
  background-size: 16px;

  &:hover {
    background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="%23666" d="M8 12c-2.2 0-4-1.8-4-4s1.8-4 4-4 4 1.8 4 4-1.8 4-4 4zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/><path fill="%23666" d="M12 10.5l-1-1L9.5 12l-1-1-1 1-1-1-1 1-1-1L2 9.5l1-1-1-1 1-1 1 1 1-1 1 1 1-1 1 1 1-1 1 1-1 1 1 1z"/></svg>') no-repeat center;
    background-size: 16px;
  }
}
</style>

4.3 Card 组件

开发一个基础的 Card 组件,支持不同的头部、内容和底部:

packages/components/card/index.vue

<template>
  <div class="uni-card" :class="{ 'uni-card--bordered': bordered }">
    <div class="uni-card__header" v-if="$slots.header || title">
      <slot name="header">
        <h3 class="uni-card__title">{{ title }}</h3>
      </slot>
    </div>
    <div class="uni-card__body">
      <slot></slot>
    </div>
    <div class="uni-card__footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator';

@Component({
  name: 'UniCard'
})
export default class Card extends Vue {
  @Prop({ type: String, default: '' })
  private title!: string;

  @Prop({ type: Boolean, default: true })
  private bordered!: boolean;
}
</script>

<style scoped lang="scss">
@import '../../themes/default.scss';

.uni-card {
  background-color: white;
  border-radius: var(--border-radius);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  overflow: hidden;

  &--bordered {
    border: 1px solid var(--border-color);
    box-shadow: none;
  }

  &__header {
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-color-light);
  }

  &__title {
    margin: 0;
    font-size: var(--font-size-lg);
    font-weight: 600;
    color: var(--text-color);
  }

  &__body {
    padding: 20px;
  }

  &__footer {
    padding: 16px 20px;
    border-top: 1px solid var(--border-color-light);
    text-align: right;
  }
}
</style>

5. 主题配置

创建主题配置文件,定义组件库的样式变量:

packages/themes/default.scss

// 颜色变量
:root {
  // 主色调
  --primary-color: #007AFF;
  --primary-color-light: #E6F2FF;
  --primary-color-dark: #0056B3;
  
  // 功能色
  --success-color: #34C759;
  --warning-color: #FF9500;
  --error-color: #FF3B30;
  --info-color: #5AC8FA;
  
  // 中性色
  --text-color: #000000;
  --text-color-secondary: #333333;
  --text-color-light: #8E8E93;
  --text-color-disabled: #C7C7CC;
  
  // 背景色
  --bg-color: #F2F2F7;
  --bg-color-light: #F9F9F9;
  --bg-color-white: #FFFFFF;
  
  // 边框色
  --border-color: #C6C6C8;
  --border-color-light: #E5E5EA;
  --border-color-dark: #8E8E93;
  
  // 字体大小
  --font-size-xs: 10px;
  --font-size-sm: 12px;
  --font-size-base: 14px;
  --font-size-lg: 16px;
  --font-size-xl: 18px;
  --font-size-xxl: 20px;
  
  // 间距
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 12px;
  --spacing-lg: 16px;
  --spacing-xl: 20px;
  --spacing-xxl: 24px;
  
  // 边框半径
  --border-radius: 8px;
  --border-radius-sm: 4px;
  --border-radius-lg: 12px;
  --border-radius-xl: 16px;
  
  // 阴影
  --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
  --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
  --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.16);
  
  // 过渡
  --transition-fast: 0.15s ease;
  --transition-normal: 0.3s ease;
  --transition-slow: 0.5s ease;
}

// 重置样式
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: var(--font-size-base);
  color: var(--text-color);
  background-color: var(--bg-color);
  line-height: 1.5;
}

// 通用工具类
.uni-text-center {
  text-align: center;
}

.uni-text-left {
  text-align: left;
}

.uni-text-right {
  text-align: right;
}

.uni-mt-1 {
  margin-top: var(--spacing-xs);
}

.uni-mt-2 {
  margin-top: var(--spacing-sm);
}

.uni-mt-3 {
  margin-top: var(--spacing-md);
}

.uni-mt-4 {
  margin-top: var(--spacing-lg);
}

.uni-mb-1 {
  margin-bottom: var(--spacing-xs);
}

.uni-mb-2 {
  margin-bottom: var(--spacing-sm);
}

.uni-mb-3 {
  margin-bottom: var(--spacing-md);
}

.uni-mb-4 {
  margin-bottom: var(--spacing-lg);
}

.uni-p-1 {
  padding: var(--spacing-xs);
}

.uni-p-2 {
  padding: var(--spacing-sm);
}

.uni-p-3 {
  padding: var(--spacing-md);
}

.uni-p-4 {
  padding: var(--spacing-lg);
}

6. 构建配置

创建构建脚本,用于构建组件库:

build/build.js

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// 构建目录
const libDir = path.resolve(__dirname, '../lib');
const esDir = path.resolve(__dirname, '../es');
const distDir = path.resolve(__dirname, '../dist');

// 清理构建目录
function cleanDir(dir) {
  if (fs.existsSync(dir)) {
    fs.readdirSync(dir).forEach(file => {
      const filePath = path.join(dir, file);
      if (fs.statSync(filePath).isDirectory()) {
        cleanDir(filePath);
      } else {
        fs.unlinkSync(filePath);
      }
    });
    fs.rmdirSync(dir);
  }
}

// 清理目录
cleanDir(libDir);
cleanDir(esDir);
cleanDir(distDir);

// 构建 lib 目录(CommonJS 模块)
console.log('Building lib directory...');
execSync('vue-cli-service build --target lib --name uni-component-library --dest lib packages/index.js', { stdio: 'inherit' });

// 构建 es 目录(ES 模块)
console.log('Building es directory...');
execSync('vue-cli-service build --target lib --name uni-component-library --dest es packages/index.js', { stdio: 'inherit' });

// 构建 dist 目录(UMD 模块)
console.log('Building dist directory...');
execSync('vue-cli-service build --target lib --name uni-component-library --dest dist packages/index.js', { stdio: 'inherit' });

// 复制 package.json 到构建目录
fs.copyFileSync(
  path.resolve(__dirname, '../packages/package.json'),
  path.resolve(__dirname, '../lib/package.json')
);

fs.copyFileSync(
  path.resolve(__dirname, '../packages/package.json'),
  path.resolve(__dirname, '../es/package.json')
);

// 复制 README.md 到构建目录
if (fs.existsSync(path.resolve(__dirname, '../README.md'))) {
  fs.copyFileSync(
    path.resolve(__dirname, '../README.md'),
    path.resolve(__dirname, '../lib/README.md')
  );
  
  fs.copyFileSync(
    path.resolve(__dirname, '../README.md'),
    path.resolve(__dirname, '../es/README.md')
  );
}

console.log('Build completed successfully!');

7. 文档生成

使用 VuePress 生成组件库文档:

7.1 初始化文档项目

docs/package.json

{
  "name": "uni-component-library-docs",
  "version": "1.0.0",
  "description": "Uni-app Component Library Documentation",
  "main": "index.js",
  "scripts": {
    "dev": "vuepress dev src",
    "build": "vuepress build src"
  },
  "dependencies": {
    "vuepress": "^1.9.9"
  },
  "devDependencies": {
    "@vuepress/plugin-back-to-top": "^1.9.9",
    "@vuepress/plugin-medium-zoom": "^1.9.9"
  },
  "keywords": [
    "uni-app",
    "component",
    "library",
    "documentation"
  ],
  "author": "Your Name",
  "license": "MIT"
}

7.2 配置 VuePress

docs/src/.vuepress/config.js

module.exports = {
  title: 'Uni Component Library',
description: '企业级 uni-app 组件库',
  base: '/uni-component-library/',
  themeConfig: {
    repo: 'yourusername/uni-component-library',
    nav: [
      {
        text: '指南',
        link: '/guide/'
      },
      {
        text: '组件',
        link: '/components/'
      },
      {
        text: 'API',
        link: '/api/'
      }
    ],
    sidebar: {
      '/guide/': [
        {
          title: '指南',
          collapsable: false,
          children: [
            '',
            'installation',
            'quick-start',
            'theme'
          ]
        }
      ],
      '/components/': [
        {
          title: '基础组件',
          collapsable: false,
          children: [
            'button',
            'input',
            'card'
          ]
        }
      ]
    }
  },
  plugins: [
    '@vuepress/back-to-top',
    '@vuepress/medium-zoom'
  ]
};

7.3 编写组件文档

docs/src/components/button.md

# Button 按钮

按钮用于触发一个操作,如提交表单、打开弹窗等。

## 基本用法

### 类型

按钮支持不同的类型,默认为 `default`。

<demo-block>
<uni-button>默认按钮</uni-button>
<uni-button type="primary">主要按钮</uni-button>
<uni-button type="success">成功按钮</uni-button>
<uni-button type="warning">警告按钮</uni-button>
<uni-button type="error">错误按钮</uni-button>

```html
<uni-button>默认按钮</uni-button>
<uni-button type="primary">主要按钮</uni-button>
<uni-button type="success">成功按钮</uni-button>
<uni-button type="warning">警告按钮</uni-button>
<uni-button type="error">错误按钮</uni-button>

尺寸

按钮支持不同的尺寸,默认为 medium

大号按钮 中号按钮 小号按钮
<uni-button size="large">大号按钮</uni-button>
<uni-button size="medium">中号按钮</uni-button>
<uni-button size="small">小号按钮</uni-button>

禁用状态

通过 disabled 属性设置按钮禁用状态。

禁用按钮 禁用主要按钮
<uni-button disabled>禁用按钮</uni-button>
<uni-button type="primary" disabled>禁用主要按钮</uni-button>

加载状态

通过 loading 属性设置按钮加载状态。

加载中按钮 加载中主要按钮
<uni-button loading>加载中按钮</uni-button>
<uni-button type="primary" loading>加载中主要按钮</uni-button>

API

Props

参数 说明 类型 默认值 可选值
type 按钮类型 String default primary, success, warning, error
size 按钮尺寸 String medium large, small
disabled 是否禁用 Boolean false true
loading 是否加载中 Boolean false true

Events

事件名 说明 参数
click 点击按钮时触发 event: MouseEvent

8. 测试配置

为组件库添加单元测试,确保组件的质量:

test/unit/example.spec.js

import { mount } from '@vue/test-utils';
import Button from '@/components/button/index.vue';
import Input from '@/components/input/index.vue';
import Card from '@/components/card/index.vue';

describe('Button Component', () => {
  test('renders correctly', () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Button Text'
      }
    });
    expect(wrapper.text()).toBe('Button Text');
  });

  test('handles click event', async () => {
    const wrapper = mount(Button, {
      slots: {
        default: 'Button Text'
      }
    });
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
  });

  test('disables when disabled prop is true', () => {
    const wrapper = mount(Button, {
      propsData: {
        disabled: true
      },
      slots: {
        default: 'Button Text'
      }
    });
    expect(wrapper.attributes('disabled')).toBe('disabled');
  });
});

describe('Input Component', () => {
  test('renders correctly', () => {
    const wrapper = mount(Input, {
      propsData: {
        value: 'Test Value'
      }
    });
    expect(wrapper.find('input').element.value).toBe('Test Value');
  });

  test('emits input event when value changes', async () => {
    const wrapper = mount(Input, {
      propsData: {
        value: ''
      }
    });
    const input = wrapper.find('input');
    await input.setValue('New Value');
    expect(wrapper.emitted('input')).toBeTruthy();
    expect(wrapper.emitted('input')[0][0]).toBe('New Value');
  });
});

describe('Card Component', () => {
  test('renders correctly with title', () => {
    const wrapper = mount(Card, {
      propsData: {
        title: 'Card Title'
      },
      slots: {
        default: 'Card Content'
      }
    });
    expect(wrapper.find('.uni-card__title').text()).toBe('Card Title');
    expect(wrapper.find('.uni-card__body').text()).toBe('Card Content');
  });

  test('renders correctly with slots', () => {
    const wrapper = mount(Card, {
      slots: {
        header: '<div>Custom Header</div>',
        default: 'Card Content',
        footer: '<div>Custom Footer</div>'
      }
    });
    expect(wrapper.find('.uni-card__header').text()).toBe('Custom Header');
    expect(wrapper.find('.uni-card__body').text()).toBe('Card Content');
    expect(wrapper.find('.uni-card__footer').text()).toBe('Custom Footer');
  });
});

实现效果

通过以上代码实现,我们可以获得以下效果:

  1. 完整的组件库结构:创建了一个包含基础组件、主题配置、构建脚本和文档的完整组件库项目。

  2. 基础组件实现:实现了 Button、Input、Card 等基础组件,支持不同的类型、尺寸和状态。

  3. 统一的设计规范:通过 SCSS 变量和统一的样式规范,确保了组件的一致性和可维护性。

  4. 多端适配:考虑了不同平台的差异,确保组件在不同平台上的表现一致。

  5. 完整的文档:使用 VuePress 生成了详细的组件文档,包括使用指南、API 参考和示例代码。

  6. 单元测试:为组件添加了单元测试,确保组件的质量和稳定性。

代码优化建议

1. 性能优化

  • 按需加载:实现组件的按需加载,减少初始包大小。
  • Tree Shaking:确保组件库支持 Tree Shaking,只打包使用的组件。
  • 缓存优化:合理使用缓存,减少重复计算和渲染。
  • 懒加载:对于大型组件,可以考虑使用懒加载,提高初始加载速度。

2. 可维护性优化

  • 模块化管理:将组件的逻辑、样式和模板分离,提高代码的可维护性。
  • 统一工具函数:提取通用的工具函数,避免代码重复。
  • 规范的注释:为组件添加详细的注释,包括功能说明、参数说明和使用示例。
  • 类型定义:使用 TypeScript 或 JSDoc 为组件添加类型定义,提高代码的类型安全性。

3. 功能扩展

  • 主题定制:提供更灵活的主题定制功能,支持用户自定义主题。
  • 国际化:为组件添加国际化支持,适应不同语言的用户。
  • 无障碍访问:优化组件的无障碍访问性,提高组件的可用性。
  • 更多组件:扩展组件库,添加更多的基础组件和功能组件。

常见问题与解决方案

1. 组件库体积过大

问题:组件库体积过大,影响应用的加载速度。

解决方案

  • 实现组件的按需加载,只打包使用的组件。
  • 优化构建配置,减少打包体积。
  • 分离主题样式,允许用户按需引入。
  • 使用 Tree Shaking 技术,移除未使用的代码。

2. 多端适配问题

问题:组件在不同平台上的表现不一致。

解决方案

  • 使用 uni-app 提供的条件编译功能,为不同平台提供适配代码。
  • 测试不同平台的组件表现,确保功能在所有平台上正常工作。
  • 对于平台特定的 API,使用平台判断工具进行适配。
  • 统一处理不同平台的样式差异,确保视觉一致性。

3. 文档维护困难

问题:组件库更新时,文档难以保持同步。

解决方案

  • 使用自动化工具生成文档,减少手动维护的工作量。
  • 建立文档与代码的关联,确保文档与代码版本保持一致。
  • 制定文档更新规范,确保每次组件更新都同步更新文档。
  • 收集用户对文档的反馈,不断改进文档质量。

4. 测试覆盖不全

问题:组件的测试覆盖不全,导致 bug 难以发现。

解决方案

  • 为每个组件添加全面的单元测试,覆盖不同的使用场景。
  • 使用端到端测试,测试组件在实际应用中的表现。
  • 建立测试规范,确保每次组件更新都运行测试。
  • 使用测试覆盖率工具,监控测试覆盖情况。

总结

本章节详细介绍了 uni-app 组件库开发的完整流程,包括组件库设计、统一规范制定、组件开发、文档生成等核心知识点,并通过开发企业级组件库的实用案例,帮助开发者掌握组件库开发的系统方法。

组件库开发是一个系统工程,需要综合考虑多方面因素,包括设计规范、技术架构、多端适配、性能优化、文档维护等。通过本章节的学习,开发者应该能够:

  1. 理解组件库开发的基本原理和流程
  2. 掌握组件库的设计和架构方法
  3. 开发高质量的 uni-app 组件
  4. 生成详细的组件文档
  5. 确保组件的质量和稳定性
  6. 解决组件库开发过程中可能遇到的常见问题

在实际开发中,开发者可以根据项目的具体需求和特点,选择合适的技术栈和开发方法,构建符合企业标准的组件库。一个优秀的组件库不仅可以提高开发效率,还可以保证代码质量,统一设计风格,为用户提供更好的使用体验。

« 上一篇 uni-app 主题切换功能 下一篇 » uni-app 脚手架工具开发