当你第一次在UniAPP项目中尝试为不同平台编写差异化代码时,很可能会遇到这样的困境:同一个.vue文件里塞满了#ifdef和#endif条件编译指令,代码可读性直线下降,维护成本却成倍增加。这种情况在需要为微信小程序、H5和App分别实现不同登录流程或支付接口时尤为明显。
跨平台开发的核心价值在于"一次开发,多端运行",但现实情况是,不同平台间存在无法避免的差异。这些差异主要体现在三个方面:
wx.login(),而H5可能需要对接第三方OAuth服务传统做法是在代码中大量使用条件编译:
javascript复制// 不推荐的写法:代码中混杂过多条件编译
onLoad() {
#ifdef MP-WEIXIN
wx.login({
success(res) {
console.log(res.code)
}
})
#endif
#ifdef H5
this.$router.beforeEach((to, from, next) => {
// H5特有的路由逻辑
})
#endif
}
这种写法虽然能解决问题,但随着业务复杂度的提升,代码会变得难以维护。更优雅的解决方案是利用platforms目录进行物理层面的代码分离。
UniAPP的platforms目录允许开发者针对不同平台创建专属的代码文件,构建时会自动合并到最终产物中。目录结构示例如下:
code复制├─platforms
│ ├─mp-weixin
│ │ └─pages
│ │ └─login
│ │ └─login.vue # 微信小程序专属登录页
│ ├─h5
│ │ └─pages
│ │ └─login
│ │ └─login.vue # H5专属登录页
│ └─app-plus
│ └─pages
│ └─login
│ └─login.vue # App专属登录页
当UniAPP编译器遇到platforms目录时,会按照以下规则处理:
platforms/[平台代号]下的文件这种机制带来几个显著优势:
假设我们需要为三个平台实现不同的登录流程:
微信小程序登录页 (platforms/mp-weixin/pages/login/login.vue)
javascript复制export default {
methods: {
handleLogin() {
wx.login({
success(res) {
// 获取微信code后调用后端接口
}
})
}
}
}
H5登录页 (platforms/h5/pages/login/login.vue)
javascript复制export default {
methods: {
handleLogin() {
// 跳转到第三方OAuth页面
window.location.href = 'https://oauth.example.com'
}
}
}
App登录页 (platforms/app-plus/pages/login/login.vue)
javascript复制export default {
methods: {
handleLogin() {
// 调用原生SDK登录
uni.login({
provider: 'weixin',
success(res) {
console.log(res.authResult)
}
})
}
}
}
虽然platforms目录能解决大部分平台差异问题,但有时我们仍需要在同一文件中处理一些小差异。这时可以结合条件编译实现更灵活的适配。
遵循以下原则能保持代码整洁:
platforms目录分离假设我们需要在不同平台显示略有差异的导航栏:
vue复制<template>
<view>
<!-- 共用部分 -->
<header class="common-header">
<!-- 平台差异部分 -->
#ifdef MP-WEIXIN
<button @click="share">分享</button>
#endif
#ifdef H5
<a href="/help">帮助中心</a>
#endif
</header>
<!-- 其余共用内容 -->
</view>
</template>
<script>
// 共用逻辑
export default {
methods: {
// 共用方法
}
}
</script>
<style>
/* 共用样式 */
.common-header {
display: flex;
justify-content: space-between;
}
</style>
对于需要在不同平台使用完全不同实现的组件,可以这样组织:
code复制├─components
│ └─AuthButton
│ ├─index.vue # 基础实现
│ ├─mp-weixin.vue # 微信小程序实现
│ └─h5.vue # H5实现
然后在index.vue中动态加载对应平台组件:
javascript复制export default {
computed: {
platformComponent() {
// 根据平台返回对应组件
return () => import(`./${process.env.VUE_APP_PLATFORM}.vue`)
}
},
render(h) {
return h(this.platformComponent)
}
}
在vue.config.js中可以为不同平台配置不同的编译选项:
javascript复制module.exports = {
configureWebpack: {
// 平台特定配置
...(process.env.UNI_PLATFORM === 'h5' ? {
// H5特有配置
} : {}),
...(process.env.UNI_PLATFORM === 'mp-weixin' ? {
// 微信小程序特有配置
} : {})
}
}
对于需要适配不同平台的静态资源,可以这样组织:
code复制├─static
│ ├─common # 共用资源
│ ├─mp-weixin # 微信小程序专属资源
│ └─h5 # H5专属资源
然后在代码中动态引用:
javascript复制const platform = process.env.UNI_PLATFORM
const imagePath = `/static/${platform}/logo.png` || '/static/common/logo.png'
调试platforms目录下的代码需要特别注意:
uni-app命令时指定平台,如npm run dev:mp-weixinjavascript复制console.log(`[${process.env.UNI_PLATFORM}] 当前平台日志`)
当多个平台的代码有部分相似时,可以考虑以下复用策略:
| 复用方式 | 适用场景 | 示例 |
|---|---|---|
| 混入(mixin) | 多个组件共享相同逻辑 | 登录状态检查逻辑 |
| 高阶组件 | 需要包装组件添加通用功能 | 添加平台特定生命周期 |
| 工具函数 | 纯逻辑复用 | 平台特定的URL处理函数 |
| 组合式API | Vue3项目中逻辑组合 | 复用平台特定的响应式数据逻辑 |
由于platforms目录包含大量平台专属代码,合理的.gitignore配置很重要:
code复制# 忽略所有平台代码
platforms/*
# 但保留目录结构
!platforms/.gitkeep
# 按需添加需要跟踪的平台
!platforms/mp-weixin/
!platforms/h5/
经过多个UniAPP项目的实践,我总结出以下目录结构建议:
code复制├─src
│ ├─api
│ │ ├─mp-weixin # 微信小程序API
│ │ └─h5 # H5 API
│ ├─assets
│ │ ├─mp-weixin # 平台专属资源
│ │ └─h5
│ ├─components
│ │ ├─common # 共用组件
│ │ ├─mp-weixin # 平台专属组件
│ │ └─h5
│ ├─composables # Vue3组合式函数
│ ├─pages # 主页面目录
│ ├─platforms # 平台专属页面
│ │ ├─mp-weixin
│ │ └─h5
│ ├─stores # 状态管理
│ └─utils # 工具函数
│ ├─mp-weixin
│ └─h5
这种结构的特点是:
让我们通过一个完整的登录模块示例,展示如何应用上述所有技巧:
code复制├─src
│ ├─api
│ │ ├─auth
│ │ │ ├─mp-weixin.js # 微信登录API
│ │ │ └─h5.js # H5登录API
│ ├─components
│ │ └─AuthButton
│ │ ├─index.vue # 基础样式和逻辑
│ │ ├─mp-weixin.vue # 微信特有逻辑
│ │ └─h5.vue # H5特有逻辑
│ ├─pages
│ │ └─login.vue # 基础登录页
│ └─platforms
│ ├─mp-weixin
│ │ └─pages
│ │ └─login.vue # 微信增强登录页
│ └─h5
│ └─pages
│ └─login.vue # H5增强登录页
共用登录组件 (src/components/AuthButton/index.vue)
vue复制<template>
<button :class="['auth-btn', platformClass]" @click="handleClick">
<slot></slot>
</button>
</template>
<script>
export default {
computed: {
platformClass() {
return `auth-btn-${process.env.UNI_PLATFORM}`
}
},
methods: {
handleClick() {
this.$emit('click')
}
}
}
</script>
<style>
.auth-btn {
/* 基础样式 */
}
.auth-btn-mp-weixin {
/* 微信特有样式 */
}
.auth-btn-h5 {
/* H5特有样式 */
}
</style>
微信登录API (src/api/auth/mp-weixin.js)
javascript复制export const login = () => {
return new Promise((resolve, reject) => {
wx.login({
success(res) {
if (res.code) {
resolve(res.code)
} else {
reject(new Error('微信登录失败'))
}
},
fail: reject
})
})
}
H5登录页增强 (src/platforms/h5/pages/login.vue)
vue复制<script>
import { login } from '@/api/auth/h5'
export default {
methods: {
async handleLogin() {
try {
const token = await login()
this.$store.commit('setToken', token)
uni.redirectTo({ url: '/home' })
} catch (error) {
uni.showToast({ title: error.message, icon: 'none' })
}
}
}
}
</script>
在多平台开发中,性能优化需要针对不同平台特性进行:
javascript复制// 动态加载平台专属组件
const PlatformComponent = () => import(`@/components/PlatformSpecific/${process.env.UNI_PLATFORM}`)
javascript复制// vite.config.js
export default {
build: {
rollupOptions: {
output: {
chunkFileNames: `assets/[name]-${process.env.UNI_PLATFORM}.[hash].js`
}
}
}
}
javascript复制// 不好的写法:每次渲染都判断
computed: {
isWeixin() {
return process.env.UNI_PLATFORM === 'mp-weixin'
}
}
// 好的写法:只判断一次
created() {
this.isWeixin = process.env.UNI_PLATFORM === 'mp-weixin'
}
javascript复制// 微信小程序需要更严格的节流
const throttleDelay = process.env.UNI_PLATFORM === 'mp-weixin' ? 1000 : 500
javascript复制// vue.config.js
const platform = process.env.UNI_PLATFORM
module.exports = {
configureWebpack: {
cache: {
type: 'filesystem',
name: `${platform}-cache`
}
}
}
json复制// package.json
{
"scripts": {
"build:all": "npm run build:mp-weixin & npm run build:h5"
}
}
多平台项目的测试需要特别关注平台差异:
javascript复制// jest.config.js
module.exports = {
globals: {
'process.env': {
UNI_PLATFORM: process.env.TEST_PLATFORM || 'h5' // 默认为H5
}
},
testMatch: [
`**/__tests__/**/*.${process.env.TEST_PLATFORM || '*'}.test.js`
]
}
javascript复制// __tests__/auth.mp-weixin.test.js
describe('微信登录测试', () => {
beforeAll(() => {
process.env.UNI_PLATFORM = 'mp-weixin'
})
it('应该成功获取微信code', async () => {
const { code } = await wxLogin()
expect(code).toBeTruthy()
})
})
javascript复制#ifdef E2E_TEST
export const testableLogin = login
#endif
json复制{
"scripts": {
"test:e2e:weixin": "TEST_PLATFORM=mp-weixin npm run test:e2e",
"test:e2e:h5": "TEST_PLATFORM=h5 npm run test:e2e"
}
}
在多平台项目中,清晰的协作规范能大幅提高效率:
建议在提交信息中标注影响的平台:
code复制git commit -m "[mp-weixin] 修复微信登录按钮样式问题"
git commit -m "[h5] 添加H5路由权限控制"
审查时应特别关注:
platforms目录替代为平台专属代码添加特殊注释标记:
javascript复制/**
* @platform mp-weixin
* 微信小程序专属支付实现
*/
export function weixinPay() {
// ...
}
多平台项目的CI/CD需要特殊配置:
yaml复制# .github/workflows/build.yml
jobs:
build:
strategy:
matrix:
platform: [mp-weixin, h5]
steps:
- run: npm run build:${{ matrix.platform }}
yaml复制- name: Run tests
run: |
export TEST_PLATFORM=${{ matrix.platform }}
npm test
在最近的一个电商项目中,我们遇到了需要为不同平台实现不同支付流程的需求。最初我们尝试在同一个文件中使用条件编译,但随着业务逻辑的复杂化,文件很快变得难以维护。后来我们重构为使用platforms目录结构:
code复制platforms/
├─mp-weixin/
│ └─pages/
│ └─payment/
│ ├─index.vue # 微信支付页
│ └─result.vue # 支付结果页
└─h5/
└─pages/
└─payment/
├─index.vue # H5支付页
└─result.vue # 支付结果页
这种结构带来了几个明显好处:
重构过程中最大的挑战是提取公共逻辑,我们最终采用了组合式API的方式:
javascript复制// src/composables/usePayment.js
export function usePayment() {
// 公共逻辑
const validateAmount = (amount) => {
return amount > 0
}
return {
validateAmount
}
}
然后在各平台实现中按需扩展:
javascript复制// platforms/mp-weixin/pages/payment/index.vue
import { usePayment } from '@/composables/usePayment'
export default {
setup() {
const { validateAmount } = usePayment()
// 微信特有逻辑
const requestPayment = () => {
// ...
}
return {
validateAmount,
requestPayment
}
}
}
这种架构下,新加入的开发者能够快速理解项目结构,并且添加新平台支持也变得非常直观。当我们需要新增App端的支付实现时,只需在platforms/app-plus目录下创建对应的页面即可,完全不影响现有功能。