六维教程

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.vuemain.jspages.jsonmanifest.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、打包优化等进阶话题,遇到问题多翻官方文档,祝你写代码愉快。

上一篇
uni-app 常用 API
下一篇
Astro 教程