只会傻傻的用Uniapp写微信小程序?快来收下这份跨平台开发避坑指南和实战教程吧
书接上篇,我们今天继续来学习一下 UniApp 聊一个让无数跨端开发者头秃的话题:不同平台下的功能和API差异,附带实现思路和所有源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
致开发者的忠告: AI编程盛行的今天,我们并不是不需要学习技术,而是更应该专研技术,拥有把控全局的架构设计思维才能在AI盛行的未来有立足之地。
言归正传,咱们今天继续来聊一聊如何区分不同平台下用uniapp开发时的UI差异、功能特性差异、API差异等等问题,带你提前避坑,轻松驾驭使用Uniapp解决环境兼容问题!
你可能会想:“UniApp不是一套代码多端运行吗?为啥还要关心这个?”但等你真的开始上线,就会遇到:
-
在H5跑得好好的定位,到微信小程序里没反应!
-
iOS上丝般顺滑的滚动,到安卓卡成PPT!
-
微信小程序能用的API,到公众号里直接报错!
-
安卓打包没问题,iOS一运行就白屏!
-
鸿蒙手机上,页面布局全乱套!
别慌!今天我就带你系统梳理一遍——H5、安卓APP、iOS APP、微信小程序、微信公众号、鸿蒙APP,这六大平台的差异点、坑点、解决方案,一次性讲透。以后遇到跨端问题,你就能像老中医一样,“望闻问切”对症下药。
一、先搞明白:UniApp是怎么“跨端”的?
在讲差异之前,咱得先理解UniApp的跨端原理。这样才能明白,为什么会有差异。
UniApp = 编译器 + 运行时
-
编译器:把你的
.vue文件,根据不同平台“翻译”成对应平台的代码。比如编译到微信小程序,就生成wxml/wxss/js;编译到H5,就生成HTML/CSS/JS。 -
运行时:在每个平台都有一个“壳”,负责解析你的代码,调用平台原生能力。
关键点来了:UniApp虽然封装了很多跨端API,但底层调用的还是各平台的原生能力。这就意味着:
-
同一套API,在不同平台上的实现方式可能不同
-
各平台的能力边界不同(小程序不能操作DOM,APP可以)
-
各平台的UI规范不同(iOS和安卓的导航栏高度就不一样)
理解了这一点,咱们再来看差异,就不会觉得奇怪了。
二、六大平台差异全景图
咱们先画个图,把这六兄弟的“性格特点”摸清楚:
| 平台 | 运行环境 | 核心限制 | 特有优势 | 常见坑点 |
|---|---|---|---|---|
| H5 | 浏览器 | 浏览器沙箱 | 可以直接操作DOM,URL直接访问 | 兼容各种浏览器内核 |
| 安卓APP | Android系统 | 需要各种权限申请 | 原生能力最强,可调用所有硬件 | 机型碎片化严重 |
| iOS APP | iOS系统 | 苹果审核严格 | 性能优化好,用户付费意愿高 | 隐私权限要求苛刻 |
| 微信小程序 | 微信环境 | 包大小限制2MB | 微信生态,分享方便 | 很多Web API不能用 |
| 微信公众号 | 微信内置浏览器 | 基于WebView | 可复用H5代码 | 跳转授权逻辑特殊 |
| 鸿蒙APP | HarmonyOS | 新系统,API可能变化 | 万物互联 | 部分API兼容性待验证 |
三、UI差异:别让布局在不同手机上“变形”
1️⃣ 导航栏高度(最坑爹的差异)
这是新人最容易踩的坑——写死了导航栏高度。
/* 错误写法 */.nav-bar {height: 44px; /* iOS上合适,安卓上可能偏小 */}
真实差异:
-
iOS:状态栏20pt(非全面屏)或44pt(全面屏),导航栏44pt
-
安卓:状态栏24-30dp,导航栏48dp
-
微信小程序:胶囊菜单高度32px,右侧边距也有差异
✅ 解决方案:动态获取
// 获取状态栏高度constsystemInfo=uni.getSystemInfoSync()conststatusBarHeight=systemInfo.statusBarHeight// 状态栏高度// 获取胶囊信息(仅微信小程序)letmenuButtonInfo= {}// #ifdef MP-WEIXINmenuButtonInfo=uni.getMenuButtonBoundingClientRect()// #endif// 计算导航栏高度letnavHeight// #ifdef H5navHeight=44// H5固定,或者从CSS变量获取// #endif// #ifdef APP-PLUSnavHeight=systemInfo.platform==='ios'?44 : 48// #endif// #ifdef MP-WEIXINnavHeight= (menuButtonInfo.top-statusBarHeight) *2+menuButtonInfo.height// #endif
2️⃣ 底部安全区域(全面屏适配)
iPhone X以后的全面屏,底部有个“小黑条”,安卓也有虚拟导航栏。
✅ 解决方案:使用env()和constant()
.safe-bottom {/* 兼容iOS 11.2+ */padding-bottom: constant(safe-area-inset-bottom);/* 兼容iOS 11.2+ */padding-bottom: env(safe-area-inset-bottom);}/* 或者用条件编译动态设置 */// #ifdefAPP-IOS || APP-ANDROIDconstsafeArea = uni.getSystemInfoSync().safeArea// 计算底部安全距离// #endif
3️⃣ 字体单位:rpx vs px
UniApp提供了rpx(响应式px),但要注意:
-
小程序和APP:
rpx完美适配,750rpx就是屏幕宽度 -
H5:
rpx也能用,但底层会转换为vw,某些复杂布局可能有误差 -
公众号:同H5
✅ 最佳实践:
-
一般使用
rpx,简单省事 -
对于需要精确1px边框的场景,用
1px配合transform: scale(0.5) -
对于字体大小,建议使用
px,因为大多数设计稿字体就是固定的
四、API差异:同一段代码,不同命运
1️⃣ 登录授权:三大流派
| 平台 | 登录方式 | 获取用户信息 | 注意事项 |
|---|---|---|---|
| H5 | 短信/密码登录 | 表单提交 | 不能静默获取手机号 |
| APP | uni.login + 后端 |
uni.getUserProfile(需弹窗) |
iOS需要配置签名,其实也可以跟H5端一样通过账号登录 |
| 微信小程序 | uni.login拿code |
uni.getUserProfile(按钮触发) |
不能直接弹窗,必须用户点击 |
| 微信公众号 | OAuth2.0跳转 | 静默授权只能拿openid | 需配置网页授权域名 |
示例:微信小程序登录(与APP不同!)
// 微信小程序:必须先通过按钮触发<buttonopen-type="getUserProfile"@click="getUserProfile">获取头像昵称</button>// 然后在方法里getUserProfile() {uni.getUserProfile({desc: '用于完善会员资料',success: (res) => {// 这里拿到用户信息,但openid还得uni.login拿 } })}// APP登录:可以直接调// #ifdef APP-PLUSuni.login({provider: 'apple', // 或 'weixin'success: (loginRes) => {// 直接拿到授权信息 }})// #endif
2️⃣ 定位API:权限要求不同
// 看似相同的代码,在不同平台行为不同uni.getLocation({type: 'wgs84',success: (res) => {console.log('经度:'+res.longitude) }})
差异点:
-
H5:浏览器会弹权限框,需要https环境,部分浏览器不支持
-
APP:需要在manifest配置定位权限,安卓6.0+需要动态申请
-
微信小程序:需在
pages.json配置permission字段,用户首次使用会弹窗 -
公众号:同H5,但微信内置浏览器有特殊定位API(
wx.getLocation)
✅ 安全写法:
asyncfunctionsafeGetLocation() {// 先判断平台能力if (!uni.canIUse('getLocation')) {uni.showToast({ title: '当前环境不支持定位', icon: 'none' })return }// #ifdef APP-PLUS// APP需要先申请权限constpermResult=awaitrequestLocationPermission()if (!permResult) return// #endif// #ifdef MP-WEIXIN// 小程序需要先检查是否已授权constauth=awaitcheckLocationAuth()if (!auth) {uni.openSetting() // 打开设置页return }// #endif// 调用定位uni.getLocation({success: (res) => {},fail: (err) => {// #ifdef H5// H5降级方案:用第三方地图API// #endif } })}
3️⃣ 存储API:容量限制
| 平台 | 单个key上限 | 总上限 | 同步API |
|---|---|---|---|
| H5 | 取决于浏览器 | 5-10MB | 支持 |
| APP | 不受限 | 不受限 | 支持 |
| 微信小程序 | 1MB | 10MB | 支持,但超限会报错 |
微信小程序特殊坑 :
// 在微信小程序中,这样写可能会崩uni.setStorageSync('bigData', largeObject) // 超过1MB直接报错// ✅ 解决方案:分片存储functionsaveLargeData(key, data) {conststr=JSON.stringify(data)constMAX_SIZE=900*1024// 留100KB缓冲if (str.length<MAX_SIZE) {uni.setStorageSync(key, str) } else {// 分片存储逻辑constchunks= []for (leti=0; i<str.length; i+=MAX_SIZE) {chunks.push(str.substr(i, MAX_SIZE)) }// 存储分片... }}
4️⃣ 支付:完全不同
-
H5:只能调起支付宝/微信的网页支付,体验差
-
APP:可调起微信/支付宝APP支付,需配置universal link(iOS)或应用签名(安卓)
-
微信小程序:
uni.requestPayment调起微信支付,最简单 -
公众号:JSAPI支付,需要用户的openid
其实推荐使用开源的聚合支付服务PddPay,支持私有化部署、Web支付、H5支付、微信公众号支付、小程序支付、APP支付都支持、支付宝/微信支付都支持,web支付和H5支付可以自动检测用户访问环境,比如在支付宝中打开、微信中打开、PC web页面打开、手机浏览器打开等等的页面风格和支付方式会自动最优匹配,简化用户操作,提高支付成功率,支付测试地址: https://pddon.cn/payment-demo/index.html
✅ 最佳实践:支付逻辑全部交给后端,前端只负责调起
// 后端返回调起支付的参数constpayParams=awaitrequestPay(orderId)// #ifdef MP-WEIXINuni.requestPayment({timeStamp: payParams.timeStamp,nonceStr: payParams.nonceStr,package: payParams.package,signType: 'MD5',paySign: payParams.paySign,success: () => {}})// #endif// #ifdef APP-PLUSuni.requestPayment({provider: 'wxpay', // 或 'alipay'orderInfo: payParams, // 不同支付平台格式不同success: () => {}})// #endif// #ifdef H5// 跳转到支付链接window.location.href=payParams.payUrl// #endif
五、生命周期差异:有的执行,有的不执行
有个学员曾经崩溃地问我:“前辈,我的onLoad在iOS上好好的,到安卓就不执行了!”
真相是:不同平台的生命周期触发时机有细微差异。
常见差异点:
-
onLoad传参:小程序和H5没问题,但APP端如果从推送通知打开,参数可能不一样 -
onShow:小程序切后台再回来会触发,H5从其他tab返回也会触发,但APP端行为可能不同 -
onReady获取DOM:在小程序里,onReady里用uni.createSelectorQuery是安全的,但在H5里可能DOM还没完全渲染
✅ 解决方案:延迟执行或重试机制
onReady() {// #ifdef MP-WEIXINthis.getDomInfo()// #endif// #ifdef H5this.$nextTick(() => {this.getDomInfo() })// #endif}// 更稳妥的方案:重试几次functionqueryWithRetry(selector, maxRetry=3) {returnnewPromise((resolve) => {letretry=0constquery= () => {constview=uni.createSelectorQuery().select(selector)view.boundingClientRect(data=> {if (data) {resolve(data) } elseif (retry<maxRetry) {retry++setTimeout(query, 100*retry) // 逐渐增加延迟 } else {resolve(null) } }).exec() }query() })}
六、配置差异:manifest.json里的秘密
1️⃣ 微信小程序特有配置
// manifest.json{"mp-weixin": {"appid": "wx1234567890","setting": {"urlCheck": true, // 开发时关闭域名校验"es6": true,"minified": true },"permission": {"scope.userLocation": {"desc": "你的位置信息将用于查找附近门店" } },"requiredPrivateInfos": ["getLocation"] // 声明需要获取位置 }}
2️⃣ APP特有配置
{"app-plus": {"distribute": {"android": {"permissions": ["<uses-permission android:name=\"android.permission.CAMERA\"/>","<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>" ] },"ios": {"plistcmds": ["Add :NSLocationWhenInUseUsageDescription string '需要您的位置信息来查找附近门店'" ] } } }}
注意:iOS的隐私描述必须写清楚,否则审核会被拒!
七、实战:条件编译——处理差异的终极武器
说了这么多差异,那怎么在代码里优雅地处理呢?答案是:条件编译
1️⃣ 什么是条件编译?
就是用特殊的注释,告诉编译器:这段代码只编译到特定平台。
// #ifdef H5console.log('这段代码只在H5平台出现')// #endif// #ifndef MP-WEIXINconsole.log('这段代码不在微信小程序平台出现')// #endif// #ifdef APP-PLUS || MP-WEIXINconsole.log('在APP或微信小程序平台出现')// #endif
2️⃣ 支持的文件类型
-
.vue/.nvue/.uvue -
.js/.uts -
.css -
pages.json -
各种预编译语言(
.scss、.less、.ts等)
3️⃣ 平台取值一览表
| 平台 | 取值 | 说明 |
|---|---|---|
| APP | APP-PLUS 或 APP | 所有APP(含安卓/iOS) |
| APP-安卓 | APP-ANDROID | 仅安卓APP |
| APP-iOS | APP-IOS | 仅iOS APP |
| APP-鸿蒙 | APP-HARMONY | 仅鸿蒙APP |
| H5 | H5 或 WEB | H5/Web |
| 微信小程序 | MP-WEIXIN | 微信小程序 |
| 支付宝小程序 | MP-ALIPAY | 支付宝小程序 |
| 百度小程序 | MP-BAIDU | 百度小程序 |
| 抖音小程序 | MP-TOUTIAO | 抖音小程序 |
| QQ小程序 | MP-QQ | QQ小程序 |
| 公众号 | H5 | 公众号本质是H5,需单独处理 |
4️⃣ 实际应用场景
场景1:不同平台的跳转逻辑
functionhandleLogin() {// #ifdef H5window.location.href='/login.html'// #endif// #ifdef MP-WEIXINuni.navigateTo({ url: '/pages/login/login' })// #endif// #ifdef APP-PLUS// APP可能用原生登录页plus.runtime.launchApplication({pname: 'com.example.login' })// #endif}
场景2:样式差异
/* #ifdef H5 */.box {height: 100vh; /* H5用视口高度 */}/* #endif *//* #ifdef APP-PLUS */.box {height: 100%; /* APP用百分比 */}/* #endif */
场景3:pages.json配置差异
{"pages": [ {"path": "pages/index/index","style": {"navigationBarTitleText": "首页" } } ],// #ifdef MP-WEIXIN"usingComponents": {"custom": "/components/custom" },"permission": {"scope.userLocation": {"desc": "你的位置信息将用于小程序" } }// #endif}
5️⃣ 条件编译的注意事项
-
嵌套深度限制:微信开发者工具对条件编译嵌套层数限制为5层,超过会导致编译异常
-
语法正确性:无论条件编译是否生效,代码本身必须语法正确(JSON不能有多余逗号)
-
静态资源路径:微信小程序要求相对路径,H5可以用绝对路径,建议统一用相对路径或别名
八、常见错误及解决方案(血泪经验)
💥 错误1:在微信小程序里用了window对象
现象:代码中有window.location或document,小程序编译报错。原因:小程序的逻辑层不在浏览器环境,没有DOM/BOM。✅ 解决方案:用条件编译包裹,或用uni API替代。
// 错误constwidth=window.innerWidth// 正确// #ifdef H5constwidth=window.innerWidth// #endif// #ifndef H5constwidth=uni.getSystemInfoSync().windowWidth// #endif
💥 错误2:在APP里用了localStorage,退出登录后数据还在
现象:APP用户退出登录,用uni.setStorageSync存的用户数据还在。原因:APP的存储是持久化的,需要手动清除。✅ 解决方案:封装存储方法,区分登录态。
// storage.jsexportconststorage= {set(key, value, isLoginData=false) {if (isLoginData) {// 登录态数据存到独立命名空间constuserId=store.getters.userIduni.setStorageSync(`user_${userId}_${key}`, value) } else {uni.setStorageSync(key, value) } },logout() {// 退出登录时清除所有登录态数据constuserId=store.getters.userId// 遍历清除... }}
💥 错误3:iOS和安卓的日期解析差异
现象:new Date('2024-05-06 12:00:00')在安卓上正常,在iOS上返回Invalid Date。原因:iOS不支持中划线格式的日期字符串,需要替换为斜杠。✅ 解决方案:统一格式化。
functionsafeParseDate(dateStr) {// iOS只支持 yyyy/mm/dd 格式returnnewDate(dateStr.replace(/-/g, '/'))}
💥 错误4:H5和微信小程序的页面跳转混用
现象:在H5里用uni.navigateTo跳转正常,到小程序里点了没反应。原因:小程序页面栈最多10层,超过后跳转会失败。✅ 解决方案:封装跳转方法,自动降级。
functionsafeNavigateTo(url) {// #ifdef MP-WEIXINconstpages=getCurrentPages()if (pages.length>=10) {uni.redirectTo({ url }) // 改用替换return }// #endifuni.navigateTo({ url })}
💥 错误5:鸿蒙手机上安全区域获取不到
现象:uni.getSystemInfoSync().safeArea返回undefined。原因:部分鸿蒙版本API兼容性问题。✅ 解决方案:降级处理。
constsystemInfo=uni.getSystemInfoSync()constsafeArea=systemInfo.safeArea|| {// 降级方案:使用屏幕尺寸减去默认值bottom: systemInfo.screenHeight-50,top: systemInfo.statusBarHeight||30}
九、完整示例:一个兼容6大平台的定位组件
最后,我写一个完整的定位组件,把所有技巧都用上:
<!-- components/LocationPicker.vue --><template><viewclass="location-picker"@click="chooseLocation"><viewclass="location-icon i-carbon:location"></view><textclass="location-text">{{ address || '点击选择位置' }}</text><viewclass="arrow-icon i-carbon:chevron-right"></view></view></template><scriptsetup>import { ref } from'vue'constaddress=ref('')constlatitude=ref(0)constlongitude=ref(0)// 选择位置constchooseLocation=async () => {// 先检查平台能力if (!checkLocationSupport()) {uni.showToast({ title: '当前环境不支持定位', icon: 'none' })return }// #ifdef H5 || MP-WEIXINawaitrequestLocationAuth()// #endif// #ifdef APP-PLUSawaitrequestAppPermission()// #endif// 调用定位uni.chooseLocation({success: (res) => {address.value=res.namelatitude.value=res.latitudelongitude.value=res.longitude// 根据平台不同,可能需要额外处理// #ifdef MP-WEIXIN// 微信小程序返回的地址可能不完整,可以再调用逆地理编码getAddressByLocation(res.latitude, res.longitude)// #endif },fail: (err) => {// #ifdef H5// H5降级方案:使用第三方地图APIuseThirdPartyMap()// #endif// #ifdef MP-WEIXINif (err.errMsg.includes('auth deny')) {uni.showModal({title: '提示',content: '需要授权位置信息',success: (res) => {if (res.confirm) {uni.openSetting() // 打开设置页 } } }) }// #endif } })}// 检查平台是否支持定位constcheckLocationSupport= () => {// #ifdef H5return'geolocation'innavigator// #endif// #ifdef MP-WEIXINreturnuni.canIUse('chooseLocation')// #endif// #ifdef APP-PLUSreturntrue// APP基本都支持// #endif// 默认返回falsereturnfalse}// 微信小程序检查授权constrequestLocationAuth= () => {returnnewPromise((resolve) => {// #ifdef MP-WEIXINuni.getSetting({success: (res) => {if (!res.authSetting['scope.userLocation']) {uni.authorize({scope: 'scope.userLocation',success: () =>resolve(true),fail: () => {uni.showModal({title: '提示',content: '需要授权位置信息',success: (modalRes) => {if (modalRes.confirm) {uni.openSetting({success: (settingRes) => {resolve(settingRes.authSetting['scope.userLocation']) } }) } else {resolve(false) } } }) } }) } else {resolve(true) } } })// #endif// #ifndef MP-WEIXINresolve(true)// #endif })}// APP权限申请constrequestAppPermission= () => {returnnewPromise((resolve) => {// #ifdef APP-PLUSplus.android?.requestPermissions?.(['android.permission.ACCESS_FINE_LOCATION'], (e) => {resolve(e.deniedAlways?.length===0) })// #endif// #ifndef APP-PLUSresolve(true)// #endif })}// 逆地理编码(仅示例,实际需调用地图API)constgetAddressByLocation= (lat, lng) => {// 调用高德或腾讯地图API}// H5降级方案constuseThirdPartyMap= () => {// #ifdef H5window.open('https://map.baidu.com/mobile/webapp/index/index/')// #endif}</script><stylescoped>.location-picker {display: flex;align-items: center;padding: 24rpx30rpx;background-color: #fff;border-radius: 12rpx;border: 1rpxsolid#e5e5e5;}.location-icon {font-size: 32rpx;color: #007aff;margin-right: 16rpx;}.location-text {flex: 1;font-size: 28rpx;color: #333;}.arrow-icon {font-size: 28rpx;color: #999;}/* 不同平台微调 *//* #ifdef APP-PLUS */.location-picker {border-width: 1px; /* APP上1rpx可能太细 */}/* #endif *//* #ifdef H5 */.location-picker {cursor: pointer;}/* #endif */</style>
十、最后的叮嘱
小老弟,跨端开发的本质是在“一套代码”和“平台特性”之间找平衡。我的建议是:
-
能用uni API的,尽量用uni API——框架帮你处理了大部分差异
-
遇到平台特有需求,用条件编译隔离——不要试图写兼容所有平台的“万能代码”
-
提前规划好测试方案——至少要在iOS、安卓、微信小程序、H5上跑一遍
-
关注官方更新——各平台每年都会出新版本,API可能变化
最后送你一句口诀:UI用rpx,逻辑用条件,权限动态要,存储分大小,定位兜底保,测试少不了。
好了,去把你的项目跑一遍吧,看看哪些地方还能优化。遇到奇怪的问题,欢迎随时带着报错来找我。
—— 一个在跨端坑里爬出来、现在能躺着过的老鸟 🕊️
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!


往期相关文章推荐
夜雨聆风
