UniApp 的强大之处在于一套代码可以运行在多个平台,但不同平台之间存在差异。掌握跨平台开发的最佳实践,能让你的应用在各平台都能完美运行。
条件编译
条件编译是 UniApp 处理平台差异的核心机制。
1. 页面级条件编译
<template>
<!-- #ifdef H5 -->
<view class="h5-only">H5 平台专用内容</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view class="weixin-only">微信小程序专用内容</view>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<view class="app-only">App 平台专用内容</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view>非 H5 平台显示</view>
<!-- #endif -->
</template>
2. 样式条件编译
<style>
/* #ifdef H5 */
.container {
padding: 20px;
}
/* #endif */
/* #ifdef MP-WEIXIN */
.container {
padding: 20rpx;
}
/* #endif */
/* #ifdef APP-PLUS */
.container {
padding: 20rpx;
/* App 特有样式 */
}
/* #endif */
</style>
3. 脚本条件编译
export default {
onLoad() {
// #ifdef H5
console.log('H5 平台')
// #endif
// #ifdef MP-WEIXIN
console.log('微信小程序平台')
// #endif
// #ifdef APP-PLUS
console.log('App 平台')
// #endif
},
methods: {
// #ifdef H5
h5Method() {
// H5 专用方法
},
// #endif
// #ifdef APP-PLUS
appMethod() {
// App 专用方法
},
// #endif
}
}
4. 静态资源条件编译
<template>
<!-- #ifdef H5 -->
<image src="/static/h5-logo.png" />
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<image src="/static/weixin-logo.png" />
<!-- #endif -->
</template>
平台差异处理
1. API 兼容性处理
// 封装统一的 API 调用
const platformAPI = {
// 获取系统信息
getSystemInfo() {
// #ifdef H5
return {
platform: 'h5',
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
}
// #endif
// #ifdef MP-WEIXIN
return uni.getSystemInfoSync()
// #endif
// #ifdef APP-PLUS
return plus.screen.getResolution()
// #endif
},
// 设置导航栏标题
setNavigationBarTitle(title) {
// #ifdef H5
document.title = title
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
uni.setNavigationBarTitle({ title })
// #endif
}
}
2. 单位处理
// 统一单位处理工具
const unitUtils = {
// 将 px 转换为 rpx
pxToRpx(px) {
const systemInfo = uni.getSystemInfoSync()
return (750 / systemInfo.windowWidth) * px
},
// 将 rpx 转换为 px
rpxToPx(rpx) {
const systemInfo = uni.getSystemInfoSync()
return (systemInfo.windowWidth / 750) * rpx
}
}
3. 样式适配
<style>
/* 使用 rpx 单位,自动适配不同屏幕 */
.container {
width: 750rpx; /* 等于屏幕宽度 */
padding: 20rpx;
font-size: 28rpx;
}
/* 使用 vh/vw 单位(H5) */
/* #ifdef H5 */
.full-screen {
width: 100vw;
height: 100vh;
}
/* #endif */
/* 使用 flex 布局 */
.flex-container {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>
网络请求封装
统一请求封装
// utils/request.js
const baseURL = 'https://api.example.com'
// 平台特定的请求配置
const getRequestConfig = () => {
const config = {
baseURL,
timeout: 10000
}
// #ifdef MP-WEIXIN
config.header = {
'content-type': 'application/json'
}
// #endif
// #ifdef H5
config.withCredentials = true
// #endif
return config
}
// 请求拦截器
const requestInterceptor = (config) => {
// 添加 token
const token = uni.getStorageSync('token')
if (token) {
config.header = config.header || {}
config.header.Authorization = `Bearer ${token}`
}
// 显示加载提示
uni.showLoading({
title: '加载中...',
mask: true
})
return config
}
// 响应拦截器
const responseInterceptor = (response) => {
uni.hideLoading()
// 统一处理响应
if (response.statusCode === 200) {
if (response.data.code === 0) {
return response.data.data
} else {
uni.showToast({
title: response.data.message || '请求失败',
icon: 'none'
})
return Promise.reject(response.data)
}
} else {
uni.showToast({
title: '网络错误',
icon: 'none'
})
return Promise.reject(response)
}
}
// 封装请求方法
const request = (options) => {
const config = {
...getRequestConfig(),
...options,
url: baseURL + options.url
}
// 请求拦截
const finalConfig = requestInterceptor(config)
return new Promise((resolve, reject) => {
uni.request({
...finalConfig,
success: (response) => {
try {
const data = responseInterceptor(response)
resolve(data)
} catch (error) {
reject(error)
}
},
fail: (error) => {
uni.hideLoading()
uni.showToast({
title: '网络请求失败',
icon: 'none'
})
reject(error)
}
})
})
}
// 导出方法
export default {
get(url, params = {}) {
return request({
url,
method: 'GET',
data: params
})
},
post(url, data = {}) {
return request({
url,
method: 'POST',
data
})
},
put(url, data = {}) {
return request({
url,
method: 'PUT',
data
})
},
delete(url, params = {}) {
return request({
url,
method: 'DELETE',
data: params
})
}
}
状态管理
使用 Vuex
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
userInfo: null,
token: ''
},
mutations: {
SET_USER_INFO(state, userInfo) {
state.userInfo = userInfo
// 同步到本地存储
uni.setStorageSync('userInfo', userInfo)
},
SET_TOKEN(state, token) {
state.token = token
uni.setStorageSync('token', token)
},
CLEAR_USER(state) {
state.userInfo = null
state.token = ''
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
}
},
actions: {
async login({ commit }, loginData) {
try {
const res = await uni.request({
url: '/api/login',
method: 'POST',
data: loginData
})
commit('SET_USER_INFO', res.data.userInfo)
commit('SET_TOKEN', res.data.token)
return res
} catch (error) {
throw error
}
},
logout({ commit }) {
commit('CLEAR_USER')
}
},
getters: {
isLogin: state => !!state.token,
userName: state => state.userInfo?.name || ''
}
})
export default store
组件封装
通用组件封装
<!-- components/CommonButton.vue -->
<template>
<button
:class="['common-button', `button-${type}`, { 'button-disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script>
export default {
name: 'CommonButton',
props: {
type: {
type: String,
default: 'primary'
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick() {
if (!this.disabled) {
this.$emit('click')
}
}
}
}
</script>
<style scoped>
.common-button {
padding: 20rpx 40rpx;
border-radius: 10rpx;
font-size: 28rpx;
border: none;
}
.button-primary {
background-color: #007aff;
color: #fff;
}
.button-default {
background-color: #f0f0f0;
color: #333;
}
.button-disabled {
opacity: 0.5;
}
</style>
性能优化
1. 图片优化
<template>
<image
:src="imageSrc"
mode="aspectFit"
:lazy-load="true"
@error="handleImageError"
/>
</template>
<script>
export default {
data() {
return {
imageSrc: ''
}
},
methods: {
// 图片加载失败处理
handleImageError() {
// #ifdef H5
this.imageSrc = '/static/default-image.png'
// #endif
// #ifdef MP-WEIXIN || APP-PLUS
this.imageSrc = '/static/default-image.png'
// #endif
},
// 图片压缩(App 平台)
compressImage(path) {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
plus.compressImage({
src: path,
quality: 80,
success: (result) => {
resolve(result.target)
},
fail: reject
})
})
// #endif
// #ifndef APP-PLUS
return Promise.resolve(path)
// #endif
}
}
}
</script>
2. 列表优化
<template>
<scroll-view
scroll-y
class="scroll-container"
@scrolltolower="loadMore"
:lower-threshold="100"
>
<view
v-for="(item, index) in list"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
<view v-if="loading" class="loading">
加载中...
</view>
<view v-if="noMore" class="no-more">
没有更多了
</view>
</scroll-view>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 10,
loading: false,
noMore: false
}
},
onLoad() {
this.loadData()
},
methods: {
async loadData() {
if (this.loading || this.noMore) return
this.loading = true
try {
const res = await this.fetchList({
page: this.page,
pageSize: this.pageSize
})
if (res.length < this.pageSize) {
this.noMore = true
}
this.list = [...this.list, ...res]
this.page++
} catch (error) {
console.error(error)
} finally {
this.loading = false
}
},
loadMore() {
this.loadData()
}
}
}
</script>
3. 防抖和节流
// utils/debounce.js
export function debounce(func, wait = 300) {
let timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// utils/throttle.js
export function throttle(func, wait = 300) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= wait) {
lastTime = now
func.apply(this, args)
}
}
}
// 使用示例
import { debounce, throttle } from '@/utils/debounce'
export default {
methods: {
// 防抖搜索
handleSearch: debounce(function(keyword) {
this.search(keyword)
}, 500),
// 节流滚动
handleScroll: throttle(function() {
// 滚动处理
}, 200)
}
}
错误处理
全局错误处理
// main.js
import App from './App'
// 全局错误处理
Vue.config.errorHandler = (err, vm, info) => {
console.error('全局错误:', err, info)
// 上报错误
// #ifdef H5
// H5 平台错误上报
// #endif
// #ifdef MP-WEIXIN
// 微信小程序错误上报
// #endif
// #ifdef APP-PLUS
// App 平台错误上报
// #endif
}
// 页面错误处理
Vue.mixin({
onError(err) {
console.error('页面错误:', err)
}
})
平台特定功能
1. 分享功能
// 分享配置
export default {
onShareAppMessage() {
// #ifdef MP-WEIXIN
return {
title: '分享标题',
path: '/pages/index/index',
imageUrl: '/static/share-image.png'
}
// #endif
},
onShareTimeline() {
// #ifdef MP-WEIXIN
return {
title: '分享到朋友圈',
imageUrl: '/static/share-image.png'
}
// #endif
}
}
2. 支付功能
// 统一支付接口
const pay = (orderInfo) => {
// #ifdef MP-WEIXIN
return uni.requestPayment({
provider: 'wxpay',
...orderInfo
})
// #endif
// #ifdef APP-PLUS
return uni.requestPayment({
provider: 'alipay',
...orderInfo
})
// #endif
// #ifdef H5
// H5 支付处理
window.location.href = orderInfo.payUrl
// #endif
}
总结
UniApp 跨平台开发的最佳实践包括:
- 条件编译:处理平台差异
- API 封装:统一接口调用
- 状态管理:使用 Vuex 管理全局状态
- 组件封装:提高代码复用性
- 性能优化:图片、列表、防抖节流
- 错误处理:全局错误捕获和处理
- 平台特性:合理使用平台特定功能
掌握这些最佳实践,能让你的 UniApp 应用在各平台都能稳定、高效地运行。
跨平台开发需要平衡通用性和平台特性,合理使用条件编译和封装,能让开发事半功倍。