第8章 8.1 移动端开发:uniapp 与 Vue3

上章我们用手把手的方式,带着你从 0 到 1 搭了一个「按钮+列表+弹窗」的组件库。说白了,就是把重复代码封装起来,下次用直接调。

但你有没有发现一个尴尬的问题——做完只能在浏览器里跑。手机扫码?不好意思,浏览器模拟不了真实 App 的体验。

这一章,我们换条路:用 uniapp + Vue3,一套代码同时跑在 iOS、Android、小程序、H5 上。你写一次,老板说「加个 App」,妈妈说「加个小程序」,你微微一笑——不用重写

🎯 开场 3 分钟:为什么要学这个?

场景带入

想象你是校园外卖小哥,每天要手动统计「哪些店爆单、哪些店没人看」。你写成网页,室友想用——你总不能让他装 Python 环境吧。但如果是个手机点开就能用的小程序,发群里,大家都方便。

uniapp 就是干这个的:用 Vue 语法写代码,编译出来是小程序、是 App、是 H5,随你挑。

痛点来了

  • 想做移动端,但不想同时学 Swift/Java/Kot\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\nlin?
  • 想快速验证想法,不想折腾环境?
  • 想让学生党/上班族用你的小工具,但没有服务器?

学完这章,你就能用 Vue3 的写法,写出一个「扫二维码就能用」的小工具。


🧱 基础 25 分钟:核心概念

8.1.1 uniapp 是什么?

uniapp 是 DCloud 公司出品的跨平台开发框架。你可以理解成:

uniapp = Vue3 的语法 + 一套编译器 + 各平台的运行环境

类比一下:就像你用美团外卖点餐,不管店家用什么厨房设备(iOS/Android),最后送到你手里的都是标准化的餐盒。uniapp 就是那个「标准化餐盒」。

核心优势

优势 解释
一次开发,多端运行 iOS、Android、小程序、H5、Flutter(部分)
上手快 会 Vue3 就能写
生态成熟 插件市场丰富,踩坑有答案

8.1.2 环境准备(10 分钟)

步骤 1:安装 HBuilderX

这是 uniapp 官方推荐的编辑器,内置了编译器,不用你手动配环境。

# 官网下载:https://www.dcloud.io/hbuilderx.html
# Mac 用户拖到应用程序文件夹,Windows 用户下一步下一步

步骤 2:创建第一个项目

打开 HBuilderX → 文件 → 新建 → 项目 → 选择「uni-app」→ 填项目名「my-first-app」→ 勾选「Vue3」版本

创建完成后,你会看到这样的目录结构:

my-first-app/
├── pages/              # 页面文件夹
│   └── index/
│       └── index.vue   # 首页
├── static/             # 静态资源
├── App.vue             # 应用入口
├── main.js             # Vue3 入口文件
├── manifest.json       # App 配置
├── pages.json          # 页面路由配置
└── uni.scss            # 全局样式变量

步骤 3:认识 Vue3 写法

打开 pages/index/index.vue,这是你的首页。uniapp 的页面本质就是一个 Vue3 单文件组件

<script setup>
// Vue3 的 composition API,写法很简洁
import { ref } from 'vue'

// 定义一个响应式变量
const message = ref('你好,uniapp!')

// 定义一个方法
const changeMessage = () => {
message.value = '我被点击了!'
}
</script>

<template>
<view class="container">
<!-- 绑定了上面的 message 变量 -->
<text class="title">{{ message }}</text>
<!-- 点击触发方法 -->
<button @click="changeMessage">点我</button>
</view>
</template>

<style>
.container {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 20px;
margin: 20px 0;
color: #333;
}
</style>

这行在干嘛
- ref() 创建响应式变量,值变化 UI 自动更新
- @click 绑定点击事件,比原生 JS 的 addEventListener 简洁太多

8.1.3 跨端编译原理(重要!)

这是理解 uniapp 的关键。很多人学不会,是因为搞不清「谁在编译、编译成什么」。

uniapp 的编译流程是这样的:

你的 Vue3 代码(.vue 文件)
    ↓
ni-app 编译器(内置于 HBuilderX)
    ↓
┌───────┼───────┬───────┐
↓       ↓       ↓       ↓
iOS    Android  微信     H5
代码   代码    小程序    页面

类比一下:uniapp 编译器就像一个「翻译官」,你用中文(Vue3)写代码,翻译官给你翻译成英文(iOS)、日文(Android)、法文(小程序)。

关键点
- 不是运行时翻译,是编译时就生成各平台代码
- 所以性能接近原生,比 WebView 方案快很多
- 部分 API 是 uniapp 封装的跨平台 API,底层自动调用各平台原生能力

8.1.4 生命周期:App 和页面的生老病死

Vue3 有组件生命周期,uniapp 多了一层应用生命周期页面生命周期

// App.vue - 应用级别
<script setup>
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'

// 应用启动时触发(类似小程序的 onLaunch)
onLaunch(() => {
console.log('App 启动了')
})

// 应用从后台切到前台(类似小程序的 onShow)
onShow(() => {
console.log('App 显示了')
})

// 应用从前台切到后台(类似小程序的 onHide)
onHide(() => {

console.log('App 隐藏了')
})
</script>
// 页面级别 - pages/index/index.vue
<script setup>
import { onLoad, onShow, onReady } from '@dcloudio/uni-app'

// 页面加载时触发(类似 onLoad)
onLoad(() => {
console.log('页面加载完成')
// 常见用法:从其他页面跳转时获取参数
// onLoad 会自动接收 url 里的参数
})

// 页面显示时触发(每次切回来都会触发)
onShow(() => {
console.log('页面显示了')
})

// 页面初次渲染完成(类似 mounted)
onReady(() => {
console.log('页面渲染完成,可以操作 DOM 了')
})
</script>

生活类比
- onLaunch = 打开 App 的那一瞬间
- onShow = 从后台切回 App
- onHide = 按了 Home 键,App 退到后台
- onLoad = 页面数据加载(常用于读取参数)
- onReady = 页面第一次画完了(可以交互了)

8.1.5 响应式系统:Vue3 的核心

uniapp 完全支持 Vue3 的响应式,这是它比微信小程序简洁的根本原因。

<script setup>
import { ref, reactive, computed, watch } from 'vue'

// ref:基础类型(字符串、数字、布尔)
const name = ref('小明')
const age = ref(20)

// reactive:对象/数组
const user = reactive({
nickname: '小明',
score: 88
})

// computed:计算属性
const info = computed(() => {
return `${name.value},${age.value}岁,分数${user.score}`
})

// watch:监听变化
watch(age, (newVal, oldVal) => {
console.log(`年龄从 ${oldVal} 变成 ${newVal}`)
})

// 修改值
const growUp = () => {
age.value++  // ref 用 .value 修改
user.score += 10  // reactive 直接修改
}
</script>

<template>
<view>
<text>{{ info }}</text>
<button @click="growUp">长大一岁</button>
</view>
</template>

为什么用 ref 和 reactive
- ref 用于简单值,修改要 .value
- reactive 用于对象,修改直接 obj.key = value
- 两者都是响应式的,值变了 UI 自动更新


🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):计数器 App

目标:点一下按钮,数字 +1,界面实时更新。

这是最入门的项目,帮你理解 uniapp 的响应式和事件绑定。

<!-- pages/counter/counter.vue -->
<script setup>
import { ref } from 'vue'

// 定义计数器和历史记录
const count = ref(0)
const history = ref([])

// 增加计数
const increment = () => {
count.value++
history.value.push(`+1 → ${count.value}`)
}

// 重置
const reset = () => {
count.value = 0
history.value = []
}
</script>

<template>
<view class="container">
<text class="count">{{ count }}</text>
<view class="btn-group">
  <button type="primary" @click="increment">+1</button>
  <button type="warn" @click="reset">重置</button>
</view>
<!-- 历史记录列表 -->
<view class="history">
  <text>操作历史:</text>
  <text v-for="(item, index) in history" :key="index" class="log">
    {{ item }}
  </text>
</view>
</view>
</template>

<style>
.container {
padding: 40px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.count {
font-size: 80px;
font-weight: bold;
color: #007AFF;
}
.btn-group {
display: flex;
gap: 20px;
margin: 30px 0;
}
.history {
width: 100%;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.log {
display: block;
font-size: 14px;
color: #666;
margin-top: 5px;
}
</style>

预期输出:界面显示数字 0,点击「+1」数字变成 1,底下出现一行「+1 → 1」。

这代码在干嘛ref() 创建响应式变量,@click 绑定点击事件,v-for 渲染列表——三板斧打天下。


项目 2(15 分钟):外卖订单管理

目标:从 JSON 读取订单数据,显示订单列表,支持按状态筛选。

这个项目引入了「数据从哪里来」的问题,帮你理解 uniapp 的数据绑定和条件渲染。

<!-- static/orders.json - 模拟后端数据 -->
[
{ "id": 1, "shop": "麦当劳", "food": "巨无霸套餐", "price": 35, "status": "已完成" },
{ "id": 2, "shop": "肯德基", "food": "吮指原味鸡", "price": 25, "status": "配送中" },
{ "id": 3, "shop": "华莱士", "food": "香辣鸡腿堡", "price": 18, "status": "待支付" },
{ "id": 4, "shop": "沙县小吃", "food": "拌面扁肉", "price": 12, "status": "已完成" },
{ "id": 5, "shop": "兰州拉面", "food": "牛肉拉面", "price": 22, "status": "配送中" }
]
<!-- pages/orders/orders.vue -->
<script setup>
import { ref, computed } from 'vue'
import orderData from '@/static/orders.json'

// 当前筛选条件
const filterStatus = ref('全部')

// 订单列表
const orders = ref(orderData)

// 筛选后的订单
const filteredOrders = computed(() => {
if (filterStatus.value === '全部') {
return orders.value
}
return orders.value.filter(order => order.status === filterStatus.value)
})

// 计算总消费
const totalPrice = computed(() => {
return filteredOrders.value.reduce((sum, order) => sum + order.price, 0)
})

// 状态选项
const statusOptions = ['全部', '待支付', '配送中', '已完成']
</script>

<template>
<view class="container">
<text class="title">我的外卖订单</text>

<!-- 筛选栏 -->
<view class="filter-bar">
  <view
    v-for="status in statusOptions"
    :key="status"
    :class="['filter-btn', { active: filterStatus === status }]"
    @click="filterStatus = status"
  >
    {{ status }}
  </view>
</view>

<!-- 订单列表 -->
<view class="order-list">
  <view v-for="order in filteredOrders" :key="order.id" class="order-card">
    <view class="order-header">
      <text class="shop">{{ order.shop }}</text>
      <text :class="['status', order.status]">{{ order.status }}</text>
    </view>
    <text class="food">{{ order.food }}</text>
    <text class="price">¥{{ order.price }}</text>
  </view>
</view>

<!-- 底部统计 -->
<view class="footer">
  <text>共 {{ filteredOrders.length }} 单,合计 </text>
  <text class="total-price">¥{{ totalPrice }}</text>
</view>
</view>
</template>

<style>
.container {
padding: 20px;
background: #f8f8f8;
min-height: 100vh;
}
.title {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.filter-bar {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-btn {
padding: 8px 16px;
border-radius: 20px;
background: #fff;
font-size: 14px;
color: #666;
}
.filter-btn.active {
background: #007AFF;
color: #fff;
}
.order-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.order-card {
background: #fff;
padding: 15px;
border-radius: 12px;
}
.order-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.shop {
font-weight: bold;
font-size: 16px;
}
.status {
font-size: 12px;
padding: 2px 8px;
border-radius: 4px;
}
.status.待支付 { background: #FFF3E0; color: #FF9800; }
.status.配送中 { background: #E3F2FD; color: #2196F3; }
.status.已完成 { background: #E8F5E9; color: #4CAF50; }
.food { color: #666; font-size: 14px; }
.price { color: #FF5722; font-size: 18px; font-weight: bold; }
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 15px 20px;
background: #fff;
display: flex;
justify-content: flex-end;
align-items: center;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
.total-price {
color: #FF5722;
font-size: 20px;
font-weight: bold;
}
</style>

预期输出:显示订单列表,点击「配送中」只显示 2 条配送中订单,底部显示合计价格。

这代码在干嘛computed() 计算属性自动根据筛选条件更新结果,v-for 渲染列表,v-bind:class 动态切换样式。


项目 3(15 分钟):校园二手交易小程序

目标:发布闲置物品、浏览商品列表、支持收藏。

这个项目组合了前两个项目的能力,加入了「页面跳转」「本地存储」等真实场景功能。

<!-- pages/market/market.vue -->
<script setup>
import { ref, onMount } from 'vue'

// 商品列表
const products = ref([
{ id: 1, name: '二手教材《高等数学》', price: 25, oldPrice: 45, seller: '学霸小明', isFavorite: false },
{ id: 2, name: '台灯 九成新', price: 30, oldPrice: 80, seller: '夜猫子', isFavorite: true },
{ id: 3, name: '自行车 150出', price: 150, oldPrice: 400, seller: '骑手老王', isFavorite: false },
])

// 我的发布
const myProducts = ref([])

// Tab 切换
const activeTab = ref('browse')

// 收藏列表
const favorites = computed(() => products.value.filter(p => p.isFavorite))

// 切换收藏状态
const toggleFavorite = (product) => {
product.isFavorite = !product.isFavorite
// 保存到本地存储
uni.setStorageSync('favorites', products.value.filter(p => p.isFavorite).map(p => p.id))
}

// 加载本地收藏状态
const loadFavorites = () => {
const savedIds = uni.getStorageSync('favorites') || []
products.value.forEach(p => {
p.isFavorite = savedIds.includes(p.id)
})
}

// 新商品表单
const newProduct = ref({ name: '', price: '', seller: '' })

// 发布商品
const publishProduct = () => {
if (!newProduct.value.name || !newProduct.value.price) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
myProducts.value.push({
id: Date.now(),
name: newProduct.value.name,
price: Number(newProduct.value.price),
oldPrice: 0,
seller: newProduct.value.seller || '匿名',
isFavorite: false
})
// 清空表单
newProduct.value = { name: '', price: '', seller: '' }
uni.showToast({ title: '发布成功', icon: 'success' })
}

// 页面加载时读取收藏
onMount(() => {
loadFavorites()
})

import { computed } from 'vue'
</script>

<template>
<view class="container">
<!-- Tab 栏 -->
<view class="tab-bar">
  <view
    :class="['tab', { active: activeTab === 'browse' }]"
    @click="activeTab = 'browse'"
  >浏览</view>
  <view
    :class="['tab', { active: activeTab === 'publish' }]"
    @click="activeTab = 'publish'"
  >发布</view>
  <view
    :class="['tab', { active: activeTab === 'favorites' }]"
    @click="activeTab = 'favorites'"
  >收藏({{ favorites.length }})</view>
</view>

<!-- 浏览模式 -->
<view v-if="activeTab === 'browse'" class="product-list">
  <view v-for="product in products" :key="product.id" class="product-card">
    <view class="product-info">
      <text class="product-name">{{ product.name }}</text>
      <text class="seller">卖家:{{ product.seller }}</text>
      <view class="price-row">
        <text class="price">¥{{ product.price }}</text>
        <text class="old-price">¥{{ product.oldPrice }}</text>
      </view>
    </view>
    <view
      :class="['favorite-btn', { favorited: product.isFavorite }]"
      @click="toggleFavorite(product)"
    >
      {{ product.isFavorite ? '❤️' : '🤍' }}
    </view>
  </view>
</view>

<!-- 发布模式 -->
<view v-if="activeTab === 'publish'" class="publish-form">
  <input
    v-model="newProduct.name"
    class="input"
    placeholder="商品名称"
  />
  <input
    v-model="newProduct.price"
    class="input"
    type="number"
    placeholder="价格(元)"
  />
  <input
    v-model="newProduct.seller"
    class="input"
    placeholder="你的昵称(选填)"
  />
  <button type="primary" @click="publishProduct">发布</button>

  <!-- 我的发布列表 -->
  <view v-if="myProducts.length > 0" class="my-products">
    <text class="section-title">我发布的</text>
    <view v-for="product in myProducts" :key="product.id" class="product-card small">
      <text>{{ product.name }}</text>
      <text class="price">¥{{ product.price }}</text>
    </view>
  </view>
</view>

<!-- 收藏模式 -->
<view v-if="activeTab === 'favorites'" class="product-list">
  <view v-if="favorites.length === 0" class="empty">
    <text>还没有收藏,快去逛逛吧~</text>
  </view>
  <view v-for="product in favorites" :key="product.id" class="product-card">
    <view class="product-info">
      <text class="product-name">{{ product.name }}</text>
      <text class="seller">卖家:{{ product.seller }}</text>
      <text class="price">¥{{ product.price }}</text>
    </view>
    <view class="favorite-btn favorited" @click="toggleFavorite(product)">❤️</view>
  </view>
</view>
</view>
</template>

<style>
.container {
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.tab-bar {
display: flex;
background: #fff;
border-radius: 12px;
padding: 5px;
margin-bottom: 20px;
}
.tab {
flex: 1;
text-align: center;
padding: 10px 0;
border-radius: 8px;
color: #666;
}
.tab.active {
background: #007AFF;
color: #fff;
}
.product-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.product-card {
background: #fff;
padding: 15px;
border-radius: 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.product-card.small {
padding: 10px;
}
.product-name {
font-size: 16px;
font-weight: bold;
display: block;
}
.seller {
font-size: 12px;
color: #999;
}
.price-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 5px;
}
.price {
color: #FF5722;
font-size: 18px;
font-weight: bold;
}
.old-price {
color: #999;
font-size: 14px;
text-decoration: line-through;
}
.favorite-btn {
font-size: 24px;
padding: 10px;
}
.publish-form {
background: #fff;
padding: 20px;
border-radius: 12px;
}
.input {
border: 1px solid #ddd;
padding: 12px;
border-radius: 8px;
margin-bottom: 15px;
}
.section-title {
display: block;
font-size: 16px;
font-weight: bold;
margin: 20px 0 10px;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
</style>

预期输出:三个 Tab 切换,浏览商品可收藏,收藏数据会保存到本地(刷新后不丢失),发布商品后可在「发布」Tab 看到自己的发布。

这代码在干嘛uni.setStorageSync() 把收藏状态存到手机本地,uni.showToast() 弹提示框——uniapp 封装好的跨平台 API。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:ref 和 reactive 选错导致不更新

// ❌ 错误示范:对象用 ref,但没有 .value
const user = ref({ name: '小明' })
user.name = '小红'  // 这样不生效!

// ✅ 正确做法 1:加 .value
const user = ref({ name: '小明' })
user.value.name = '小红'  // 生效

// ✅ 正确做法 2:用 reactive(推荐用于对象)
const user = reactive({ name: '小明' })
user.name = '小红'  // 生效

坑 2:onMounted 里拿不到 onLoad 传递的参数

// ❌ 错误示范:在 onMounted 里读页面参数
onMount(() => {
// onLoad 的参数在这里拿不到!
const id = options.id  // undefined
})

// ✅ 正确做法:参数在 onLoad 里拿
onLoad((options) => {
const id = options.id  // 正确
// 如果需要在其他地方用,存到 data 里
productId.value = id
loadProductDetail()
})

坑 3:v-for 没加 key 导致列表错乱

<!-- ❌ 错误示范:列表渲染不加 key -->
<view v-for="item in list" @click="deleteItem(item)">
{{ item.name }}
</view>

<!-- ✅ 正确做法:加唯一 key -->
<view v-for="item in list" :key="item.id" @click="deleteItem(item)">
{{ item.name }}
</view>

为什么重要:没有 key,删除某一项时 Vue 可能只更新了 UI 没更新数据,导致显示错乱。

坑 4:小程序里 console.log 不显示

// ❌ 瞎调试:在小程序里用 console.log
console.log('看看这个值是啥')  // 控制台看不到!

// ✅ 正确做法:用 uni.showToast 弹出来
uni.showToast({ title: `值是 ${someValue}`, icon: 'none', duration: 3000 })

// ✅ 进阶:编译成 H5 时用 console.log
// HBuilderX 控制台能看到,小程序里用 vConsole(uniapp 内置)

坑 5:样式不生效?可能是 scoped 搞的鬼

<!-- 父组件 -->
<template>
<view class="parent">
<child-component class="child" />
</view>
</template>

<!-- ❌ 如果 child-component 用了 scoped,子组件里 .parent 样式不生效 -->

<!-- ✅ 正确做法:用 :deep() 穿透 -->
<style scoped>
.parent :deep(.child) {
/* 这样能穿透到子组件 */
}
</style>

性能小贴士:图片懒加载

<!-- ❌ 一次性加载所有图片,慢 -->
<image v-for="img in images" :src="img.url" />

<!-- ✅ 懒加载,只在可见时加载 -->
<image
v-for="img in images"
:src="img.url"
lazy-load
mode="widthFix"
/>

调试技巧:用 console.group 分组日志

// 乱七八糟的日志不好找?
console.log('开始请求')
console.log('参数:', params)
console.log('结果:', result)

// ✅ 用分组,看起来清晰多了
console.group('订单查询')
console.log('参数:', params)
console.log('结果:', result)
console.groupEnd()

✏️ 练习题 + 作业题

练习题(5 道,10 分钟)

练习 1(2 分钟):计数器改一改
- 输入:在计数器项目里,把初始值改成 10
- 预期输出:页面打开显示 10,点击 +1 变成 11
- 提示:找 ref(0) 改成 ref(10)

练习 2(2 分钟):加个条件判断
- 输入:在项目 1 里加个逻辑:如果 count 超过 20,弹提示「不能再加了」
- 预期输出:count 到 20 后点击按钮,弹出提示
- 提示:用 if (count.value >= 20) 判断

练习 3(2 分钟):换个数据源
- 输入:把项目 2 的 orders.json 改成 3 条你自己的订单(可以是零食、游戏道具等)
- 预期输出:页面显示你的新订单数据
- 提示:直接改 JSON 文件内容

练习 4(2 分钟):串起两个项目
- 输入:把项目 2 的订单筛选 + 项目 3 的 Tab 切换合并
- 预期输出:一个有「全部/已完成/待支付」Tab 的订单管理页面
- 提示:复制项目 2 的筛选逻辑,粘贴到项目 3 的结构里

练习 5(2 分钟):看报错修 Bug
- 输入:下面代码点按钮没反应,是什么问题?

const count = ref(0)
const add = () => {
count = count + 1  // 少了 .value
}
  • 预期输出:说出错误原因并给出正确代码
  • 提示:ref 创建的响应式变量,修改要加 .value

作业题(30 分钟-2 小时)

作业:做一个「室友作息互助表」

  • 需求描述:期末考试周,4 个人住一个宿舍,作息不一致容易互相打扰。做一个工具让大家登记自己的「可打扰时间」和「勿扰时间」。
  • 功能点:
    1. 添加室友信息(名字、可打扰时段、勿扰时段)
    2. 查看所有人的作息时间表
    3. 计算「大家都不忙」的公共时间段
    4. 数据保存到本地(下次打开还在)
  • 加分项:
    1. 用不同颜色区分「可打扰」「勿扰」「公共时间」
    2. 支持删除和编辑已添加的室友
  • 验收标准:能跑起来 + 切换 Tab 有响应 + 数据刷新不丢失
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

本章 3 个核心收获

  1. uniapp 是 Vue3 的跨平台外套:用 Vue3 语法写,一次编译出 iOS/Android/小程序/H5
  2. 响应式是 Vue3 的精髓ref()reactive() 让数据变化自动更新 UI,不用手动操作 DOM
  3. 生命周期要分清onLoad 拿页面参数,onMounted 做初始化,顺序不能乱

延伸学习资源

资源 推荐理由
uniapp 官方文档 最权威,有各平台 API 差异说明
DCloud 插件市场 别人写好的轮子,避免重复造轮子
《Vue3 设计与实现》 进阶理解 Vue3 响应式原理

互动钩子

你有没有遇到过「App 和小程序体验不一致」的坑?比如同一个按钮在 iOS 上能点,在安卓上点不了?评论区聊聊,老粉优先回复!


下章剧透:uniapp 写完只能算「网页套壳」,想做一个真正的桌面应用(带系统托盘、能读本地文件、能打包成 .exe)?下章我们用 Electron + Vue3,让你写一个「原生味」十足的桌面软件。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。