uni-app 实战
前面 11 篇学完了 uni-app 的核心知识点,本文把它们全部串起来,从零搭建一个待办事项工具(Todo List),最终运行在 H5、微信小程序、App 三个平台。
你学完后可以把这套项目当成模板,快速搭建自己的工具类跨端产品。
功能规划
| 功能 | 用到的知识点 | 对应教程篇目 |
|---|---|---|
| 添加任务 | input + v-model + button | 组件篇(下) |
| 完成 / 未完成切换 | 列表渲染 + 条件样式 | 组件篇(上)、样式篇 |
| 删除任务 | 事件处理 + showModal 确认 | 常用 API 篇 |
| 按状态筛选 | 计算属性 computed | Vue3 教程 |
| 本地持久化 | setStorageSync / getStorageSync | 常用 API 篇 |
| 异步数据加载 | 数据服务层 + 生命周期 onLoad | 网络请求篇、生命周期篇 |
| 三端差异功能 | 条件编译 | 跨端适配篇 |
第一步:创建项目
用 HBuilderX 新建 uni-app 项目(默认模板,Vue3),工程名 uni-todo,具体步骤参考 uni-app 开发环境搭建 篇。
提示:新建项目后先确认目录结构包含 pages/、App.vue、main.js、pages.json、manifest.json,这就是 uni-app 项目目录结构 篇讲过的骨架。
第二步:规划页面与路由
本项目只需要一个页面,pages/index/index.vue。在 pages.json 中注册并配置导航条:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "待办事项"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "待办事项",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F5F5F5"
}
}
pages 数组里只有一项,它就是首页。
第三步:数据服务层
把「读数据、写数据」从页面逻辑里抽出来,单独放到 service/todo.js,页面只关心业务,不关心数据从哪来。
// service/todo.js
// 数据服务层,页面通过 fetchTodos / saveTodos 读写数据
// 当前用本地存储实现,换成后端接口时页面代码不用改
const STORAGE_KEY = 'uni_todo_list'
// 模拟网络延迟,演示异步加载的效果(真实接口请求有天然延迟)
function delay(data, ms = 200) {
return new Promise((resolve) => {
setTimeout(() => resolve(data), ms)
})
}
// 获取任务列表
export function fetchTodos() {
const list = uni.getStorageSync(STORAGE_KEY) || []
return delay(list)
}
// 保存任务列表
export function saveTodos(list) {
uni.setStorageSync(STORAGE_KEY, list)
return delay(true)
}
真实项目里,把这两个函数换成 uni.request 即可,页面代码一行不用改:
// 接后端接口的写法示例(替换上面的两个函数)
export function fetchTodos() {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://api.example.com/todos',
method: 'GET',
success: (res) => resolve(res.data.data),
fail: reject
})
})
}
export function saveTodos(list) {
return new Promise((resolve, reject) => {
uni.request({
url: 'https://api.example.com/todos',
method: 'POST',
data: JSON.stringify(list),
header: { 'Content-Type': 'application/json' },
success: (res) => resolve(res.data.data),
fail: reject
})
})
}
这就是 uni-app 网络请求 篇讲的封装思路,Promise + async/await 让调用方代码非常干净。
第四步:页面实现
页面是核心,分模板、脚本、样式三部分实现。
模板部分
页面结构从上到下分四块,输入区、筛选区、任务列表、空状态:
<template>
<view class="page">
<!-- 顶部:标题 + 剩余数量 + 分享按钮 -->
<view class="header">
<view>
<text class="title">待办事项</text>
<text class="count">还剩 {{ remainCount }} 项未完成</text>
</view>
<!-- #ifdef MP-WEIXIN -->
<button class="share-btn" size="mini" open-type="share">分享</button>
<!-- #endif -->
</view>
<!-- 输入区:input + 添加按钮 -->
<view class="input-bar">
<input
class="input"
v-model="inputText"
placeholder="输入新任务,点击添加"
confirm-type="done"
@confirm="addTodo"
/>
<button class="add-btn" size="mini" type="primary" @click="addTodo">添加</button>
</view>
<!-- 筛选区:全部 / 未完成 / 已完成 -->
<view class="filter-bar">
<text
v-for="item in filters"
:key="item.key"
class="filter-item"
:class="{ active: filter === item.key }"
@click="filter = item.key"
>{{ item.name }}</text>
</view>
<!-- 任务列表:scroll-view 纵向滚动 -->
<scroll-view class="list" scroll-y>
<view class="todo-item" v-for="item in filteredTodos" :key="item.id">
<view class="todo-check" :class="{ checked: item.done }" @click="toggleTodo(item)">
<text v-if="item.done" class="check-mark">✓</text>
</view>
<text class="todo-text" :class="{ done: item.done }" @click="toggleTodo(item)">{{ item.text }}</text>
<view class="todo-actions">
<!-- #ifdef APP-PLUS -->
<text class="todo-remind" @click="remind(item)">提醒</text>
<!-- #endif -->
<text class="todo-del" @click="removeTodo(item.id)">删除</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty" v-if="filteredTodos.length === 0">
<text>{{ emptyText }}</text>
</view>
</scroll-view>
</view>
</template>
模板里的知识点对照:
v-model绑定输入框,uni-app 内置组件(下) 讲过的表单绑定v-for渲染任务列表,:class动态切换完成样式,uni-app 样式开发 的动态样式- 分享按钮和提醒按钮被
<!-- #ifdef -->包裹,只在对应平台编译
脚本部分
<script setup>
import { ref, computed } from 'vue'
import { onLoad, onShow, onShareAppMessage } from '@dcloudio/uni-app'
import { fetchTodos, saveTodos } from '../../service/todo.js'
const todos = ref([]) // 任务列表
const inputText = ref('') // 输入框内容
const filter = ref('all') // 当前筛选:all / active / done
const filters = [
{ key: 'all', name: '全部' },
{ key: 'active', name: '未完成' },
{ key: 'done', name: '已完成' }
]
// ---- 生命周期:页面加载时读取数据 ----
onLoad(() => {
loadTodos()
})
onShow(() => {
// 从其他页面返回时刷新数据(本应用无跳转,保留用于演示)
loadTodos()
})
async function loadTodos() {
todos.value = await fetchTodos()
}
// ---- 添加任务 ----
async function addTodo() {
const text = inputText.value.trim()
if (!text) {
uni.showToast({ title: '请输入任务内容', icon: 'none' })
return
}
todos.value.unshift({
id: Date.now(),
text,
done: false
})
inputText.value = ''
await saveTodos(todos.value)
}
// ---- 切换完成状态 ----
async function toggleTodo(item) {
item.done = !item.done
await saveTodos(todos.value)
}
// ---- 删除任务 ----
async function removeTodo(id) {
uni.showModal({
title: '删除任务',
content: '确定要删除这条任务吗?',
success: async (res) => {
if (res.confirm) {
todos.value = todos.value.filter((t) => t.id !== id)
await saveTodos(todos.value)
uni.showToast({ title: '已删除', icon: 'success' })
}
}
})
}
// ---- 计算属性:筛选列表、剩余数量 ----
const filteredTodos = computed(() => {
if (filter.value === 'active') return todos.value.filter((t) => !t.done)
if (filter.value === 'done') return todos.value.filter((t) => t.done)
return todos.value
})
const remainCount = computed(() => todos.value.filter((t) => !t.done).length)
const emptyText = computed(() => {
if (todos.value.length === 0) return '暂无任务,先添加一条吧'
if (filter.value === 'active') return '没有未完成的任务'
if (filter.value === 'done') return '没有已完成的任务'
return '暂无任务'
})
// #ifdef H5
// ---- H5 端专属:键盘快捷键,按回车添加任务 ----
import { onMounted, onUnmounted } from 'vue'
function onKeyDown(e) {
// 输入框内的回车由 @confirm 处理,这里处理页面其他位置的回车
if (e.key === 'Enter' && e.target.tagName !== 'INPUT') {
addTodo()
}
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown)
})
// #endif
// #ifdef MP-WEIXIN
// ---- 微信小程序专属:自定义分享内容 ----
onShareAppMessage(() => {
return { title: '我的待办清单', path: '/pages/index/index' }
})
// #endif
// ---- App 端专属:设置本地提醒 ----
function remind(item) {
// #ifdef APP-PLUS
plus.push.createMessage(item.text, String(item.id), { title: '待办提醒' })
uni.showToast({ title: '已设置本地提醒', icon: 'success' })
// #endif
}
</script>
脚本部分的知识点对照:
onLoad在页面加载时读取数据,uni-app 生命周期 篇的数据加载位置computed派生筛选列表和剩余数量,数据一变界面自动更新saveTodos每次都把最新列表写入本地存储,实现 uni-app 常用 API 篇的持久化- H5 键盘快捷键、小程序分享、App 本地提醒,全是 uni-app 条件编译 篇的实战形态
注意:plus 对象只在 App 端存在,所以引用它的代码必须全部包在 #ifdef APP-PLUS 里,否则其他平台编译或运行会报错。
样式部分
rpx 响应式单位 + Flex 布局,uni-app 样式开发 的完整应用:
<style scoped>
.page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 10rpx 30rpx;
}
.title {
font-size: 44rpx;
font-weight: bold;
color: #333;
}
.count {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #999;
}
.share-btn {
margin: 0;
}
.input-bar {
display: flex;
align-items: center;
}
.input {
flex: 1;
height: 80rpx;
padding: 0 24rpx;
background-color: #fff;
border-radius: 16rpx;
}
.add-btn {
margin-left: 20rpx;
}
.filter-bar {
display: flex;
padding: 20rpx 0;
}
.filter-item {
margin-right: 30rpx;
font-size: 28rpx;
color: #666;
}
.filter-item.active {
color: #007aff;
font-weight: bold;
}
.list {
height: calc(100vh - 320rpx);
}
.todo-item {
display: flex;
align-items: center;
padding: 24rpx;
margin-bottom: 16rpx;
background-color: #fff;
border-radius: 16rpx;
}
.todo-check {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
display: flex;
align-items: center;
justify-content: center;
}
.todo-check.checked {
background-color: #007aff;
border-color: #007aff;
}
.check-mark {
color: #fff;
font-size: 28rpx;
}
.todo-text {
flex: 1;
margin-left: 20rpx;
font-size: 30rpx;
color: #333;
}
.todo-text.done {
color: #ccc;
text-decoration: line-through;
}
.todo-actions {
display: flex;
}
.todo-remind {
margin-right: 30rpx;
font-size: 26rpx;
color: #007aff;
}
.todo-del {
font-size: 26rpx;
color: #ff4d4f;
}
.empty {
padding: 100rpx 0;
text-align: center;
color: #999;
}
</style>
- 尺寸全部用 rpx,750rpx = 屏幕宽度,三端自动适配
flex: 1让输入框和任务文字占满剩余空间.done完成态用删除线 + 灰色,点击圆形图标切换
第五步:三端差异化功能
同一个页面,三个平台各有一块专属功能:
| 平台 | 专属功能 | 实现方式 |
|---|---|---|
| H5 | 回车键快捷添加任务 | window 监听 keydown,包在 #ifdef H5 |
| 微信小程序 | 自定义分享卡片 | onShareAppMessage + 分享按钮 |
| App | 任务本地提醒 | plus.push.createMessage,包在 #ifdef APP-PLUS |
编译到 H5 时,分享按钮、提醒按钮、plus 相关的代码会被整体剔除,页面只包含网页逻辑,其他端同理。这就是一套代码多端发布的精髓,公共逻辑只写一遍,差异用条件编译精确控制。
第六步:运行验证
用 HBuilderX 依次验证三个平台:
运行到浏览器,点「运行 → 运行到浏览器 → Chrome」,验证添加、筛选、删除、刷新后数据还在。
运行到微信开发者工具,点「运行 → 运行到小程序模拟器 → 微信开发者工具」(需先安装微信开发者工具并开启服务端口,见 uni-app 开发环境搭建 篇),验证分享按钮,转发后好友打开能看到自己的清单。
运行到手机,连接安卓手机开启 USB 调试,点「运行 → 运行到手机或模拟器」,点某条任务的「提醒」,手机会弹出本地通知。
第七步:打包发布
三端的发布入口都在 HBuilderX 的「发行」菜单:
| 平台 | 操作 | 产物 |
|---|---|---|
| H5 | 发行 → 网站 H5-App | 静态文件,部署到服务器 |
| 微信小程序 | 发行 → 小程序-微信,上传后在后台提审 | 小程序包 |
| App | 发行 → 原生 App-云打包,填写证书和图标 | apk / ipa 安装包 |
云打包免费版可以直接用测试证书体验,正式发布需要申请各平台的证书和 appid。
总结
一个待办工具麻雀虽小,五脏俱全,把本系列的知识点全部过了一遍:
| 阶段 | 用了什么 |
|---|---|
| 创建项目 | HBuilderX 新建,默认模板 |
| 结构路由 | pages/ + pages.json 注册页面 |
| 界面 | view/text/input/button/scroll-view 内置组件 |
| 样式 | rpx 单位 + Flex 布局 + 动态 class |
| 数据 | 服务层封装 + 本地存储持久化(可换 uni.request) |
| 交互 | showToast / showModal 反馈 |
| 生命周期 | onLoad 加载数据 |
| 多端 | 条件编译实现三端差异化功能 |
到此,本系列教程结束。从认识 uni-app 到搭建出三端可运行的应用,你已经掌握了跨端开发的核心方法。后续可以继续深入 uni-ui 组件库、nvue、打包优化等进阶话题,遇到问题多翻官方文档,祝你写代码愉快。