一、uni-app 框架概述
uni-app 是 DCloud 公司推出的跨平台开发框架,其最大的特点是 "一次编写,多端运行"。不同于传统的跨平台方案,uni-app 并非简单的网页封装,而是通过编译器将 Vue 代码转换为各平台的原生代码,实现了接近原生应用的性能体验。
1.1 核心优势
- 跨平台能力:支持编译到 10+ 平台,包括 iOS、Android、微信小程序、H5 等
- 性能优化:采用原生渲染技术,部分场景下性能接近原生应用
- 生态丰富:拥有庞大的插件市场和组件市场,支持原生插件扩展
- 开发效率:使用 Vue.js 语法,学习成本低,开发效率高
- 条件编译:通过特殊语法实现平台差异化逻辑
1.2 应用场景
uni-app 适用于以下场景:
- 企业级多端应用(APP + 小程序 + H5 统一开发)
- 快速迭代的创业项目(减少开发成本)
- 内容型应用(博客、新闻、电商等)
- 需要多平台覆盖的营销活动页面
二、快速入门:从环境搭建到第一个应用
2.1 开发环境准备
-
安装 HBuilderX:uni-app 推荐使用 DCloud 开发的 HBuilderX 编辑器,下载地址:HBuilderX-高效极客技巧
-
Node.js 环境:确保已安装 Node.js(建议 v14+)
-
配置运行环境:
- 手机调试:安装 HBuilderX 内置的模拟器或连接真实手机
- 小程序调试:下载对应平台的开发者工具(如微信开发者工具)
2.2 创建第一个 uni-app 项目
- 在 HBuilderX 中点击 "文件" -> "新建" -> "项目"
- 选择 "uni-app" 模板,输入项目名称和路径
- 项目结构说明:
plaintext
my-uni-app/
├── pages/ # 页面文件
├── static/ # 静态资源
├── uni_modules/ # 组件模块
├── App.vue # 应用入口
├── main.js # 主逻辑
├── manifest.json # 应用配置
└── pages.json # 页面路由配置
2.3 编写第一个页面
在 pages/index/index.vue
中编写一个简单的问候页面:
vue
<template>
<view class="container">
<view class="header">
<text class="title">欢迎使用 uni-app</text>
</view>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<text class="text-area">
<text class="text">{{ message }}</text>
</text-area>
<view class="platform-info">
<text>当前平台: {{ platform }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
message: 'Hello uni-app!',
platform: ''
}
},
onLoad() {
// 获取当前运行平台
this.platform = uni.getSystemInfoSync().platform;
}
}
</script>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 20rpx;
box-sizing: border-box;
}
.header {
width: 100%;
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #3A7BD5;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.logo {
width: 200rpx;
height: 200rpx;
margin: 30rpx 0;
}
.text-area {
display: flex;
justify-content: center;
margin-top: 20rpx;
width: 100%;
}
.text {
font-size: 32rpx;
color: #666;
}
.platform-info {
margin-top: 30rpx;
font-size: 28rpx;
color: #999;
}
</style>
2.4 运行与调试
- 点击 HBuilderX 工具栏中的 "运行" 按钮
- 选择运行目标(如 "运行到手机模拟器" 或 "运行到微信开发者工具")
- 查看效果:应用会根据选择的平台编译并运行,显示当前平台信息
三、uni-app 核心功能与开发技巧
3.1 跨平台开发核心 - 条件编译
uni-app 通过 #ifdef
、#ifndef
、#endif
实现平台差异化逻辑:
vue
<script>
export default {
onLoad() {
// #ifdef APP-PLUS
// 仅在 App 平台执行的代码
this.initAppFeatures();
// #endif
// #ifdef H5
// 仅在 H5 平台执行的代码
this.initH5Features();
// #endif
// #ifdef MP
// 仅在小程序平台执行的代码
this.initMiniProgramFeatures();
// #endif
}
}
</script>
3.2 原生能力调用
uni-app 封装了丰富的原生 API,以下是调用相机拍照的示例:
javascript
// 调用相机拍照
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths;
uni.showModal({
title: '拍照成功',
content: '图片路径: ' + tempFilePaths[0]
});
// 上传图片到服务器
uni.uploadFile({
url: 'https://your-server.com/upload',
filePath: tempFilePaths[0],
name: 'file',
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data);
uni.showToast({
title: '上传成功',
icon: 'success'
});
}
});
}
});
3.3 组件化开发
uni-app 支持 Vue 组件系统,可以创建复用组件:
vue
<!-- components/button/index.vue -->
<template>
<button
:class="['custom-button', type === 'primary' ? 'primary' : type === 'secondary' ? 'secondary' : '']"
:disabled="disabled"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'default'
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick() {
if (!this.disabled) {
this.$emit('click');
}
}
}
}
</script>
<style>
.custom-button {
padding: 16rpx 40rpx;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: normal;
}
.primary {
background-color: #3A7BD5;
color: #FFFFFF;
}
.secondary {
background-color: #F2F2F2;
color: #333333;
border: 1rpx solid #DDDDDD;
}
</style>
使用组件:
vue
<template>
<view>
<custom-button type="primary" @click="submitForm">提交表单</custom-button>
<custom-button type="secondary" @click="cancelForm">取消</custom-button>
</view>
</template>
<script>
import CustomButton from '@/components/button/index.vue'
export default {
components: {
CustomButton
},
methods: {
submitForm() {
uni.showToast({
title: '表单提交成功',
icon: 'success'
});
},
cancelForm() {
uni.showModal({
title: '提示',
content: '确定要取消吗?',
success: (res) => {
if (res.confirm) {
uni.navigateBack();
}
}
});
}
}
}
</script>
四、实战案例:开发跨平台 TODO 应用
4.1 项目规划
我们将开发一个 TODO 应用,具备以下功能:
- 任务列表展示
- 新增任务
- 完成 / 取消完成任务
- 删除任务
- 任务筛选(全部 / 进行中 / 已完成)
4.2 数据模型设计
javascript
// models/todo.js
export default {
id: '', // 任务ID
title: '', // 任务标题
description: '', // 任务描述
isCompleted: false, // 是否完成
createTime: '', // 创建时间
dueTime: '' // 截止时间
}
4.3 页面开发
首页 (pages/todo/index.vue)
vue
<template>
<view class="todo-container">
<!-- 头部标题 -->
<view class="header">
<text class="title">我的待办事项</text>
</view>
<!-- 新增任务 -->
<view class="add-todo">
<input
v-model="newTodoTitle"
placeholder="添加新任务..."
class="todo-input"
@keyup.enter="addTodo"
>
<button class="add-button" @click="addTodo">+</button>
</view>
<!-- 任务统计 -->
<view class="stats">
<text>共有 {{ totalTodos }} 项任务,已完成 {{ completedTodos }} 项</text>
</view>
<!-- 任务筛选 -->
<view class="filter">
<button
class="filter-btn"
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>全部</button>
<button
class="filter-btn"
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>进行中</button>
<button
class="filter-btn"
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>已完成</button>
</view>
<!-- 任务列表 -->
<view class="todo-list">
<view
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
>
<view class="todo-content">
<checkbox
:checked="todo.isCompleted"
@change="toggleTodoStatus(todo)"
class="todo-checkbox"
>
</checkbox>
<view class="todo-text" :class="{ completed: todo.isCompleted }">
<text class="todo-title">{{ todo.title }}</text>
<text v-if="todo.description" class="todo-desc">{{ todo.description }}</text>
</view>
</view>
<button class="delete-btn" @click="deleteTodo(todo)">
<text>删除</text>
</button>
</view>
<!-- 空状态提示 -->
<view v-if="filteredTodos.length === 0" class="empty-state">
<text>暂无待办任务,点击添加按钮创建任务</text>
</view>
</view>
</view>
</template>
<script>
import TodoService from '@/services/todo.js'
export default {
data() {
return {
newTodoTitle: '',
newTodoDesc: '',
filter: 'all',
todos: []
}
},
computed: {
totalTodos() {
return this.todos.length;
},
completedTodos() {
return this.todos.filter(todo => todo.isCompleted).length;
},
filteredTodos() {
if (this.filter === 'all') {
return this.todos;
} else if (this.filter === 'active') {
return this.todos.filter(todo => !todo.isCompleted);
} else if (this.filter === 'completed') {
return this.todos.filter(todo => todo.isCompleted);
}
return this.todos;
}
},
onLoad() {
this.loadTodos();
},
methods: {
// 加载任务列表
loadTodos() {
TodoService.getTodos().then(todos => {
this.todos = todos;
}).catch(err => {
uni.showToast({
title: '加载失败',
icon: 'none'
});
console.error(err);
});
},
// 添加任务
addTodo() {
if (!this.newTodoTitle.trim()) {
uni.showToast({
title: '任务标题不能为空',
icon: 'none'
});
return;
}
const newTodo = {
id: new Date().getTime().toString(),
title: this.newTodoTitle.trim(),
description: this.newTodoDesc,
isCompleted: false,
createTime: new Date().toISOString(),
dueTime: ''
};
TodoService.addTodo(newTodo).then(() => {
this.loadTodos();
this.newTodoTitle = '';
this.newTodoDesc = '';
}).catch(err => {
uni.showToast({
title: '添加失败',
icon: 'none'
});
console.error(err);
});
},
// 切换任务状态
toggleTodoStatus(todo) {
todo.isCompleted = !todo.isCompleted;
TodoService.updateTodo(todo).catch(err => {
uni.showToast({
title: '更新失败',
icon: 'none'
});
console.error(err);
todo.isCompleted = !todo.isCompleted; // 回滚状态
});
},
// 删除任务
deleteTodo(todo) {
uni.showModal({
title: '确认删除',
content: '确定要删除这个任务吗?',
success: (res) => {
if (res.confirm) {
TodoService.deleteTodo(todo.id).then(() => {
this.loadTodos();
}).catch(err => {
uni.showToast({
title: '删除失败',
icon: 'none'
});
console.error(err);
});
}
}
});
}
}
}
</script>
<style>
.todo-container {
padding: 30rpx;
box-sizing: border-box;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #333;
}
.add-todo {
display: flex;
margin-bottom: 30rpx;
}
.todo-input {
flex: 1;
height: 88rpx;
background-color: #F5F5F5;
border-radius: 44rpx;
padding: 0 30rpx;
font-size: 28rpx;
}
.add-button {
width: 88rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 50%;
background-color: #3A7BD5;
color: #FFFFFF;
margin-left: 20rpx;
padding: 0;
font-size: 40rpx;
}
.stats {
font-size: 24rpx;
color: #999;
margin-bottom: 30rpx;
}
.filter {
display: flex;
margin-bottom: 30rpx;
border-bottom: 1rpx solid #EEEEEE;
padding-bottom: 20rpx;
}
.filter-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
padding: 0;
font-size: 28rpx;
color: #666;
background-color: transparent;
}
.filter-btn.active {
color: #3A7BD5;
font-weight: bold;
position: relative;
}
.filter-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
background-color: #3A7BD5;
border-radius: 3rpx;
}
.todo-list {
width: 100%;
}
.todo-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #EEEEEE;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-checkbox {
margin-right: 20rpx;
}
.todo-content {
flex: 1;
display: flex;
align-items: flex-start;
}
.todo-text {
flex: 1;
}
.todo-title {
font-size: 28rpx;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.todo-desc {
font-size: 24rpx;
color: #999;
display: block;
}
.todo-text.completed .todo-title,
.todo-text.completed .todo-desc {
color: #CCCCCC;
text-decoration: line-through;
}
.delete-btn {
width: 80rpx;
height: 80rpx;
line-height: 80rpx;
padding: 0;
font-size: 24rpx;
color: #F56C6C;
background-color: transparent;
}
.empty-state {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
</style>
4.4 数据服务实现
javascript
// services/todo.js
export default {
// 获取任务列表
getTodos() {
return new Promise((resolve, reject) => {
// 从本地存储获取
const todos = uni.getStorageSync('todos');
if (todos) {
resolve(todos);
} else {
// 初始化空列表
uni.setStorageSync('todos', []);
resolve([]);
}
});
},
// 添加任务
addTodo(todo) {
return new Promise((resolve, reject) => {
this.getTodos().then(todos => {
todos.unshift(todo);
uni.setStorageSync('todos', todos);
resolve(todo);
}).catch(reject);
});
},
// 更新任务
updateTodo(todo) {
return new Promise((resolve, reject) => {
this.getTodos().then(todos => {
const index = todos.findIndex(item => item.id === todo.id);
if (index !== -1) {
todos[index] = todo;
uni.setStorageSync('todos', todos);
resolve(todo);
} else {
reject(new Error('任务不存在'));
}
}).catch(reject);
});
},
// 删除任务
deleteTodo(id) {
return new Promise((resolve, reject) => {
this.getTodos().then(todos => {
const newTodos = todos.filter(todo => todo.id !== id);
uni.setStorageSync('todos', newTodos);
resolve(newTodos);
}).catch(reject);
});
}
}
五、uni-app 开发中的挑战与解决方案
5.1 平台差异处理
虽然 uni-app 提供了跨平台能力,但各平台仍有差异,常见解决方案:
- 条件编译:通过
#ifdef
等预处理指令实现平台差异化逻辑 - 组件封装:对平台特有组件进行封装,提供统一接口
- 插件扩展:使用 uni-app 原生插件市场的插件解决特殊需求
5.2 性能优化
- 图片优化:使用
uni.createSelectorQuery()
实现图片懒加载 - 长列表优化:使用
scroll-view
结合虚拟列表技术 - 减少重绘:合理使用
v-show
与v-if
,避免频繁切换 - 本地存储优化:批量操作本地存储,减少 IO 次数
5.3 小程序平台限制
小程序平台有独特的限制,如:
- 页面层级限制(通常不超过 10 层)
- 包大小限制(通常不超过 2MB)
- 运行环境限制(无浏览器完整 DOM API)
- 分包加载:将应用拆分为多个包
- 组件化设计:提高代码复用率
- 优化资源:压缩图片、代码丑化等
六、uni-app 生态与未来发展
6.1 生态系统
uni-app 拥有丰富的生态系统:
- 插件市场:超过 10,000+ 原生插件,覆盖各类功能需求
- 组件市场:大量可复用的 UI 组件和业务组件
- 官方文档:详细的开发文档和示例
- 社区支持:DCloud 官方社区、微信社群、掘金 / 知乎等技术社区
6.2 未来发展趋势
uni-app 正在不断进化,未来发展方向包括:
- 性能提升:进一步优化渲染引擎,接近原生应用性能
- 跨平台能力扩展:支持更多新兴平台和设备
- 开发体验优化:完善工具链,提升开发效率
- 生态繁荣:吸引更多开发者和企业加入生态建设
七、总结
uni-app 为开发者提供了一个高效的跨平台解决方案,通过一套代码实现多端部署,大大降低了开发成本和维护成本。无论是创业团队、企业应用还是个人开发者,uni-app 都是一个值得考虑的选择。
通过本文的介绍,你已经了解了 uni-app 的基本概念、开发流程和实战技巧。接下来,建议你动手实践,从简单的应用开始,逐步掌握这一强大的跨平台开发框架。