uni-app 组件库开发
章节介绍
组件库是现代前端开发的重要组成部分,它可以提高开发效率、保证代码质量、统一设计风格。在 uni-app 开发中,构建一套符合企业标准的组件库尤为重要,因为它需要兼顾多端适配、性能优化和开发体验。本章节将详细介绍 uni-app 组件库开发的完整流程,从设计规范到最终发布,帮助开发者掌握组件库开发的系统方法。
核心知识点
1. 组件库设计
组件库设计是组件库开发的基础,它决定了组件库的整体架构和使用体验。在设计组件库时,需要考虑以下几个方面:
1.1 设计系统
设计系统是组件库的灵魂,它包括:
- 设计语言:颜色、字体、间距、阴影等设计元素的规范
- 组件体系:组件的分类、层级关系和使用场景
- 交互规范:组件的交互方式、动画效果和反馈机制
1.2 技术架构
技术架构是组件库的骨架,它包括:
- 目录结构:组件库的文件组织方式
- 构建工具:使用何种工具进行开发、构建和发布
- 依赖管理:组件库的依赖项和版本控制
- 多端适配:如何处理不同平台的差异
1.3 组件规划
组件规划是组件库的蓝图,它包括:
- 基础组件:按钮、输入框、标签等基础 UI 元素
- 布局组件:网格、面板、卡片等布局结构
- 功能组件:日历、表单、表格等具有特定功能的组件
- 业务组件:与具体业务相关的复合组件
2. 统一规范
统一规范是组件库质量的保证,它确保了组件的一致性和可维护性。在制定统一规范时,需要考虑以下几个方面:
2.1 命名规范
命名规范包括:
- 组件命名:使用 PascalCase 命名组件,如
Button、Input - 文件命名:组件文件与组件名称保持一致
- 变量命名:使用 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');
});
});实现效果
通过以上代码实现,我们可以获得以下效果:
完整的组件库结构:创建了一个包含基础组件、主题配置、构建脚本和文档的完整组件库项目。
基础组件实现:实现了 Button、Input、Card 等基础组件,支持不同的类型、尺寸和状态。
统一的设计规范:通过 SCSS 变量和统一的样式规范,确保了组件的一致性和可维护性。
多端适配:考虑了不同平台的差异,确保组件在不同平台上的表现一致。
完整的文档:使用 VuePress 生成了详细的组件文档,包括使用指南、API 参考和示例代码。
单元测试:为组件添加了单元测试,确保组件的质量和稳定性。
代码优化建议
1. 性能优化
- 按需加载:实现组件的按需加载,减少初始包大小。
- Tree Shaking:确保组件库支持 Tree Shaking,只打包使用的组件。
- 缓存优化:合理使用缓存,减少重复计算和渲染。
- 懒加载:对于大型组件,可以考虑使用懒加载,提高初始加载速度。
2. 可维护性优化
- 模块化管理:将组件的逻辑、样式和模板分离,提高代码的可维护性。
- 统一工具函数:提取通用的工具函数,避免代码重复。
- 规范的注释:为组件添加详细的注释,包括功能说明、参数说明和使用示例。
- 类型定义:使用 TypeScript 或 JSDoc 为组件添加类型定义,提高代码的类型安全性。
3. 功能扩展
- 主题定制:提供更灵活的主题定制功能,支持用户自定义主题。
- 国际化:为组件添加国际化支持,适应不同语言的用户。
- 无障碍访问:优化组件的无障碍访问性,提高组件的可用性。
- 更多组件:扩展组件库,添加更多的基础组件和功能组件。
常见问题与解决方案
1. 组件库体积过大
问题:组件库体积过大,影响应用的加载速度。
解决方案:
- 实现组件的按需加载,只打包使用的组件。
- 优化构建配置,减少打包体积。
- 分离主题样式,允许用户按需引入。
- 使用 Tree Shaking 技术,移除未使用的代码。
2. 多端适配问题
问题:组件在不同平台上的表现不一致。
解决方案:
- 使用 uni-app 提供的条件编译功能,为不同平台提供适配代码。
- 测试不同平台的组件表现,确保功能在所有平台上正常工作。
- 对于平台特定的 API,使用平台判断工具进行适配。
- 统一处理不同平台的样式差异,确保视觉一致性。
3. 文档维护困难
问题:组件库更新时,文档难以保持同步。
解决方案:
- 使用自动化工具生成文档,减少手动维护的工作量。
- 建立文档与代码的关联,确保文档与代码版本保持一致。
- 制定文档更新规范,确保每次组件更新都同步更新文档。
- 收集用户对文档的反馈,不断改进文档质量。
4. 测试覆盖不全
问题:组件的测试覆盖不全,导致 bug 难以发现。
解决方案:
- 为每个组件添加全面的单元测试,覆盖不同的使用场景。
- 使用端到端测试,测试组件在实际应用中的表现。
- 建立测试规范,确保每次组件更新都运行测试。
- 使用测试覆盖率工具,监控测试覆盖情况。
总结
本章节详细介绍了 uni-app 组件库开发的完整流程,包括组件库设计、统一规范制定、组件开发、文档生成等核心知识点,并通过开发企业级组件库的实用案例,帮助开发者掌握组件库开发的系统方法。
组件库开发是一个系统工程,需要综合考虑多方面因素,包括设计规范、技术架构、多端适配、性能优化、文档维护等。通过本章节的学习,开发者应该能够:
- 理解组件库开发的基本原理和流程
- 掌握组件库的设计和架构方法
- 开发高质量的 uni-app 组件
- 生成详细的组件文档
- 确保组件的质量和稳定性
- 解决组件库开发过程中可能遇到的常见问题
在实际开发中,开发者可以根据项目的具体需求和特点,选择合适的技术栈和开发方法,构建符合企业标准的组件库。一个优秀的组件库不仅可以提高开发效率,还可以保证代码质量,统一设计风格,为用户提供更好的使用体验。