第8章 8.1 移动端开发:uniapp 与 Vue3
上章我们用手把手的方式,带着你从 0 到 1 搭了一个「按钮+列表+弹窗」的组件库。说白了,就是把重复代码封装起来,下次用直接调。
但你有没有发现一个尴尬的问题——做完只能在浏览器里跑。手机扫码?不好意思,浏览器模拟不了真实 App 的体验。
这一章,我们换条路:用 uniapp + Vue3,一套代码同时跑在 iOS、Android、小程序、H5 上。你写一次,老板说「加个 App」,妈妈说「加个小程序」,你微微一笑——不用重写。
🎯 开场 3 分钟:为什么要学这个?
场景带入
想象你是校园外卖小哥,每天要手动统计「哪些店爆单、哪些店没人看」。你写成网页,室友想用——你总不能让他装 Python 环境吧。但如果是个手机点开就能用的小程序,发群里,大家都方便。
uniapp 就是干这个的:用 Vue 语法写代码,编译出来是小程序、是 App、是 H5,随你挑。
痛点来了
- 想做移动端,但不想同时学 Swift/Java/Kot\n\n
\n\n
\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 个核心收获
- uniapp 是 Vue3 的跨平台外套:用 Vue3 语法写,一次编译出 iOS/Android/小程序/H5
- 响应式是 Vue3 的精髓:
ref()和reactive()让数据变化自动更新 UI,不用手动操作 DOM - 生命周期要分清:
onLoad拿页面参数,onMounted做初始化,顺序不能乱
延伸学习资源
| 资源 | 推荐理由 |
|---|---|
| uniapp 官方文档 | 最权威,有各平台 API 差异说明 |
| DCloud 插件市场 | 别人写好的轮子,避免重复造轮子 |
| 《Vue3 设计与实现》 | 进阶理解 Vue3 响应式原理 |
互动钩子
你有没有遇到过「App 和小程序体验不一致」的坑?比如同一个按钮在 iOS 上能点,在安卓上点不了?评论区聊聊,老粉优先回复!
下章剧透:uniapp 写完只能算「网页套壳」,想做一个真正的桌面应用(带系统托盘、能读本地文件、能打包成 .exe)?下章我们用 Electron + Vue3,让你写一个「原生味」十足的桌面软件。敬请期待!

评论(0)