uni-app 4.0 跨平台开发实战:一套代码多端运行

2026-05-06 0 593

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环境中测试通过。

uni-app 4.0 跨平台开发实战:一套代码多端运行
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 uniapp uni-app 4.0 跨平台开发实战:一套代码多端运行 https://www.taomawang.com/web/uniapp/1773.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务