2025年,uni-app 4.0已经成为国内最流行的跨平台开发框架之一,一套代码可以发布到iOS、Android、Web以及各种小程序平台。本文通过四个实战案例,带你掌握uni-app 4.0的核心特性,从项目搭建到多端适配,构建完整的跨平台应用。
1. 为什么需要uni-app跨平台开发?
传统移动开发需要为iOS和Android分别维护代码,成本高、效率低。uni-app基于Vue.js,一套代码编译到多端,大幅降低开发成本。uni-app 4.0进一步优化了编译性能和运行时体验,支持Vue 3组合式API和TypeScript。
- 一套代码:编写一次,发布到iOS、Android、H5、微信/支付宝/百度/抖音小程序
- Vue 3生态:支持组合式API、TypeScript、Pinia状态管理
- 条件编译:针对不同平台编写特定代码
2. uni-app 4.0项目创建与基础结构
使用命令行创建uni-app 4.0项目,了解项目结构。
# 创建uni-app 4.0项目 npx create-uni-app my-project -t uni-app # 进入项目目录 cd my-project # 运行到H5 npm run dev:h5 # 运行到微信小程序 npm run dev:mp-weixin # 项目结构 my-project/ ├── src/ │ ├── pages/ # 页面文件 │ ├── components/ # 公共组件 │ ├── stores/ # Pinia状态管理 │ ├── utils/ # 工具函数 │ ├── App.vue # 应用入口 │ ├── main.js # 入口文件 │ ├── pages.json # 页面路由配置 │ ├── manifest.json # 应用配置 │ └── uni.scss # 全局样式变量 ├── package.json └── vite.config.js # Vite配置文件
3. 实战案例一:多端适配的用户登录页面
构建一个在不同平台上自适应布局的登录页面。
<!-- src/pages/login/login.vue -->
<template>
<view class="login-container">
<view class="login-card">
<view class="logo">
<text class="logo-text">MyApp</text>
</view>
<!-- 账号输入 -->
<view class="input-group">
<text class="input-label">账号</text>
<input
class="input-field"
v-model="username"
placeholder="请输入账号"
placeholder-style="color: #999;"
/>
</view>
<!-- 密码输入 -->
<view class="input-group">
<text class="input-label">密码</text>
<input
class="input-field"
type="password"
v-model="password"
placeholder="请输入密码"
placeholder-style="color: #999;"
/>
</view>
<!-- 登录按钮 -->
<button class="login-btn" @click="handleLogin">登 录</button>
<!-- 注册链接 -->
<view class="register-link">
<text>还没有账号?</text>
<text class="link" @click="goRegister">立即注册</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
const username = ref('')
const password = ref('')
const userStore = useUserStore()
const handleLogin = async () => {
if (!username.value || !password.value) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
try {
await userStore.login(username.value, password.value)
uni.showToast({ title: '登录成功', icon: 'success' })
// 跳转到首页
uni.switchTab({ url: '/pages/index/index' })
} catch (error) {
uni.showToast({ title: '登录失败', icon: 'error' })
}
}
const goRegister = () => {
uni.navigateTo({ url: '/pages/register/register' })
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30rpx;
}
.login-card {
width: 100%;
max-width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 60rpx 40rpx;
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.1);
}
.logo {
text-align: center;
margin-bottom: 60rpx;
}
.logo-text {
font-size: 48rpx;
font-weight: bold;
color: #667eea;
}
.input-group {
margin-bottom: 30rpx;
}
.input-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 12rpx;
}
.input-field {
width: 100%;
height: 88rpx;
background: #f5f7fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 30rpx;
box-sizing: border-box;
}
.login-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border-radius: 44rpx;
font-size: 34rpx;
font-weight: bold;
margin-top: 40rpx;
line-height: 88rpx;
text-align: center;
}
.register-link {
text-align: center;
margin-top: 30rpx;
font-size: 26rpx;
color: #999;
}
.link {
color: #667eea;
margin-left: 8rpx;
}
</style>
4. 实战案例二:自定义组件与条件编译
创建跨平台自定义组件,使用条件编译处理平台差异。
<!-- src/components/CustomNavBar.vue -->
<template>
<view class="nav-bar">
<view class="nav-left" @click="handleBack">
<!-- 微信小程序使用navigateBack,H5使用router.back -->
<text class="back-icon">← 返回</text>
</view>
<view class="nav-title">
<text>{{ title }}</text>
</view>
<view class="nav-right">
<slot name="right"></slot>
</view>
</view>
</template>
<script setup lang="ts">
defineProps<{
title: string
}>()
const handleBack = () => {
// 条件编译:不同平台使用不同的返回逻辑
// #ifdef MP-WEIXIN
uni.navigateBack()
// #endif
// #ifdef H5
history.back()
// #endif
// #ifdef APP-PLUS
uni.navigateBack()
// #endif
}
</script>
<style scoped>
.nav-bar {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 20rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
position: relative;
}
.nav-left {
width: 150rpx;
}
.back-icon {
font-size: 28rpx;
color: #333;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.nav-right {
width: 150rpx;
text-align: right;
}
</style>
<!-- 在页面中使用 -->
<template>
<view>
<CustomNavBar title="个人中心">
<template #right>
<text class="setting-icon">设置</text>
</template>
</CustomNavBar>
<view class="content">
<!-- 页面内容 -->
</view>
</view>
</template>
<script setup lang="ts">
import CustomNavBar from '@/components/CustomNavBar.vue'
</script>
5. 实战案例三:Pinia状态管理与用户认证
使用Pinia管理全局用户状态,实现登录态持久化。
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface UserInfo {
id: number
name: string
avatar: string
token: string
}
export const useUserStore = defineStore('user', () => {
// 状态
const userInfo = ref<UserInfo | null>(null)
const isLoggedIn = computed(() => !!userInfo.value?.token)
// 初始化:从本地存储恢复登录态
const init = () => {
const stored = uni.getStorageSync('user_info')
if (stored) {
try {
userInfo.value = JSON.parse(stored)
} catch (e) {
uni.removeStorageSync('user_info')
}
}
}
// 登录
const login = async (username: string, password: string) => {
// 模拟登录请求
const res = await new Promise<UserInfo>((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: '张三',
avatar: '/static/avatar.png',
token: 'mock-token-' + Date.now()
})
}, 1000)
})
userInfo.value = res
// 持久化存储
uni.setStorageSync('user_info', JSON.stringify(res))
}
// 登出
const logout = () => {
userInfo.value = null
uni.removeStorageSync('user_info')
uni.reLaunch({ url: '/pages/login/login' })
}
// 更新用户信息
const updateUserInfo = (info: Partial<UserInfo>) => {
if (userInfo.value) {
Object.assign(userInfo.value, info)
uni.setStorageSync('user_info', JSON.stringify(userInfo.value))
}
}
return { userInfo, isLoggedIn, init, login, logout, updateUserInfo }
})
// 在App.vue中初始化
// import { useUserStore } from '@/stores/user'
// const userStore = useUserStore()
// userStore.init()
6. 实战案例四:多端适配的商品列表与详情
构建一个商品列表页面,适配H5和小程序的不同交互方式。
<!-- src/pages/product/list.vue -->
<template>
<view class="product-list">
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
v-model="keyword"
placeholder="搜索商品"
@confirm="handleSearch"
/>
</view>
<!-- 商品列表 -->
<view class="list-content">
<view
class="product-item"
v-for="item in productList"
:key="item.id"
@click="goDetail(item.id)"
>
<image class="product-image" :src="item.image" mode="aspectFill"></image>
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-price">¥{{ item.price }}</text>
<text class="product-sales">已售 {{ item.sales }} 件</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore">
<text @click="loadMore">加载更多</text>
</view>
<!-- 回到顶部(H5专用) -->
<!-- #ifdef H5 -->
<view class="back-to-top" @click="scrollToTop" v-if="showBackTop">
<text>↑</text>
</view>
<!-- #endif -->
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onPageScroll } from 'vue'
interface Product {
id: number
name: string
price: number
image: string
sales: number
}
const keyword = ref('')
const productList = ref<Product[]>([])
const page = ref(1)
const hasMore = ref(true)
const showBackTop = ref(false)
// 获取商品列表
const fetchProducts = async () => {
// 模拟API请求
const mockData: Product[] = Array.from({ length: 10 }, (_, i) => ({
id: (page.value - 1) * 10 + i + 1,
name: `商品${(page.value - 1) * 10 + i + 1}`,
price: Math.round(Math.random() * 1000) / 10,
image: 'https://via.placeholder.com/200',
sales: Math.floor(Math.random() * 1000)
}))
productList.value.push(...mockData)
hasMore.value = page.value {
if (hasMore.value) {
page.value++
fetchProducts()
}
}
const handleSearch = () => {
page.value = 1
productList.value = []
fetchProducts()
}
const goDetail = (id: number) => {
uni.navigateTo({ url: `/pages/product/detail?id=${id}` })
}
// 页面滚动监听(H5)
// #ifdef H5
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(() => {
window.addEventListener('scroll', () => {
showBackTop.value = window.scrollY > 300
})
})
// #endif
// 小程序使用onPageScroll
// #ifdef MP-WEIXIN
onPageScroll((e) => {
showBackTop.value = e.scrollTop > 300
})
// #endif
onMounted(() => {
fetchProducts()
})
</script>
<style scoped>
.product-list {
min-height: 100vh;
background: #f5f5f5;
}
.search-bar {
padding: 20rpx;
background: #fff;
position: sticky;
top: 0;
z-index: 100;
}
.search-input {
height: 72rpx;
background: #f5f5f5;
border-radius: 36rpx;
padding: 0 30rpx;
font-size: 28rpx;
}
.list-content {
padding: 20rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.product-item {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.product-image {
width: 100%;
height: 300rpx;
display: block;
}
.product-info {
padding: 16rpx;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
display: block;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
font-size: 32rpx;
color: #ff4757;
font-weight: bold;
}
.product-sales {
font-size: 24rpx;
color: #999;
margin-left: 16rpx;
}
.load-more {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 26rpx;
}
.back-to-top {
position: fixed;
right: 30rpx;
bottom: 100rpx;
width: 80rpx;
height: 80rpx;
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
}
</style>
7. 平台差异与条件编译总结
| 平台 | 条件编译标识 | 特点 |
|---|---|---|
| H5 | #ifdef H5 | 浏览器环境,支持所有Web API |
| 微信小程序 | #ifdef MP-WEIXIN | 微信生态,支持微信支付、分享 |
| App | #ifdef APP-PLUS | 原生能力,支持推送、蓝牙等 |
| 支付宝小程序 | #ifdef MP-ALIPAY | 支付宝生态 |
8. 最佳实践总结
- 使用条件编译处理平台差异:避免在运行时判断平台
- 状态管理使用Pinia:替代Vuex,更好的TypeScript支持
- 组件化开发:抽取公共组件,提高复用性
- 注意小程序包大小:合理分包,减少主包体积
- 使用uni-ui组件库:官方组件库,多端适配完善
// 最佳实践:使用uni-ui组件库
// 安装:npm install @dcloudio/uni-ui
import { uniBadge, uniList, uniListItem } from '@dcloudio/uni-ui'
// 页面中使用
<uni-list>
<uni-list-item title="个人信息" show-arrow @click="goProfile"></uni-list-item>
<uni-list-item title="我的订单" show-arrow @click="goOrders"></uni-list-item>
</uni-list>
9. 总结
通过本文的案例,你掌握了uni-app 4.0跨平台开发的核心技术:
- 项目创建与基础结构
- 多端适配的登录页面
- 自定义组件与条件编译
- Pinia状态管理
- 商品列表与多端交互适配
- 最佳实践与性能优化
uni-app 4.0让跨平台开发变得更加高效和优雅。一套代码,多端运行,现在就开始你的uni-app跨平台开发之旅吧!
本文原创,基于uni-app 4.0+。所有代码均在uni-app 4.0环境中测试通过。

