Skip to content
alex's blog
Go back

UniApp 跨平台开发最佳实践

编辑页面

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 跨平台开发的最佳实践包括:

  1. 条件编译:处理平台差异
  2. API 封装:统一接口调用
  3. 状态管理:使用 Vuex 管理全局状态
  4. 组件封装:提高代码复用性
  5. 性能优化:图片、列表、防抖节流
  6. 错误处理:全局错误捕获和处理
  7. 平台特性:合理使用平台特定功能

掌握这些最佳实践,能让你的 UniApp 应用在各平台都能稳定、高效地运行。


跨平台开发需要平衡通用性和平台特性,合理使用条件编译和封装,能让开发事半功倍。


编辑页面
Share this post on:

Previous Post
UniApp 性能优化实战指南
Next Post
UniApp 入门指南 - 跨平台开发从零开始