uni-app 多端适配技巧
章节介绍
uni-app 的核心优势之一是能够开发一次,发布到多个平台。然而,由于不同平台的特性和限制存在差异,开发者需要掌握一定的多端适配技巧,才能确保应用在所有平台上都能正常运行并提供良好的用户体验。本章节将详细介绍 uni-app 的多端适配技巧,包括平台差异分析、适配策略和条件编译最佳实践。
核心知识点
1. 平台差异分析
uni-app 支持的平台包括:
- App 端:iOS、Android
- 小程序端:微信小程序、支付宝小程序、百度小程序、抖音小程序、QQ 小程序等
- Web 端:PC 浏览器、移动浏览器
不同平台的主要差异包括:
1.1 运行环境差异
- App 端:基于原生引擎运行,性能最好,功能最完整
- 小程序端:基于各平台的小程序运行时,功能和性能有一定限制
- Web 端:基于浏览器运行,受浏览器特性和安全策略限制
1.2 API 差异
- App 端:支持完整的 uni-app API 和原生 API
- 小程序端:只支持 uni-app API 中已适配的部分,且各平台 API 可能存在差异
- Web 端:只支持 uni-app API 中已适配的部分,且部分 API 可能无法在浏览器中实现
1.3 组件差异
- App 端:支持完整的 uni-app 组件和原生组件
- 小程序端:只支持 uni-app 组件中已适配的部分,且各平台组件可能存在差异
- Web 端:只支持 uni-app 组件中已适配的部分,且部分组件可能无法在浏览器中完美实现
1.4 样式差异
- App 端:支持大部分 CSS 特性,且有一些原生特有的样式
- 小程序端:对 CSS 特性的支持程度不同,且各平台可能有特殊的样式限制
- Web 端:支持标准 CSS 特性,但受浏览器兼容性影响
1.5 性能差异
- App 端:性能最好,支持复杂的动画和交互
- 小程序端:性能次之,对复杂的动画和交互有一定限制
- Web 端:性能受浏览器和网络环境影响较大
2. 适配策略
针对不同平台的差异,uni-app 提供了多种适配策略:
2.1 条件编译
条件编译是 uni-app 最核心的多端适配技术,它允许开发者在同一套代码中,为不同平台编写不同的代码。条件编译使用特殊的注释语法,在编译时根据目标平台只保留对应平台的代码。
2.2 平台判断
在运行时,通过 uni.getSystemInfoSync().platform 或 process.env.VUE_APP_PLATFORM 来判断当前平台,从而执行不同的代码逻辑。
2.3 API 适配
- 使用 uni-app 提供的跨平台 API,避免直接使用平台特定的 API
- 对于平台特定的 API,使用条件编译或平台判断进行适配
- 对于不存在的 API,提供降级方案
2.4 组件适配
- 使用 uni-app 提供的跨平台组件,避免直接使用平台特定的组件
- 对于平台特定的组件,使用条件编译或平台判断进行适配
- 对于不存在的组件,提供自定义组件或降级方案
2.5 样式适配
- 使用 uni-app 推荐的样式方案,如 rpx 单位
- 对于平台特定的样式,使用条件编译进行适配
- 对于样式差异较大的平台,提供平台特定的样式文件
2.6 性能适配
- 根据平台性能差异,调整应用的复杂度和动画效果
- 对于性能较差的平台,提供简化版的界面和交互
- 优化代码,减少不必要的计算和渲染
3. 条件编译最佳实践
条件编译是 uni-app 多端适配的核心技术,正确使用条件编译可以大大提高开发效率和代码质量。
3.1 条件编译的语法
uni-app 支持以下条件编译语法:
- HTML 模板:
<!-- #ifdef PLATFORM -->和<!-- #endif --> - CSS 样式:
/* #ifdef PLATFORM */和/* #endif */ - JavaScript:
// #ifdef PLATFORM和// #endif - JSON 配置:
"__PLATFORM__": true
其中,PLATFORM 可以是以下值:
APP-PLUS:App 端APP-PLUS-NVUE:App 端的 nvue 页面MP-WEIXIN:微信小程序MP-ALIPAY:支付宝小程序MP-BAIDU:百度小程序MP-TOUTIAO:抖音小程序MP-QQ:QQ 小程序MP-KUAISHOU:快手小程序MP-JD:京东小程序MP-REDMI:红米小程序WEB:Web 端H5:H5 端(等同于 WEB)MP:所有小程序端APP:所有 App 端
3.2 条件编译的使用场景
- API 调用:当不同平台的 API 存在差异时
- 组件使用:当不同平台的组件存在差异时
- 样式调整:当不同平台的样式需要调整时
- 功能开关:当某些功能只在特定平台支持时
- 性能优化:当不同平台需要不同的性能优化策略时
3.3 条件编译的最佳实践
- 合理使用:只在必要时使用条件编译,避免过度使用导致代码混乱
- 分类管理:将平台特定的代码集中管理,提高代码可维护性
- 注释说明:为条件编译的代码添加注释,说明适配的原因和平台
- 测试验证:在所有目标平台上测试条件编译的代码,确保其正常工作
- 文档化:将多端适配的策略和注意事项文档化,方便团队协作
4. 多端适配工具和插件
uni-app 生态提供了多种多端适配工具和插件:
- uni-app 官方插件市场:提供了大量多端适配相关的插件
- uni-cloud:提供了云端能力,减少端侧差异
- uni-ui:提供了一套跨平台的 UI 组件库
- uni-app x:新一代 uni-app 引擎,提供更好的多端适配能力
5. 多端适配的常见问题和解决方案
5.1 API 不存在或差异
问题:某些 API 在特定平台不存在或行为不同。
解决方案:
- 使用条件编译为不同平台提供不同的实现
- 提供降级方案,当 API 不存在时使用替代方案
- 使用 uni-app 官方推荐的跨平台 API
5.2 组件表现不一致
问题:同一组件在不同平台的表现不一致。
解决方案:
- 使用条件编译为不同平台提供不同的组件实现
- 针对特定平台的组件进行样式调整
- 使用自定义组件统一不同平台的表现
5.3 样式差异
问题:相同的样式在不同平台的表现不一致。
解决方案:
- 使用 rpx 单位,自动适配不同屏幕尺寸
- 使用条件编译为不同平台提供不同的样式
- 避免使用平台特定的样式特性
5.4 性能问题
问题:应用在某些平台上性能较差。
解决方案:
- 根据平台性能差异,调整应用的复杂度和动画效果
- 使用条件编译为不同平台提供不同的性能优化策略
- 优化代码,减少不必要的计算和渲染
5.5 审核问题
问题:应用在某些平台审核不通过。
解决方案:
- 了解各平台的审核规则和禁忌
- 使用条件编译为不同平台提供符合其审核规则的实现
- 避免使用平台禁止的 API 和功能
实用案例分析
案例一:实现平台特定的登录功能
功能说明
实现一个登录功能,在不同平台使用不同的登录方式:
- App 端:支持手机号登录、微信登录、Apple ID 登录(iOS)
- 微信小程序:支持微信登录
- 支付宝小程序:支持支付宝登录
- Web 端:支持手机号登录
实现步骤
创建登录页面
<template> <view class="login-container"> <view class="login-form"> <view class="form-item"> <input type="text" v-model="phone" placeholder="请输入手机号" /> </view> <view class="form-item"> <input type="password" v-model="password" placeholder="请输入密码" /> </view> <button class="login-btn" @click="phoneLogin">手机号登录</button> </view> <view class="third-party-login"> <!-- #ifdef MP-WEIXIN --> <button class="wechat-login-btn" @click="wechatLogin">微信登录</button> <!-- #endif --> <!-- #ifdef MP-ALIPAY --> <button class="alipay-login-btn" @click="alipayLogin">支付宝登录</button> <!-- #endif --> <!-- #ifdef APP-PLUS --> <button class="wechat-login-btn" @click="wechatLogin">微信登录</button> <!-- #endif --> <!-- #ifdef APP-PLUS && iOS --> <button class="apple-login-btn" @click="appleLogin">Apple ID 登录</button> <!-- #endif --> </view> </view> </template> <script> export default { data() { return { phone: '', password: '' }; }, methods: { // 手机号登录 phoneLogin() { // 实现手机号登录逻辑 console.log('手机号登录', this.phone, this.password); }, // 微信登录 wechatLogin() { // #ifdef APP-PLUS // App 端微信登录 uni.login({ provider: 'weixin', success: (res) => { console.log('微信登录成功', res); // 发送 code 到服务器进行验证 }, fail: (err) => { console.log('微信登录失败', err); } }); // #endif // #ifdef MP-WEIXIN // 微信小程序登录 uni.login({ success: (res) => { console.log('微信登录成功', res); // 发送 code 到服务器进行验证 }, fail: (err) => { console.log('微信登录失败', err); } }); // #endif }, // 支付宝登录 alipayLogin() { // #ifdef MP-ALIPAY // 支付宝小程序登录 uni.login({ provider: 'alipay', success: (res) => { console.log('支付宝登录成功', res); // 发送 code 到服务器进行验证 }, fail: (err) => { console.log('支付宝登录失败', err); } }); // #endif }, // Apple ID 登录 appleLogin() { // #ifdef APP-PLUS && iOS // iOS 端 Apple ID 登录 uni.login({ provider: 'apple', success: (res) => { console.log('Apple ID 登录成功', res); // 发送 code 到服务器进行验证 }, fail: (err) => { console.log('Apple ID 登录失败', err); } }); // #endif } } }; </script> <style> .login-container { padding: 20px; } .login-form { margin-bottom: 30px; } .form-item { margin-bottom: 20px; } .form-item input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } .login-btn { width: 100%; padding: 12px; background-color: #007aff; color: white; border: none; border-radius: 5px; font-size: 16px; } .third-party-login { display: flex; flex-direction: column; gap: 10px; } .wechat-login-btn { width: 100%; padding: 12px; background-color: #07C160; color: white; border: none; border-radius: 5px; font-size: 16px; } .alipay-login-btn { width: 100%; padding: 12px; background-color: #1677FF; color: white; border: none; border-radius: 5px; font-size: 16px; } .apple-login-btn { width: 100%; padding: 12px; background-color: #000; color: white; border: none; border-radius: 5px; font-size: 16px; } /* #ifdef H5 */ /* Web 端特定样式 */ .third-party-login { display: none; } /* #endif */ </style>
案例二:实现平台特定的支付功能
功能说明
实现一个支付功能,在不同平台使用不同的支付方式:
- App 端:支持微信支付、支付宝支付
- 微信小程序:支持微信支付
- 支付宝小程序:支持支付宝支付
- Web 端:支持微信 H5 支付、支付宝 H5 支付
实现步骤
创建支付服务
// services/payment.js class PaymentService { // 发起支付 async requestPayment(orderInfo) { try { const platform = uni.getSystemInfoSync().platform; // 根据平台选择支付方式 if (platform === 'ios' || platform === 'android') { // App 端支付 return await this.appPayment(orderInfo); } else if (process.env.VUE_APP_PLATFORM === 'mp-weixin') { // 微信小程序支付 return await this.wechatMiniPayment(orderInfo); } else if (process.env.VUE_APP_PLATFORM === 'mp-alipay') { // 支付宝小程序支付 return await this.alipayMiniPayment(orderInfo); } else if (process.env.VUE_APP_PLATFORM === 'h5') { // Web 端支付 return await this.webPayment(orderInfo); } else { throw new Error('当前平台不支持支付功能'); } } catch (error) { console.log('支付失败', error); throw error; } } // App 端支付 async appPayment(orderInfo) { return new Promise((resolve, reject) => { uni.requestPayment({ provider: orderInfo.provider, // 'wxpay' 或 'alipay' orderInfo: orderInfo.orderInfo, success: (res) => { console.log('支付成功', res); resolve(res); }, fail: (err) => { console.log('支付失败', err); reject(err); } }); }); } // 微信小程序支付 async wechatMiniPayment(orderInfo) { return new Promise((resolve, reject) => { uni.requestPayment({ timeStamp: orderInfo.timeStamp, nonceStr: orderInfo.nonceStr, package: orderInfo.package, signType: orderInfo.signType, paySign: orderInfo.paySign, success: (res) => { console.log('支付成功', res); resolve(res); }, fail: (err) => { console.log('支付失败', err); reject(err); } }); }); } // 支付宝小程序支付 async alipayMiniPayment(orderInfo) { return new Promise((resolve, reject) => { uni.requestPayment({ provider: 'alipay', orderInfo: orderInfo.orderInfo, success: (res) => { console.log('支付成功', res); resolve(res); }, fail: (err) => { console.log('支付失败', err); reject(err); } }); }); } // Web 端支付 async webPayment(orderInfo) { // Web 端支付需要跳转到支付页面 if (orderInfo.provider === 'wxpay') { // 微信 H5 支付 window.location.href = orderInfo.payUrl; } else if (orderInfo.provider === 'alipay') { // 支付宝 H5 支付 window.location.href = orderInfo.payUrl; } else { throw new Error('不支持的支付方式'); } } } export default new PaymentService();使用支付服务
<template> <view class="payment-page"> <view class="order-info"> <view class="order-item"> <text>商品名称:</text> <text>{{ order.goodsName }}</text> </view> <view class="order-item"> <text>商品价格:</text> <text class="price">{{ order.price }} 元</text> </view> </view> <view class="payment-methods"> <view class="method-item" @click="selectPayment('wxpay')"> <text>微信支付</text> <radio :checked="selectedPayment === 'wxpay'" /> </view> <view class="method-item" @click="selectPayment('alipay')"> <text>支付宝支付</text> <radio :checked="selectedPayment === 'alipay'" /> </view> </view> <button class="pay-btn" @click="pay">立即支付</button> </view> </template> <script> import paymentService from '@/services/payment'; export default { data() { return { order: { goodsName: '测试商品', price: 99.9 }, selectedPayment: 'wxpay' }; }, methods: { selectPayment(method) { this.selectedPayment = method; }, async pay() { try { // 显示加载中 uni.showLoading({ title: '正在发起支付...' }); // 调用后端接口获取支付参数 const res = await uni.request({ url: 'https://example.com/api/payment/create', method: 'POST', data: { goodsName: this.order.goodsName, price: this.order.price, paymentMethod: this.selectedPayment, platform: process.env.VUE_APP_PLATFORM } }); if (res.statusCode === 200 && res.data.code === 0) { const orderInfo = res.data.data; // 发起支付 await paymentService.requestPayment(orderInfo); // 支付成功 uni.hideLoading(); uni.showToast({ title: '支付成功', icon: 'success' }); } else { uni.hideLoading(); uni.showToast({ title: '获取支付参数失败', icon: 'none' }); } } catch (error) { uni.hideLoading(); uni.showToast({ title: '支付失败,请稍后重试', icon: 'none' }); } } } }; </script> <style> .payment-page { padding: 20px; } .order-info { background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .order-item { display: flex; justify-content: space-between; margin-bottom: 10px; } .price { color: #ff4d4f; font-weight: bold; } .payment-methods { background-color: #fff; border-radius: 5px; margin-bottom: 30px; } .method-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #f0f0f0; } .method-item:last-child { border-bottom: none; } .pay-btn { width: 100%; padding: 12px; background-color: #007aff; color: white; border: none; border-radius: 5px; font-size: 16px; } /* #ifdef MP-WEIXIN */ /* 微信小程序隐藏支付宝支付 */ .method-item:nth-child(2) { display: none; } /* #endif */ /* #ifdef MP-ALIPAY */ /* 支付宝小程序隐藏微信支付 */ .method-item:nth-child(1) { display: none; } /* #endif */ </style>
案例三:实现响应式布局适配
功能说明
实现一个响应式布局,在不同屏幕尺寸和平台上都能提供良好的用户体验:
- App 端:适配不同尺寸的手机和平板
- 小程序端:适配不同尺寸的手机
- Web 端:适配 PC 浏览器和移动浏览器
实现步骤
使用 rpx 单位
uni-app 推荐使用 rpx 单位进行布局,rpx 会根据屏幕宽度自动调整大小。
<template> <view class="responsive-layout"> <view class="header"> <text class="title">响应式布局示例</text> </view> <view class="content"> <view class="grid-container"> <view class="grid-item" v-for="item in items" :key="item.id"> <image :src="item.image" mode="aspectFill" /> <text class="item-title">{{ item.title }}</text> </view> </view> </view> <view class="footer"> <text>© 2023 响应式布局示例</text> </view> </view> </template> <script> export default { data() { return { items: [ { id: 1, title: '商品 1', image: 'https://example.com/image1.jpg' }, { id: 2, title: '商品 2', image: 'https://example.com/image2.jpg' }, { id: 3, title: '商品 3', image: 'https://example.com/image3.jpg' }, { id: 4, title: '商品 4', image: 'https://example.com/image4.jpg' } ] }; } }; </script> <style> .responsive-layout { display: flex; flex-direction: column; min-height: 100vh; } .header { height: 80rpx; background-color: #007aff; color: white; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: bold; } .content { flex: 1; padding: 20rpx; } .grid-container { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; } .grid-item { background-color: #f5f5f5; border-radius: 10rpx; overflow: hidden; } .grid-item image { width: 100%; height: 300rpx; } .item-title { padding: 15rpx; font-size: 28rpx; } .footer { height: 60rpx; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 24rpx; color: #666; } /* #ifdef H5 */ /* Web 端适配 */ @media (min-width: 768px) { .grid-container { grid-template-columns: repeat(4, 1fr); } .grid-item image { height: 400rpx; } } @media (min-width: 1024px) { .grid-container { grid-template-columns: repeat(6, 1fr); } } /* #endif */ </style>使用条件编译适配不同平台
<template> <view class="platform-adaptation"> <!-- #ifdef APP-PLUS --> <view class="app-specific"> <text>App 端特有的内容</text> </view> <!-- #endif --> <!-- #ifdef MP --> <view class="mini-specific"> <text>小程序端特有的内容</text> </view> <!-- #endif --> <!-- #ifdef H5 --> <view class="web-specific"> <text>Web 端特有的内容</text> </view> <!-- #endif --> <view class="common-content"> <text>所有平台都有的内容</text> </view> </view> </template> <script> export default { mounted() { // 平台判断 const platform = uni.getSystemInfoSync().platform; console.log('当前平台:', platform); // 根据平台执行不同的逻辑 if (platform === 'ios') { console.log('iOS 平台特有的逻辑'); } else if (platform === 'android') { console.log('Android 平台特有的逻辑'); } else if (process.env.VUE_APP_PLATFORM === 'h5') { console.log('Web 平台特有的逻辑'); } } }; </script> <style> .platform-adaptation { padding: 20rpx; } .app-specific { background-color: #e3f2fd; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; } .mini-specific { background-color: #e8f5e8; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; } .web-specific { background-color: #fff3e0; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; } .common-content { background-color: #f5f5f5; padding: 20rpx; border-radius: 10rpx; } </style>
技术要点总结
平台差异识别:了解不同平台的运行环境、API、组件、样式和性能差异,是进行多端适配的基础。
适配策略选择:根据不同的场景和需求,选择合适的适配策略,如条件编译、平台判断、API 适配等。
条件编译使用:掌握条件编译的语法和最佳实践,合理使用条件编译来处理平台差异。
响应式布局:使用 rpx 单位和媒体查询,实现不同屏幕尺寸的适配。
性能优化:根据不同平台的性能特性,调整应用的复杂度和动画效果,确保在所有平台上都能提供良好的用户体验。
测试验证:在所有目标平台上测试应用,确保适配方案的有效性和稳定性。
文档化:将多端适配的策略和注意事项文档化,方便团队协作和后续维护。
学习目标
- 了解 uni-app 支持的平台及其差异
- 掌握 uni-app 的多端适配策略和方法
- 学会使用条件编译处理平台差异
- 理解如何实现响应式布局适配
- 掌握多端适配的最佳实践
- 能够开发在所有平台上都能正常运行的应用
章节小结
本章节详细介绍了 uni-app 的多端适配技巧,包括平台差异分析、适配策略和条件编译最佳实践。通过三个实用案例,展示了如何实现平台特定的登录功能、支付功能和响应式布局适配。
在进行多端适配时,开发者需要注意以下几点:
- 了解并识别不同平台的差异
- 选择合适的适配策略和方法
- 合理使用条件编译,避免过度使用
- 实现响应式布局,适配不同屏幕尺寸
- 根据平台性能差异,调整应用的复杂度和动画效果
- 在所有目标平台上测试应用,确保适配方案的有效性和稳定性
- 将多端适配的策略和注意事项文档化,方便团队协作和后续维护
通过掌握这些多端适配技巧,开发者可以充分发挥 uni-app 的跨平台优势,开发出在所有平台上都能正常运行并提供良好用户体验的应用。同时,多端适配也是一个持续优化的过程,开发者需要不断学习和实践,才能掌握其中的精髓。