天天吹“一套代码,多端运行”,但是真当App、小程序、H5一起上的时候,你就知道不是所有组件都能“一套通吃”。比如支付、分享、推送……每个平台都有自己的玩法。前端代码写着写着就开始到处堆if (process.env.UNI_PLATFORM)了,又丑又乱。
上个月帮朋友搞了一个商业项目,用uni-app同时发微信小程序、支付宝小程序、还有H5。其中登录页设计稿要求:小程序上要有个一键获取微信手机号按钮,H5上要露出账号密码输入框,App端又要跳转第三方登录SDK。这就是典型的多端差异需求。在uni-app里,最标准的解决办法就是“条件编译”。
这篇文章用这个登录页案例,把条件编译的几种用法理一遍,顺便讲一下怎么组织代码才不显得乱。
条件编译到底是什么鬼
uni-app的条件编译有点像C语言的#ifdef指令。你在注释里添加一些特殊的标识,当项目编译到某个平台时,对应的代码块才会被保留。其他平台的代码在打包时直接扔掉,连打包进去都不会。这样就能针对不同平台写不同实现,同时又保证工程文件只有一套。
条件编译有两种写法:一种是用注释包住代码块,另一种是用条件表达式包裹具体的JS逻辑。实际开发中两种都会用到。
注释写法:适合模板和样式
<!-- #ifdef MP-WEIXIN -->
<button open-type="getPhoneNumber">微信手机号一键登录</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<input type="text" placeholder="用户名">
<input type="password" placeholder="密码">
<!-- #endif -->
这样在微信小程序里,H5的输入框代码根本不会出现;在H5里,那微信的按钮也不会渲染。
JS表达式写法:适合逻辑
let provider = 'none'
// #ifdef MP-WEIXIN
provider = 'weixin'
// #endif
// #ifdef H5
provider = 'password'
// #endif
这段代码编译到微信小程序时,只保留provider = 'weixin'这一句,H5那句被删掉。
登录页需求具体拆解
我们做一个login.vue页面,功能要求是这样的:
- 微信小程序:显示微信一键登录按钮,同时保留一个“手机号登录”的小链接用于手动输入。
- H5:显示账号、密码输入框,外加一个“验证码登录”的tab。
- App:显示第三方登录图标(微信、QQ、苹果),点击后调用uni.login。
同时,页面底部有一个服务协议勾选,这个三种端都有,不需要差异。
页面结构怎么写才能不乱
不要在每个组件下面写一堆条件编译,容易看花眼。推荐在template里把不同平台的内容拆成三个区域,用大块条件编译包住。
<template>
<view class="login-container">
<!-- 微信小程序登录区 -->
<!-- #ifdef MP-WEIXIN -->
<view class="login-section weixin-login">
<button class="login-btn" open-type="getPhoneNumber" @getphonenumber="handlePhoneLogin">微信手机号一键登录</button>
<view class="toggle-type" @click="switchToPhone">使用手机号密码登录</view>
</view>
<!-- #endif -->
<!-- H5登录区 -->
<!-- #ifdef H5 -->
<view class="login-section h5-login">
<view class="tabs">
<view class="tab active">密码登录</view>
<view class="tab">验证码登录</view>
</view>
<input v-model="username" type="text" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button class="login-btn" @click="handlePwdLogin">登录</button>
</view>
<!-- #endif -->
<!-- App端登录区 -->
<!-- #ifdef APP-PLUS -->
<view class="login-section app-login">
<view class="third-party-icons">
<view class="icon wechat" @click="loginWith('weixin')">微信登录</view>
<view class="icon qq" @click="loginWith('qq')">QQ登录</view>
<view class="icon apple" @click="loginWith('apple')">Apple登录</view>
</view>
</view>
<!-- #endif -->
</view>
</template>
每个平台区域的代码都比较独立,改起来也不会牵扯到别的地方。这里用了#ifdef表示“如果定义了某平台”,也可以用#ifndef表示“如果没有定义某平台”。
JS逻辑里的条件编译
登录方法必然也得有平台差异。你在methods里写一个通用的handleLogin,然后在函数内部用条件编译调不同实现。
methods: {
// 通用登录方法,主要处理协议和参数校验
handleLogin(type) {
// #ifdef H5
this.handleH5Login()
// #endif
// #ifdef MP-WEIXIN
this.handleWeixinLogin()
// #endif
// #ifdef APP-PLUS
this.handleAppLogin(type)
// #endif
},
// #ifdef H5
handleH5Login() {
console.log('H5登录,走密码登录')
// 调用后端登录接口
},
// #endif
// #ifdef MP-WEIXIN
handleWeixinLogin() {
console.log('微信小程序登录,走微信授权')
},
// #endif
// #ifdef APP-PLUS
handleAppLogin(provider) {
uni.login({
provider: provider,
success: (res) => {
console.log('App登录成功', res)
}
})
}
// #endif
}
注意这些平台专属的方法,一定要用条件编译包住,不然在别的平台编译时,可能会引入不存在的API(比如H5没有uni.login的微信provider),导致报错。条件编译会在打包时直接剔除不需要的代码,所以你就大胆写。
样式差异:用CSS条件编译挺好
很多时候不同平台的布局间距也不一样。比如小程序里按钮要加圆角,H5里用方角。除了用rpx之类的通用单位外,还可以在<style lang="scss">里用条件编译处理特殊的样式。
<style lang="scss">
.login-btn {
width: 80%;
height: 44px;
line-height: 44px;
border-radius: 4px;
font-size: 16px;
}
/* #ifdef MP-WEIXIN */
.login-btn {
border-radius: 8px;
font-weight: 500;
}
/* #endif */
/* #ifdef H5 */
.login-btn {
border-radius: 0;
background-color: #ff6600;
}
/* #endif */
</style>
这样不同端最终编译出来的样式就是各端自己那套,不会互相覆盖。有些同学喜欢用uni.getSystemInfoSync().platform来动态判断,然后再绑class,其实完全没必要,既增加运行时代码体积,又可能触发渲染闪烁。条件编译是在编译期就解决掉的。
页面生命周期里也要注意
比如在onLoad里,要根据平台初始化不同的东西。App端可能要调用SDK注册,小程序端要设置分享参数,H5什么都不用干。同样可以用条件编译。
onLoad() {
// #ifdef APP-PLUS
this.initAppSDK()
// #endif
// #ifdef MP-WEIXIN
uni.showShareMenu({
menus: ['shareAppMessage', 'shareTimeline']
})
// #endif
}
这样省得写一堆if (platform === 'app')之类的判断,因为打包后那些分支代码根本不会存在,逻辑更干净。
踩坑记录
1. 条件编译注释不能乱嵌套
你可以在一块区域里写多个#ifdef,但不能在#ifdef里边再嵌套一个#ifdef,容易导致编译异常。我的原则是一个块只处理一个平台差异,如果多个平台有交集,就单独抽到公共区域。
2. 不是所有地方都支持条件编译
比如manifest.json和pages.json里的配置不支持条件编译。如果你某个页面只在微信小程序里用,那你最好写成单独一个页面,然后在pages.json里用条件编译配置?实际上pages.json也支持条件编译,但是官方文档说只在部分字段生效,建议还是单独页面。但一般登录页都是每个端都有的,只是内容不同,所以用模板条件编译就够了。
3. 别忘记#endif
少写一个endif就会导致编译错乱,而且报错信息可能会找不到实际位置,非常坑。每次写完一段条件编译,马上检查是否闭合。
4. 插件和原生SDK还是分开
条件编译只能处理前端代码,如果涉及原生插件的依赖,比如App端的某个支付SDK,最好还是在特定平台目录下放原生代码,不要指望纯条件编译能解决原生SDK的集成。条件编译只针对js、template、css,原生配置还是得走各自平台的项目目录。
我觉得这样组织最舒服
弄完这个登录页之后,我在整个项目里定了条规矩:凡是跨端的差异,统一放在独立的目录或文件里,通过条件编译去拼接。比如新建一个apis/platform.js,专门用来导出不同端的方法。
// apis/platform.js
// #ifdef H5
export const loginByPassword = () => {
// H5密码登录逻辑
}
// #endif
// #ifdef MP-WEIXIN
export const loginByWeixin = () => {
// 微信授权登录
}
// #endif
然后在页面里直接import并调用方法,页面里的条件编译就少了很多,看起来更清爽。这个做法比在页面里到处堆条件编译更好维护。
总结
条件编译是uni-app多端开发的一等公民,它的优势在于编译期直接裁剪,不会带来运行时性能损耗。如果你还在运行环境里用uni.getSystemInfoSync().uniPlatform去做一大堆if else,我建议你早点改成条件编译。代码会更短,逻辑更清晰,而且不容易出现“一个端改了,另一个端挂掉”的尴尬。
这次登录页案例只是个开始,后面还有支付、分享、地图、推送……你会越来越觉得,条件编译用得好,实在能省不少心。
以上就是本次实战的全部内容,如果有细节想讨论的,可以留言。

