平台適配
uni-app是一個使用Vue.js開發跨平台應用程式的前端框架,可以同時運行在iOS、Android、H5以及各種小程式平台。本文將詳細介紹uni-app的跨平台適配策略和最佳實踐,幫助開發者高效地進行多平台開發。
條件編譯
條件編譯是uni-app中用於處理平台差異的重要機制,可以根據不同平台編譯不同的程式碼。
條件編譯語法
1. 使用 #ifdef 和 #ifndef 進行條件編譯
js
// #ifdef 僅在某平台編譯
// #ifdef APP-PLUS
console.log('這段程式碼只在App平台編譯');
// #endif
// #ifndef 除了某平台均編譯
// #ifndef MP-WEIXIN
console.log('這段程式碼不會在微信小程式平台編譯');
// #endif2. 支援的平台
| 值 | 平台 |
|---|---|
| APP-PLUS | App |
| APP-PLUS-NVUE | App nvue |
| H5 | H5 |
| MP-WEIXIN | 微信小程式 |
| MP-ALIPAY | 支付寶小程式 |
| MP-BAIDU | 百度小程式 |
| MP-TOUTIAO | 字節跳動小程式 |
| MP-QQ | QQ小程式 |
| MP-KUAISHOU | 快手小程式 |
| MP | 所有小程式 |
| QUICKAPP-WEBVIEW | 快應用通用 |
| QUICKAPP-WEBVIEW-UNION | 快應用聯盟 |
| QUICKAPP-WEBVIEW-HUAWEI | 快應用華為 |
3. 多平台條件編譯
js
// #ifdef APP-PLUS || H5
console.log('這段程式碼只在App和H5平台編譯');
// #endif條件編譯應用場景
1. 在 template 中使用條件編譯
html
<template>
<view>
<!-- #ifdef APP-PLUS -->
<view>這是App特有的元件</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view>這是微信小程式特有的元件</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<view>這是H5特有的元件</view>
<!-- #endif -->
<!-- 在所有平台都顯示的內容 -->
<view>這是所有平台都顯示的內容</view>
</view>
</template>2. 在 script 中使用條件編譯
js
<script>
export default {
data() {
return {
platformInfo: ''
}
},
onLoad() {
// #ifdef APP-PLUS
this.platformInfo = '目前是App平台';
// 呼叫App特有的API
const currentWebview = this.$scope.$getAppWebview();
// #endif
// #ifdef MP-WEIXIN
this.platformInfo = '目前是微信小程式平台';
// 呼叫微信小程式特有的API
wx.getSystemInfo({
success: (res) => {
console.log(res);
}
});
// #endif
// #ifdef H5
this.platformInfo = '目前是H5平台';
// 呼叫H5特有的API
document.addEventListener('click', () => {
console.log('H5點擊事件');
});
// #endif
},
methods: {
// #ifdef APP-PLUS
appMethod() {
// App平台特有方法
}
// #endif
}
}
</script>3. 在 style 中使用條件編譯
css
<style>
/* 所有平台通用樣式 */
.content {
padding: 15px;
}
/* #ifdef APP-PLUS */
/* App平台特有樣式 */
.app-content {
background-color: #007AFF;
}
/* #endif */
/* #ifdef MP-WEIXIN */
/* 微信小程式平台特有樣式 */
.mp-content {
background-color: #04BE02;
}
/* #endif */
/* #ifdef H5 */
/* H5平台特有樣式 */
.h5-content {
background-color: #FC0;
}
/* #endif */
</style>4. 整體條件編譯
可以對整個檔案進行條件編譯,例如建立平台特有的頁面或元件:
- 特定平台的頁面:pages/login/login.app.vue(僅在App中編譯)
- 特定平台的元件:components/ad/ad.mp.vue(僅在小程式中編譯)
5. 在 static 目錄中的條件編譯
static 目錄下的檔案不會被編譯,但可以透過目錄名稱實現條件編譯:
┌─static
│ ├─app-plus # 僅app平台生效
│ │ └─logo.png
│ ├─h5 # 僅H5平台生效
│ │ └─logo.png
│ ├─mp-weixin # 僅微信小程式平台生效
│ │ └─logo.png
│ └─logo.png # 所有平台生效條件編譯最佳實踐
1. 抽離平台差異程式碼
將平台特有的程式碼抽離到單獨的檔案中,透過條件編譯引入:
js
// platform.js
// #ifdef APP-PLUS
export const platform = {
name: 'APP',
// App平台特有方法和屬性
share: function(options) {
uni.share({
provider: 'weixin',
...options
});
}
};
// #endif
// #ifdef MP-WEIXIN
export const platform = {
name: 'MP-WEIXIN',
// 微信小程式平台特有方法和屬性
share: function(options) {
wx.showShareMenu({
withShareTicket: true,
...options
});
}
};
// #endif
// #ifdef H5
export const platform = {
name: 'H5',
// H5平台特有方法和屬性
share: function(options) {
// H5分享邏輯
}
};
// #endif然後在業務程式碼中統一呼叫:
js
import { platform } from './platform.js';
// 呼叫平台特有的分享方法
platform.share({
title: '分享標題',
content: '分享內容'
});2. 使用統一的API封裝
為不同平台的特性提供統一的API封裝:
js
// api.js
/**
* 取得位置資訊
* @returns {Promise} 位置資訊Promise
*/
export function getLocation() {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
(error) => {
reject(error);
},
{ timeout: 10000 }
);
// #endif
// #ifdef MP-WEIXIN
wx.getLocation({
type: 'gcj02',
success: (res) => {
resolve({
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy
});
},
fail: (err) => {
reject(err);
}
});
// #endif
// #ifdef H5
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
(error) => {
reject(error);
},
{ timeout: 10000 }
);
} else {
reject(new Error('瀏覽器不支援Geolocation API'));
}
// #endif
});
}3. 避免過度使用條件編譯
過度使用條件編譯會導致程式碼難以維護。應盡量減少條件編譯的使用,優先考慮以下方案:
- 使用uni-app提供的跨平台API
- 抽象出平台差異,提供統一的介面
- 使用元件化思想,將平台特有的功能封裝為元件
樣式適配
1. 螢幕適配
uni-app支援基於750rpx螢幕寬度的自適應單位,可以在不同尺寸的螢幕上實現一致的佈局效果。
css
.container {
width: 750rpx; /* 滿螢幕寬度 */
padding: 30rpx; /* 內邊距 */
}
.card {
width: 690rpx; /* 750rpx - 30rpx * 2 */
height: 300rpx;
margin-bottom: 20rpx;
}2. 樣式相容性處理
不同平台對CSS的支援程度不同,需要注意樣式相容性:
css
/* 使用flex佈局,相容性較好 */
.flex-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
/* 避免使用小程式不支援的選擇器 */
/* 不推薦: .parent > .child */
/* 推薦: */
.parent-child {
color: #333;
}
/* 使用條件編譯處理平台特有樣式 */
/* #ifdef H5 */
.special-style {
transition: all 0.3s;
}
/* #endif */3. 安全區域適配
針對全螢幕手機,需要處理安全區域:
css
/* 頁面根元素 */
.page {
padding-bottom: constant(safe-area-inset-bottom); /* iOS 11.0 */
padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
/* 底部固定導航列 */
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(100rpx + constant(safe-area-inset-bottom));
height: calc(100rpx + env(safe-area-inset-bottom));
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}4. 暗黑模式適配
支援系統的暗黑模式:
css
/* 定義變數 */
page {
/* 淺色模式變數 */
--bg-color: #ffffff;
--text-color: #333333;
--border-color: #eeeeee;
}
/* 暗黑模式變數 */
@media (prefers-color-scheme: dark) {
page {
--bg-color: #1a1a1a;
--text-color: #f2f2f2;
--border-color: #333333;
}
}
/* 使用變數 */
.container {
background-color: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}功能適配
1. 導航列適配
不同平台的導航列表現不同,需要進行適配:
html
<template>
<view class="page">
<!-- 自訂導航列 -->
<!-- #ifdef APP-PLUS || H5 -->
<view class="custom-nav" :style="{ height: navHeight + 'px', paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="back" @click="goBack">
<text class="iconfont icon-back"></text>
</view>
<view class="title">{{ title }}</view>
<view class="placeholder"></view>
</view>
</view>
<!-- #endif -->
<!-- 頁面內容,根據平台調整內邊距 -->
<view class="content" :style="contentStyle">
<!-- 頁面內容 -->
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: '頁面標題',
statusBarHeight: 0,
navHeight: 0
}
},
computed: {
contentStyle() {
let style = {};
// #ifdef APP-PLUS || H5
style.paddingTop = this.navHeight + 'px';
// #endif
// #ifdef MP
// 小程式使用原生導航列,不需要額外的內邊距
// #endif
return style;
}
},
onLoad() {
this.initNavBar();
},
methods: {
initNavBar() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight;
// #ifdef APP-PLUS || H5
// App和H5使用自訂導航列
this.navHeight = this.statusBarHeight + 44; // 狀態列高度 + 導航列高度
// #endif
// #ifdef MP-WEIXIN
// 設定小程式原生導航列
uni.setNavigationBarTitle({
title: this.title
});
// #endif
},
goBack() {
uni.navigateBack({
delta: 1
});
}
}
}
</script>
<style>
.page {
position: relative;
}
/* 自訂導航列 */
.custom-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #ffffff;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
}
.nav-content {
display: flex;
height: 44px;
align-items: center;
justify-content: space-between;
padding: 0 15px;
}
.back {
width: 30px;
height: 30px;
display: flex;
align-items: center;
}
.title {
font-size: 18px;
font-weight: 500;
}
.placeholder {
width: 30px;
}
/* 內容區域 */
.content {
width: 100%;
}
</style>2. 底部安全區域適配
針對iPhone X等帶有底部安全區域的裝置:
html
<template>
<view class="page">
<!-- 頁面內容 -->
<view class="content">
<!-- 內容 -->
</view>
<!-- 底部導航列 -->
<view class="footer safe-area-bottom">
<view class="tab-item" v-for="(item, index) in tabs" :key="index" @click="switchTab(index)">
<view class="icon">
<text class="iconfont" :class="currentTab === index ? item.activeIcon : item.icon"></text>
</view>
<view class="text" :class="{ active: currentTab === index }">{{ item.text }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
currentTab: 0,
tabs: [
{ text: '首頁', icon: 'icon-home', activeIcon: 'icon-home-fill' },
{ text: '分類', icon: 'icon-category', activeIcon: 'icon-category-fill' },
{ text: '購物車', icon: 'icon-cart', activeIcon: 'icon-cart-fill' },
{ text: '我的', icon: 'icon-user', activeIcon: 'icon-user-fill' }
]
}
},
methods: {
switchTab(index) {
this.currentTab = index;
}
}
}
</script>
<style>
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.content {
flex: 1;
padding-bottom: 120rpx; /* 為底部導航列預留空間 */
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
background-color: #ffffff;
border-top: 1px solid #eeeeee;
height: 100rpx;
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0;
}
.icon {
font-size: 44rpx;
color: #999999;
margin-bottom: 8rpx;
}
.text {
font-size: 20rpx;
color: #999999;
}
.text.active {
color: #007AFF;
}
.tab-item .icon .iconfont.active {
color: #007AFF;
}
</style>3. 鍵盤遮擋處理
在表單輸入時,需要處理鍵盤遮擋問題:
html
<template>
<view class="page" :style="{ paddingBottom: keyboardHeight + 'px' }">
<view class="form">
<view class="form-item">
<input
type="text"
placeholder="請輸入使用者名稱"
v-model="username"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</view>
<view class="form-item">
<input
type="password"
placeholder="請輸入密碼"
v-model="password"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</view>
<button class="submit-btn" @click="submit">登入</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
keyboardHeight: 0
}
},
onLoad() {
// 監聽鍵盤高度變化
uni.onKeyboardHeightChange((res) => {
this.keyboardHeight = res.height;
});
},
methods: {
onInputFocus() {
// 輸入框獲得焦點時的處理
},
onInputBlur() {
// 輸入框失去焦點時的處理
},
submit() {
// 提交表單
console.log('使用者名稱:', this.username);
console.log('密碼:', this.password);
}
}
}
</script>
<style>
.page {
min-height: 100vh;
padding: 30rpx;
transition: padding-bottom 0.3s;
}
.form {
max-width: 600rpx;
margin: 0 auto;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 1px solid #dddddd;
border-radius: 8rpx;
font-size: 28rpx;
}
.submit-btn {
width: 100%;
height: 80rpx;
background-color: #007AFF;
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 32rpx;
margin-top: 40rpx;
}
</style>平台特有功能
1. 分享功能
不同平台的分享實現方式不同:
js
// share.js
/**
* 統一分享功能
*/
class ShareManager {
/**
* 分享內容
* @param {Object} options 分享選項
*/
static share(options) {
const { title, content, imageUrl, path } = options;
// #ifdef APP-PLUS
// App平台使用原生分享
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href: path,
title: title,
summary: content,
imageUrl: imageUrl,
success: (res) => {
console.log('分享成功');
},
fail: (err) => {
console.error('分享失敗:', err);
}
});
// #endif
// #ifdef MP-WEIXIN
// 微信小程式使用轉發
wx.showShareMenu({
withShareTicket: true,
success: (res) => {
console.log('設定分享選單成功');
}
});
// #endif
// #ifdef H5
// H5平台使用Web Share API或自訂分享
if (navigator.share) {
navigator.share({
title: title,
text: content,
url: window.location.href
}).then(() => {
console.log('分享成功');
}).catch((err) => {
console.error('分享失敗:', err);
});
} else {
// 降級到自訂分享
this.customShare(options);
}
// #endif
}
/**
* 自訂分享(H5降級方案)
* @param {Object} options 分享選項
*/
static customShare(options) {
// 顯示分享選單
uni.showActionSheet({
itemList: ['複製連結', '分享到微信', '分享到微博'],
success: (res) => {
switch (res.tapIndex) {
case 0:
this.copyToClipboard(window.location.href);
break;
case 1:
this.shareToWeChat(options);
break;
case 2:
this.shareToWeibo(options);
break;
}
}
});
}
/**
* 複製到剪貼簿
* @param {string} text 要複製的文字
*/
static copyToClipboard(text) {
// #ifdef H5
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
uni.showToast({
title: '連結已複製',
icon: 'success'
});
});
} else {
// 降級方案
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
uni.showToast({
title: '連結已複製',
icon: 'success'
});
}
// #endif
// #ifdef APP-PLUS || MP
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '連結已複製',
icon: 'success'
});
}
});
// #endif
}
/**
* 分享到微信(H5)
* @param {Object} options 分享選項
*/
static shareToWeChat(options) {
// 實現微信分享邏輯
console.log('分享到微信:', options);
}
/**
* 分享到微博(H5)
* @param {Object} options 分享選項
*/
static shareToWeibo(options) {
// 實現微博分享邏輯
const url = `https://service.weibo.com/share/share.php?url=${encodeURIComponent(window.location.href)}&title=${encodeURIComponent(options.title)}&pic=${encodeURIComponent(options.imageUrl)}`;
window.open(url, '_blank');
}
}
export default ShareManager;2. 支付功能
不同平台的支付實現:
js
// payment.js
/**
* 統一支付功能
*/
class PaymentManager {
/**
* 發起支付
* @param {Object} options 支付選項
*/
static pay(options) {
const { provider, orderInfo, amount } = options;
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
// App平台支付
uni.requestPayment({
provider: provider, // 'alipay', 'wxpay'
orderInfo: orderInfo,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
// #endif
// #ifdef MP-WEIXIN
// 微信小程式支付
wx.requestPayment({
timeStamp: orderInfo.timeStamp,
nonceStr: orderInfo.nonceStr,
package: orderInfo.package,
signType: orderInfo.signType,
paySign: orderInfo.paySign,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
// #endif
// #ifdef H5
// H5平台支付(跳轉到支付頁面)
if (provider === 'alipay') {
window.location.href = orderInfo.payUrl;
} else if (provider === 'wxpay') {
// 微信H5支付
this.wxH5Pay(orderInfo);
} else {
reject(new Error('不支援的支付方式'));
}
// #endif
});
}
/**
* 微信H5支付
* @param {Object} orderInfo 訂單資訊
*/
static wxH5Pay(orderInfo) {
// #ifdef H5
// 跳轉到微信支付頁面
window.location.href = orderInfo.mweb_url;
// #endif
}
/**
* 檢查支付結果
* @param {string} orderId 訂單ID
*/
static checkPaymentResult(orderId) {
return new Promise((resolve, reject) => {
// 呼叫後端API檢查支付結果
uni.request({
url: '/api/payment/check',
method: 'POST',
data: { orderId },
success: (res) => {
if (res.data.success) {
resolve(res.data);
} else {
reject(new Error(res.data.message));
}
},
fail: (err) => {
reject(err);
}
});
});
}
}
export default PaymentManager;3. 推送功能
不同平台的推送實現:
js
// push.js
/**
* 統一推送功能
*/
class PushManager {
/**
* 初始化推送
*/
static init() {
// #ifdef APP-PLUS
// App平台推送初始化
const main = plus.android.runtimeMainActivity();
const pkgName = main.getPackageName();
const uid = main.getApplicationInfo().uid;
// 監聽推送訊息
plus.push.addEventListener('click', (msg) => {
console.log('推送訊息點擊:', msg);
this.handlePushMessage(msg);
});
plus.push.addEventListener('receive', (msg) => {
console.log('收到推送訊息:', msg);
this.handlePushMessage(msg);
});
// #endif
// #ifdef MP-WEIXIN
// 微信小程式訂閱訊息
this.requestSubscribeMessage();
// #endif
// #ifdef H5
// H5平台Web Push
this.initWebPush();
// #endif
}
/**
* 處理推送訊息
* @param {Object} msg 推送訊息
*/
static handlePushMessage(msg) {
const { title, content, payload } = msg;
// 根據推送內容進行相應處理
if (payload && payload.type) {
switch (payload.type) {
case 'order':
// 跳轉到訂單頁面
uni.navigateTo({
url: `/pages/order/detail?id=${payload.orderId}`
});
break;
case 'message':
// 跳轉到訊息頁面
uni.navigateTo({
url: '/pages/message/index'
});
break;
default:
// 預設處理
break;
}
}
}
/**
* 請求訂閱訊息(微信小程式)
*/
static requestSubscribeMessage() {
// #ifdef MP-WEIXIN
wx.requestSubscribeMessage({
tmplIds: ['template_id_1', 'template_id_2'],
success: (res) => {
console.log('訂閱訊息授權成功:', res);
},
fail: (err) => {
console.error('訂閱訊息授權失敗:', err);
}
});
// #endif
}
/**
* 初始化Web Push(H5)
*/
static initWebPush() {
// #ifdef H5
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('Service Worker註冊成功:', registration);
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array('your-vapid-public-key')
});
})
.then((subscription) => {
console.log('Push訂閱成功:', subscription);
// 將訂閱資訊傳送到伺服器
this.sendSubscriptionToServer(subscription);
})
.catch((err) => {
console.error('Push初始化失敗:', err);
});
}
// #endif
}
/**
* 將訂閱資訊傳送到伺服器
* @param {Object} subscription 訂閱資訊
*/
static sendSubscriptionToServer(subscription) {
// #ifdef H5
fetch('/api/push/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
});
// #endif
}
/**
* 轉換VAPID金鑰格式
* @param {string} base64String base64字串
*/
static urlBase64ToUint8Array(base64String) {
// #ifdef H5
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
// #endif
}
}
export default PushManager;效能最佳化
1. 平台特定最佳化
不同平台有不同的效能特點,需要針對性最佳化:
js
// performance.js
/**
* 效能最佳化工具
*/
class PerformanceOptimizer {
/**
* 初始化效能最佳化
*/
static init() {
// #ifdef APP-PLUS
// App平台最佳化
this.optimizeForApp();
// #endif
// #ifdef H5
// H5平台最佳化
this.optimizeForH5();
// #endif
// #ifdef MP
// 小程式平台最佳化
this.optimizeForMiniProgram();
// #endif
}
/**
* App平台最佳化
*/
static optimizeForApp() {
// #ifdef APP-PLUS
// 設定webview最佳化
const currentWebview = this.$scope.$getAppWebview();
currentWebview.setStyle({
hardwareAccelerated: true, // 開啟硬體加速
scrollIndicator: 'none', // 隱藏滾動條
bounce: 'vertical' // 設定回彈效果
});
// 預載入下一個頁面
this.preloadNextPage();
// #endif
}
/**
* H5平台最佳化
*/
static optimizeForH5() {
// #ifdef H5
// 圖片懶載入
this.enableImageLazyLoading();
// 啟用Service Worker快取
this.enableServiceWorkerCache();
// 預載入關鍵資源
this.preloadCriticalResources();
// #endif
}
/**
* 小程式平台最佳化
*/
static optimizeForMiniProgram() {
// #ifdef MP
// 設定分包預載入
this.enableSubpackagePreload();
// 最佳化setData呼叫
this.optimizeSetData();
// #endif
}
/**
* 預載入下一個頁面
*/
static preloadNextPage() {
// #ifdef APP-PLUS
uni.preloadPage({
url: '/pages/next/next'
});
// #endif
}
/**
* 啟用圖片懶載入
*/
static enableImageLazyLoading() {
// #ifdef H5
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
// #endif
}
/**
* 啟用Service Worker快取
*/
static enableServiceWorkerCache() {
// #ifdef H5
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('Service Worker註冊成功:', registration);
})
.catch(error => {
console.log('Service Worker註冊失敗:', error);
});
}
// #endif
}
/**
* 預載入關鍵資源
*/
static preloadCriticalResources() {
// #ifdef H5
const criticalResources = [
'/static/css/critical.css',
'/static/js/critical.js',
'/static/images/logo.png'
];
criticalResources.forEach(resource => {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource;
if (resource.endsWith('.css')) {
link.as = 'style';
} else if (resource.endsWith('.js')) {
link.as = 'script';
} else if (resource.match(/\.(png|jpg|jpeg|gif|webp)$/)) {
link.as = 'image';
}
document.head.appendChild(link);
});
// #endif
}
/**
* 啟用分包預載入
*/
static enableSubpackagePreload() {
// #ifdef MP
// 在pages.json中配置preloadRule
// 這裡只是示例,實際配置在pages.json中
console.log('分包預載入已在pages.json中配置');
// #endif
}
/**
* 最佳化setData呼叫
*/
static optimizeSetData() {
// #ifdef MP
// 批次更新資料
let pendingData = {};
let updateTimer = null;
const batchSetData = (data) => {
Object.assign(pendingData, data);
if (updateTimer) {
clearTimeout(updateTimer);
}
updateTimer = setTimeout(() => {
this.setData(pendingData);
pendingData = {};
updateTimer = null;
}, 16); // 約60fps
};
// 替換原生setData方法
const originalSetData = this.setData;
this.setData = batchSetData;
// #endif
}
}
export default PerformanceOptimizer;2. 記憶體管理
不同平台的記憶體管理策略:
js
// memory.js
/**
* 記憶體管理工具
*/
class MemoryManager {
/**
* 初始化記憶體管理
*/
static init() {
// 監聽記憶體警告
this.watchMemoryWarning();
// 設定自動清理
this.setupAutoCleanup();
}
/**
* 監聽記憶體警告
*/
static watchMemoryWarning() {
// #ifdef APP-PLUS
// App平台記憶體警告
plus.globalEvent.addEventListener('newintent', () => {
this.cleanup();
});
// #endif
// #ifdef MP
// 小程式記憶體警告
wx.onMemoryWarning(() => {
console.log('記憶體不足警告');
this.cleanup();
});
// #endif
// #ifdef H5
// H5平台記憶體監控
if ('memory' in performance) {
setInterval(() => {
const memory = performance.memory;
const usedPercent = memory.usedJSHeapSize / memory.totalJSHeapSize;
if (usedPercent > 0.8) {
console.log('記憶體使用率過高:', usedPercent);
this.cleanup();
}
}, 10000);
}
// #endif
}
/**
* 設定自動清理
*/
static setupAutoCleanup() {
// 頁面隱藏時清理
uni.onAppHide(() => {
this.cleanup();
});
// 定期清理
setInterval(() => {
this.cleanup();
}, 300000); // 5分鐘清理一次
}
/**
* 清理記憶體
*/
static cleanup() {
// 清理圖片快取
this.clearImageCache();
// 清理資料快取
this.clearDataCache();
// 清理事件監聽器
this.clearEventListeners();
// 強制垃圾回收(如果支援)
this.forceGarbageCollection();
}
/**
* 清理圖片快取
*/
static clearImageCache() {
// #ifdef H5
// 清理已載入的圖片
const images = document.querySelectorAll('img');
images.forEach(img => {
if (!img.getBoundingClientRect().top < window.innerHeight) {
img.src = '';
}
});
// #endif
}
/**
* 清理資料快取
*/
static clearDataCache() {
// 清理過期的本地儲存
const now = Date.now();
const keys = uni.getStorageInfoSync().keys;
keys.forEach(key => {
if (key.startsWith('cache_')) {
const data = uni.getStorageSync(key);
if (data && data.expireTime && now > data.expireTime) {
uni.removeStorageSync(key);
}
}
});
}
/**
* 清理事件監聽器
*/
static clearEventListeners() {
// 移除不必要的事件監聽器
// 這裡需要根據具體應用程式進行實現
}
/**
* 強制垃圾回收
*/
static forceGarbageCollection() {
// #ifdef H5
if (window.gc) {
window.gc();
}
// #endif
}
}
export default MemoryManager;除錯與測試
1. 跨平台除錯
不同平台的除錯方法:
js
// debug.js
/**
* 跨平台除錯工具
*/
class DebugManager {
/**
* 初始化除錯
*/
static init() {
// 設定除錯模式
this.isDebug = process.env.NODE_ENV === 'development';
// 初始化日誌系統
this.initLogger();
// 設定錯誤捕獲
this.setupErrorCapture();
}
/**
* 初始化日誌系統
*/
static initLogger() {
// #ifdef APP-PLUS
// App平台日誌
this.logger = {
log: (message, ...args) => {
if (this.isDebug) {
console.log(`[APP] ${message}`, ...args);
plus.console.log(`[APP] ${message}`, ...args);
}
},
error: (message, ...args) => {
console.error(`[APP] ${message}`, ...args);
plus.console.error(`[APP] ${message}`, ...args);
}
};
// #endif
// #ifdef H5
// H5平台日誌
this.logger = {
log: (message, ...args) => {
if (this.isDebug) {
console.log(`[H5] ${message}`, ...args);
}
},
error: (message, ...args) => {
console.error(`[H5] ${message}`, ...args);
// 傳送錯誤到監控服務
this.sendErrorToMonitoring(message, args);
}
};
// #endif
// #ifdef MP
// 小程式平台日誌
this.logger = {
log: (message, ...args) => {
if (this.isDebug) {
console.log(`[MP] ${message}`, ...args);
}
},
error: (message, ...args) => {
console.error(`[MP] ${message}`, ...args);
}
};
// #endif
}
/**
* 設定錯誤捕獲
*/
static setupErrorCapture() {
// Vue錯誤處理
Vue.config.errorHandler = (err, vm, info) => {
this.logger.error('Vue錯誤:', err, info);
};
// Promise錯誤處理
window.addEventListener('unhandledrejection', (event) => {
this.logger.error('未處理的Promise拒絕:', event.reason);
});
// #ifdef H5
// H5平台全域錯誤處理
window.addEventListener('error', (event) => {
this.logger.error('全域錯誤:', event.error);
});
// #endif
}
/**
* 傳送錯誤到監控服務
* @param {string} message 錯誤訊息
* @param {Array} args 錯誤參數
*/
static sendErrorToMonitoring(message, args) {
// #ifdef H5
// 傳送到錯誤監控服務(如Sentry)
if (window.Sentry) {
window.Sentry.captureException(new Error(message));
}
// #endif
}
/**
* 效能監控
*/
static performanceMonitor() {
// #ifdef H5
// 監控頁面載入效能
window.addEventListener('load', () => {
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;
this.logger.log('頁面載入時間:', loadTime + 'ms');
});
// #endif
// #ifdef APP-PLUS
// App平台效能監控
const startTime = Date.now();
plus.globalEvent.addEventListener('plusready', () => {
const loadTime = Date.now() - startTime;
this.logger.log('App啟動時間:', loadTime + 'ms');
});
// #endif
}
}
// 建立全域日誌物件
const logger = {
log: DebugManager.logger?.log || console.log,
error: DebugManager.logger?.error || console.error,
warn: DebugManager.logger?.warn || console.warn,
info: DebugManager.logger?.info || console.info
};
export default logger;2. 跨平台測試
針對不同平台的測試方法:
js
// test-utils.js
/**
* 跨平台測試工具
*/
class TestUtils {
/**
* 取得目前平台
* @returns {string} 平台標識
*/
static getPlatform() {
// #ifdef APP-PLUS
return 'APP';
// #endif
// #ifdef H5
return 'H5';
// #endif
// #ifdef MP-WEIXIN
return 'MP-WEIXIN';
// #endif
// #ifdef MP-ALIPAY
return 'MP-ALIPAY';
// #endif
// #ifdef MP-BAIDU
return 'MP-BAIDU';
// #endif
// #ifdef MP-TOUTIAO
return 'MP-TOUTIAO';
// #endif
// #ifdef MP-QQ
return 'MP-QQ';
// #endif
return 'UNKNOWN';
}
/**
* 判斷是否是App平台
* @returns {boolean} 是否是App平台
*/
static isApp() {
// #ifdef APP-PLUS
return true;
// #endif
return false;
}
/**
* 判斷是否是H5平台
* @returns {boolean} 是否是H5平台
*/
static isH5() {
// #ifdef H5
return true;
// #endif
return false;
}
/**
* 判斷是否是小程式平台
* @returns {boolean} 是否是小程式平台
*/
static isMp() {
// #ifdef MP
return true;
// #endif
return false;
}
/**
* 模擬API呼叫
* @param {Function} api 要呼叫的API
* @param {Object} params 參數
* @param {Object} mockResult 模擬結果
* @returns {Promise} Promise物件
*/
static mockApiCall(api, params, mockResult) {
return new Promise((resolve, reject) => {
// 開發環境使用模擬資料
if (process.env.NODE_ENV === 'development') {
setTimeout(() => {
if (mockResult.success) {
resolve(mockResult.data);
} else {
reject(mockResult.error);
}
}, mockResult.delay || 300);
} else {
// 生產環境實際呼叫
api(params)
.then(resolve)
.catch(reject);
}
});
}
/**
* 檢查API是否可用
* @param {string} apiName API名稱
* @returns {boolean} 是否可用
*/
static isApiAvailable(apiName) {
if (!uni[apiName]) {
return false;
}
// 特殊API檢查
if (apiName === 'share' && !this.isApp()) {
return false;
}
if (apiName === 'requestPayment' && this.isH5()) {
return false;
}
return true;
}
/**
* 安全呼叫API
* @param {string} apiName API名稱
* @param {Object} params 參數
* @param {Function} fallback 降級函數
* @returns {Promise} Promise物件
*/
static safeApiCall(apiName, params, fallback) {
return new Promise((resolve, reject) => {
if (this.isApiAvailable(apiName)) {
uni[apiName]({
...params,
success: resolve,
fail: reject
});
} else if (typeof fallback === 'function') {
try {
const result = fallback(params);
resolve(result);
} catch (err) {
reject(err);
}
} else {
reject(new Error(`API ${apiName} 不可用`));
}
});
}
}
export default TestUtils;總結
uni-app的跨平台適配是一個系統性工程,需要從多個方面進行考慮:
- 條件編譯:使用條件編譯處理平台差異,但要避免過度使用
- 樣式適配:使用rpx單位、安全區域適配、暗黑模式等技術實現一致的視覺效果
- 功能適配:針對導航列、底部安全區域、鍵盤遮擋等常見問題提供解決方案
- 平台特有功能:為分享、支付、推送等平台特有功能提供統一的API封裝
- 效能最佳化:針對不同平台的效能特點,採用相應的最佳化策略
- 除錯與測試:提供跨平台的除錯和測試工具,確保應用程式在各平台的穩定性
透過合理的架構設計和適配策略,可以在保持程式碼統一性的同時,充分發揮各平台的特性,提供良好的使用者體驗。