uni-app 多端适配技巧

章节介绍

uni-app 的核心优势之一是能够开发一次,发布到多个平台。然而,由于不同平台的特性和限制存在差异,开发者需要掌握一定的多端适配技巧,才能确保应用在所有平台上都能正常运行并提供良好的用户体验。本章节将详细介绍 uni-app 的多端适配技巧,包括平台差异分析、适配策略和条件编译最佳实践。

核心知识点

1. 平台差异分析

uni-app 支持的平台包括:

  • App 端:iOS、Android
  • 小程序端:微信小程序、支付宝小程序、百度小程序、抖音小程序、QQ 小程序等
  • Web 端:PC 浏览器、移动浏览器

不同平台的主要差异包括:

1.1 运行环境差异

  • App 端:基于原生引擎运行,性能最好,功能最完整
  • 小程序端:基于各平台的小程序运行时,功能和性能有一定限制
  • Web 端:基于浏览器运行,受浏览器特性和安全策略限制

1.2 API 差异

  • App 端:支持完整的 uni-app API 和原生 API
  • 小程序端:只支持 uni-app API 中已适配的部分,且各平台 API 可能存在差异
  • Web 端:只支持 uni-app API 中已适配的部分,且部分 API 可能无法在浏览器中实现

1.3 组件差异

  • App 端:支持完整的 uni-app 组件和原生组件
  • 小程序端:只支持 uni-app 组件中已适配的部分,且各平台组件可能存在差异
  • Web 端:只支持 uni-app 组件中已适配的部分,且部分组件可能无法在浏览器中完美实现

1.4 样式差异

  • App 端:支持大部分 CSS 特性,且有一些原生特有的样式
  • 小程序端:对 CSS 特性的支持程度不同,且各平台可能有特殊的样式限制
  • Web 端:支持标准 CSS 特性,但受浏览器兼容性影响

1.5 性能差异

  • App 端:性能最好,支持复杂的动画和交互
  • 小程序端:性能次之,对复杂的动画和交互有一定限制
  • Web 端:性能受浏览器和网络环境影响较大

2. 适配策略

针对不同平台的差异,uni-app 提供了多种适配策略:

2.1 条件编译

条件编译是 uni-app 最核心的多端适配技术,它允许开发者在同一套代码中,为不同平台编写不同的代码。条件编译使用特殊的注释语法,在编译时根据目标平台只保留对应平台的代码。

2.2 平台判断

在运行时,通过 uni.getSystemInfoSync().platformprocess.env.VUE_APP_PLATFORM 来判断当前平台,从而执行不同的代码逻辑。

2.3 API 适配

  • 使用 uni-app 提供的跨平台 API,避免直接使用平台特定的 API
  • 对于平台特定的 API,使用条件编译或平台判断进行适配
  • 对于不存在的 API,提供降级方案

2.4 组件适配

  • 使用 uni-app 提供的跨平台组件,避免直接使用平台特定的组件
  • 对于平台特定的组件,使用条件编译或平台判断进行适配
  • 对于不存在的组件,提供自定义组件或降级方案

2.5 样式适配

  • 使用 uni-app 推荐的样式方案,如 rpx 单位
  • 对于平台特定的样式,使用条件编译进行适配
  • 对于样式差异较大的平台,提供平台特定的样式文件

2.6 性能适配

  • 根据平台性能差异,调整应用的复杂度和动画效果
  • 对于性能较差的平台,提供简化版的界面和交互
  • 优化代码,减少不必要的计算和渲染

3. 条件编译最佳实践

条件编译是 uni-app 多端适配的核心技术,正确使用条件编译可以大大提高开发效率和代码质量。

3.1 条件编译的语法

uni-app 支持以下条件编译语法:

  • HTML 模板<!-- #ifdef PLATFORM --><!-- #endif -->
  • CSS 样式/* #ifdef PLATFORM *//* #endif */
  • JavaScript// #ifdef PLATFORM// #endif
  • JSON 配置"__PLATFORM__": true

其中,PLATFORM 可以是以下值:

  • APP-PLUS:App 端
  • APP-PLUS-NVUE:App 端的 nvue 页面
  • MP-WEIXIN:微信小程序
  • MP-ALIPAY:支付宝小程序
  • MP-BAIDU:百度小程序
  • MP-TOUTIAO:抖音小程序
  • MP-QQ:QQ 小程序
  • MP-KUAISHOU:快手小程序
  • MP-JD:京东小程序
  • MP-REDMI:红米小程序
  • WEB:Web 端
  • H5:H5 端(等同于 WEB)
  • MP:所有小程序端
  • APP:所有 App 端

3.2 条件编译的使用场景

  • API 调用:当不同平台的 API 存在差异时
  • 组件使用:当不同平台的组件存在差异时
  • 样式调整:当不同平台的样式需要调整时
  • 功能开关:当某些功能只在特定平台支持时
  • 性能优化:当不同平台需要不同的性能优化策略时

3.3 条件编译的最佳实践

  • 合理使用:只在必要时使用条件编译,避免过度使用导致代码混乱
  • 分类管理:将平台特定的代码集中管理,提高代码可维护性
  • 注释说明:为条件编译的代码添加注释,说明适配的原因和平台
  • 测试验证:在所有目标平台上测试条件编译的代码,确保其正常工作
  • 文档化:将多端适配的策略和注意事项文档化,方便团队协作

4. 多端适配工具和插件

uni-app 生态提供了多种多端适配工具和插件:

  • uni-app 官方插件市场:提供了大量多端适配相关的插件
  • uni-cloud:提供了云端能力,减少端侧差异
  • uni-ui:提供了一套跨平台的 UI 组件库
  • uni-app x:新一代 uni-app 引擎,提供更好的多端适配能力

5. 多端适配的常见问题和解决方案

5.1 API 不存在或差异

问题:某些 API 在特定平台不存在或行为不同。

解决方案

  • 使用条件编译为不同平台提供不同的实现
  • 提供降级方案,当 API 不存在时使用替代方案
  • 使用 uni-app 官方推荐的跨平台 API

5.2 组件表现不一致

问题:同一组件在不同平台的表现不一致。

解决方案

  • 使用条件编译为不同平台提供不同的组件实现
  • 针对特定平台的组件进行样式调整
  • 使用自定义组件统一不同平台的表现

5.3 样式差异

问题:相同的样式在不同平台的表现不一致。

解决方案

  • 使用 rpx 单位,自动适配不同屏幕尺寸
  • 使用条件编译为不同平台提供不同的样式
  • 避免使用平台特定的样式特性

5.4 性能问题

问题:应用在某些平台上性能较差。

解决方案

  • 根据平台性能差异,调整应用的复杂度和动画效果
  • 使用条件编译为不同平台提供不同的性能优化策略
  • 优化代码,减少不必要的计算和渲染

5.5 审核问题

问题:应用在某些平台审核不通过。

解决方案

  • 了解各平台的审核规则和禁忌
  • 使用条件编译为不同平台提供符合其审核规则的实现
  • 避免使用平台禁止的 API 和功能

实用案例分析

案例一:实现平台特定的登录功能

功能说明

实现一个登录功能,在不同平台使用不同的登录方式:

  • App 端:支持手机号登录、微信登录、Apple ID 登录(iOS)
  • 微信小程序:支持微信登录
  • 支付宝小程序:支持支付宝登录
  • Web 端:支持手机号登录

实现步骤

  1. 创建登录页面

    <template>
      <view class="login-container">
        <view class="login-form">
          <view class="form-item">
            <input type="text" v-model="phone" placeholder="请输入手机号" />
          </view>
          <view class="form-item">
            <input type="password" v-model="password" placeholder="请输入密码" />
          </view>
          <button class="login-btn" @click="phoneLogin">手机号登录</button>
        </view>
        
        <view class="third-party-login">
          <!-- #ifdef MP-WEIXIN -->
          <button class="wechat-login-btn" @click="wechatLogin">微信登录</button>
          <!-- #endif -->
          
          <!-- #ifdef MP-ALIPAY -->
          <button class="alipay-login-btn" @click="alipayLogin">支付宝登录</button>
          <!-- #endif -->
          
          <!-- #ifdef APP-PLUS -->
          <button class="wechat-login-btn" @click="wechatLogin">微信登录</button>
          <!-- #endif -->
          
          <!-- #ifdef APP-PLUS && iOS -->
          <button class="apple-login-btn" @click="appleLogin">Apple ID 登录</button>
          <!-- #endif -->
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          phone: '',
          password: ''
        };
      },
      methods: {
        // 手机号登录
        phoneLogin() {
          // 实现手机号登录逻辑
          console.log('手机号登录', this.phone, this.password);
        },
        
        // 微信登录
        wechatLogin() {
          // #ifdef APP-PLUS
          // App 端微信登录
          uni.login({
            provider: 'weixin',
            success: (res) => {
              console.log('微信登录成功', res);
              // 发送 code 到服务器进行验证
            },
            fail: (err) => {
              console.log('微信登录失败', err);
            }
          });
          // #endif
          
          // #ifdef MP-WEIXIN
          // 微信小程序登录
          uni.login({
            success: (res) => {
              console.log('微信登录成功', res);
              // 发送 code 到服务器进行验证
            },
            fail: (err) => {
              console.log('微信登录失败', err);
            }
          });
          // #endif
        },
        
        // 支付宝登录
        alipayLogin() {
          // #ifdef MP-ALIPAY
          // 支付宝小程序登录
          uni.login({
            provider: 'alipay',
            success: (res) => {
              console.log('支付宝登录成功', res);
              // 发送 code 到服务器进行验证
            },
            fail: (err) => {
              console.log('支付宝登录失败', err);
            }
          });
          // #endif
        },
        
        // Apple ID 登录
        appleLogin() {
          // #ifdef APP-PLUS && iOS
          // iOS 端 Apple ID 登录
          uni.login({
            provider: 'apple',
            success: (res) => {
              console.log('Apple ID 登录成功', res);
              // 发送 code 到服务器进行验证
            },
            fail: (err) => {
              console.log('Apple ID 登录失败', err);
            }
          });
          // #endif
        }
      }
    };
    </script>
    
    <style>
    .login-container {
      padding: 20px;
    }
    
    .login-form {
      margin-bottom: 30px;
    }
    
    .form-item {
      margin-bottom: 20px;
    }
    
    .form-item input {
      width: 100%;
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    
    .login-btn {
      width: 100%;
      padding: 12px;
      background-color: #007aff;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 16px;
    }
    
    .third-party-login {
      display: flex;
      flex-direction: column;
      gap: 10px;
    }
    
    .wechat-login-btn {
      width: 100%;
      padding: 12px;
      background-color: #07C160;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 16px;
    }
    
    .alipay-login-btn {
      width: 100%;
      padding: 12px;
      background-color: #1677FF;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 16px;
    }
    
    .apple-login-btn {
      width: 100%;
      padding: 12px;
      background-color: #000;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 16px;
    }
    
    /* #ifdef H5 */
    /* Web 端特定样式 */
    .third-party-login {
      display: none;
    }
    /* #endif */
    </style>

案例二:实现平台特定的支付功能

功能说明

实现一个支付功能,在不同平台使用不同的支付方式:

  • App 端:支持微信支付、支付宝支付
  • 微信小程序:支持微信支付
  • 支付宝小程序:支持支付宝支付
  • Web 端:支持微信 H5 支付、支付宝 H5 支付

实现步骤

  1. 创建支付服务

    // services/payment.js
    class PaymentService {
      // 发起支付
      async requestPayment(orderInfo) {
        try {
          const platform = uni.getSystemInfoSync().platform;
          
          // 根据平台选择支付方式
          if (platform === 'ios' || platform === 'android') {
            // App 端支付
            return await this.appPayment(orderInfo);
          } else if (process.env.VUE_APP_PLATFORM === 'mp-weixin') {
            // 微信小程序支付
            return await this.wechatMiniPayment(orderInfo);
          } else if (process.env.VUE_APP_PLATFORM === 'mp-alipay') {
            // 支付宝小程序支付
            return await this.alipayMiniPayment(orderInfo);
          } else if (process.env.VUE_APP_PLATFORM === 'h5') {
            // Web 端支付
            return await this.webPayment(orderInfo);
          } else {
            throw new Error('当前平台不支持支付功能');
          }
        } catch (error) {
          console.log('支付失败', error);
          throw error;
        }
      }
      
      // App 端支付
      async appPayment(orderInfo) {
        return new Promise((resolve, reject) => {
          uni.requestPayment({
            provider: orderInfo.provider, // 'wxpay' 或 'alipay'
            orderInfo: orderInfo.orderInfo,
            success: (res) => {
              console.log('支付成功', res);
              resolve(res);
            },
            fail: (err) => {
              console.log('支付失败', err);
              reject(err);
            }
          });
        });
      }
      
      // 微信小程序支付
      async wechatMiniPayment(orderInfo) {
        return new Promise((resolve, reject) => {
          uni.requestPayment({
            timeStamp: orderInfo.timeStamp,
            nonceStr: orderInfo.nonceStr,
            package: orderInfo.package,
            signType: orderInfo.signType,
            paySign: orderInfo.paySign,
            success: (res) => {
              console.log('支付成功', res);
              resolve(res);
            },
            fail: (err) => {
              console.log('支付失败', err);
              reject(err);
            }
          });
        });
      }
      
      // 支付宝小程序支付
      async alipayMiniPayment(orderInfo) {
        return new Promise((resolve, reject) => {
          uni.requestPayment({
            provider: 'alipay',
            orderInfo: orderInfo.orderInfo,
            success: (res) => {
              console.log('支付成功', res);
              resolve(res);
            },
            fail: (err) => {
              console.log('支付失败', err);
              reject(err);
            }
          });
        });
      }
      
      // Web 端支付
      async webPayment(orderInfo) {
        // Web 端支付需要跳转到支付页面
        if (orderInfo.provider === 'wxpay') {
          // 微信 H5 支付
          window.location.href = orderInfo.payUrl;
        } else if (orderInfo.provider === 'alipay') {
          // 支付宝 H5 支付
          window.location.href = orderInfo.payUrl;
        } else {
          throw new Error('不支持的支付方式');
        }
      }
    }
    
    export default new PaymentService();
  2. 使用支付服务

    <template>
      <view class="payment-page">
        <view class="order-info">
          <view class="order-item">
            <text>商品名称:</text>
            <text>{{ order.goodsName }}</text>
          </view>
          <view class="order-item">
            <text>商品价格:</text>
            <text class="price">{{ order.price }} 元</text>
          </view>
        </view>
        
        <view class="payment-methods">
          <view class="method-item" @click="selectPayment('wxpay')">
            <text>微信支付</text>
            <radio :checked="selectedPayment === 'wxpay'" />
          </view>
          
          <view class="method-item" @click="selectPayment('alipay')">
            <text>支付宝支付</text>
            <radio :checked="selectedPayment === 'alipay'" />
          </view>
        </view>
        
        <button class="pay-btn" @click="pay">立即支付</button>
      </view>
    </template>
    
    <script>
    import paymentService from '@/services/payment';
    
    export default {
      data() {
        return {
          order: {
            goodsName: '测试商品',
            price: 99.9
          },
          selectedPayment: 'wxpay'
        };
      },
      methods: {
        selectPayment(method) {
          this.selectedPayment = method;
        },
        
        async pay() {
          try {
            // 显示加载中
            uni.showLoading({
              title: '正在发起支付...'
            });
            
            // 调用后端接口获取支付参数
            const res = await uni.request({
              url: 'https://example.com/api/payment/create',
              method: 'POST',
              data: {
                goodsName: this.order.goodsName,
                price: this.order.price,
                paymentMethod: this.selectedPayment,
                platform: process.env.VUE_APP_PLATFORM
              }
            });
            
            if (res.statusCode === 200 && res.data.code === 0) {
              const orderInfo = res.data.data;
              
              // 发起支付
              await paymentService.requestPayment(orderInfo);
              
              // 支付成功
              uni.hideLoading();
              uni.showToast({
                title: '支付成功',
                icon: 'success'
              });
            } else {
              uni.hideLoading();
              uni.showToast({
                title: '获取支付参数失败',
                icon: 'none'
              });
            }
          } catch (error) {
            uni.hideLoading();
            uni.showToast({
              title: '支付失败,请稍后重试',
              icon: 'none'
            });
          }
        }
      }
    };
    </script>
    
    <style>
    .payment-page {
      padding: 20px;
    }
    
    .order-info {
      background-color: #f5f5f5;
      padding: 15px;
      border-radius: 5px;
      margin-bottom: 20px;
    }
    
    .order-item {
      display: flex;
      justify-content: space-between;
      margin-bottom: 10px;
    }
    
    .price {
      color: #ff4d4f;
      font-weight: bold;
    }
    
    .payment-methods {
      background-color: #fff;
      border-radius: 5px;
      margin-bottom: 30px;
    }
    
    .method-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px;
      border-bottom: 1px solid #f0f0f0;
    }
    
    .method-item:last-child {
      border-bottom: none;
    }
    
    .pay-btn {
      width: 100%;
      padding: 12px;
      background-color: #007aff;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 16px;
    }
    
    /* #ifdef MP-WEIXIN */
    /* 微信小程序隐藏支付宝支付 */
    .method-item:nth-child(2) {
      display: none;
    }
    /* #endif */
    
    /* #ifdef MP-ALIPAY */
    /* 支付宝小程序隐藏微信支付 */
    .method-item:nth-child(1) {
      display: none;
    }
    /* #endif */
    </style>

案例三:实现响应式布局适配

功能说明

实现一个响应式布局,在不同屏幕尺寸和平台上都能提供良好的用户体验:

  • App 端:适配不同尺寸的手机和平板
  • 小程序端:适配不同尺寸的手机
  • Web 端:适配 PC 浏览器和移动浏览器

实现步骤

  1. 使用 rpx 单位

    uni-app 推荐使用 rpx 单位进行布局,rpx 会根据屏幕宽度自动调整大小。

    <template>
      <view class="responsive-layout">
        <view class="header">
          <text class="title">响应式布局示例</text>
        </view>
        
        <view class="content">
          <view class="grid-container">
            <view class="grid-item" v-for="item in items" :key="item.id">
              <image :src="item.image" mode="aspectFill" />
              <text class="item-title">{{ item.title }}</text>
            </view>
          </view>
        </view>
        
        <view class="footer">
          <text>© 2023 响应式布局示例</text>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          items: [
            { id: 1, title: '商品 1', image: 'https://example.com/image1.jpg' },
            { id: 2, title: '商品 2', image: 'https://example.com/image2.jpg' },
            { id: 3, title: '商品 3', image: 'https://example.com/image3.jpg' },
            { id: 4, title: '商品 4', image: 'https://example.com/image4.jpg' }
          ]
        };
      }
    };
    </script>
    
    <style>
    .responsive-layout {
      display: flex;
      flex-direction: column;
      min-height: 100vh;
    }
    
    .header {
      height: 80rpx;
      background-color: #007aff;
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 32rpx;
      font-weight: bold;
    }
    
    .content {
      flex: 1;
      padding: 20rpx;
    }
    
    .grid-container {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 20rpx;
    }
    
    .grid-item {
      background-color: #f5f5f5;
      border-radius: 10rpx;
      overflow: hidden;
    }
    
    .grid-item image {
      width: 100%;
      height: 300rpx;
    }
    
    .item-title {
      padding: 15rpx;
      font-size: 28rpx;
    }
    
    .footer {
      height: 60rpx;
      background-color: #f0f0f0;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24rpx;
      color: #666;
    }
    
    /* #ifdef H5 */
    /* Web 端适配 */
    @media (min-width: 768px) {
      .grid-container {
        grid-template-columns: repeat(4, 1fr);
      }
      
      .grid-item image {
        height: 400rpx;
      }
    }
    
    @media (min-width: 1024px) {
      .grid-container {
        grid-template-columns: repeat(6, 1fr);
      }
    }
    /* #endif */
    </style>
  2. 使用条件编译适配不同平台

    <template>
      <view class="platform-adaptation">
        <!-- #ifdef APP-PLUS -->
        <view class="app-specific">
          <text>App 端特有的内容</text>
        </view>
        <!-- #endif -->
        
        <!-- #ifdef MP -->
        <view class="mini-specific">
          <text>小程序端特有的内容</text>
        </view>
        <!-- #endif -->
        
        <!-- #ifdef H5 -->
        <view class="web-specific">
          <text>Web 端特有的内容</text>
        </view>
        <!-- #endif -->
        
        <view class="common-content">
          <text>所有平台都有的内容</text>
        </view>
      </view>
    </template>
    
    <script>
    export default {
      mounted() {
        // 平台判断
        const platform = uni.getSystemInfoSync().platform;
        console.log('当前平台:', platform);
        
        // 根据平台执行不同的逻辑
        if (platform === 'ios') {
          console.log('iOS 平台特有的逻辑');
        } else if (platform === 'android') {
          console.log('Android 平台特有的逻辑');
        } else if (process.env.VUE_APP_PLATFORM === 'h5') {
          console.log('Web 平台特有的逻辑');
        }
      }
    };
    </script>
    
    <style>
    .platform-adaptation {
      padding: 20rpx;
    }
    
    .app-specific {
      background-color: #e3f2fd;
      padding: 20rpx;
      border-radius: 10rpx;
      margin-bottom: 20rpx;
    }
    
    .mini-specific {
      background-color: #e8f5e8;
      padding: 20rpx;
      border-radius: 10rpx;
      margin-bottom: 20rpx;
    }
    
    .web-specific {
      background-color: #fff3e0;
      padding: 20rpx;
      border-radius: 10rpx;
      margin-bottom: 20rpx;
    }
    
    .common-content {
      background-color: #f5f5f5;
      padding: 20rpx;
      border-radius: 10rpx;
    }
    </style>

技术要点总结

  1. 平台差异识别:了解不同平台的运行环境、API、组件、样式和性能差异,是进行多端适配的基础。

  2. 适配策略选择:根据不同的场景和需求,选择合适的适配策略,如条件编译、平台判断、API 适配等。

  3. 条件编译使用:掌握条件编译的语法和最佳实践,合理使用条件编译来处理平台差异。

  4. 响应式布局:使用 rpx 单位和媒体查询,实现不同屏幕尺寸的适配。

  5. 性能优化:根据不同平台的性能特性,调整应用的复杂度和动画效果,确保在所有平台上都能提供良好的用户体验。

  6. 测试验证:在所有目标平台上测试应用,确保适配方案的有效性和稳定性。

  7. 文档化:将多端适配的策略和注意事项文档化,方便团队协作和后续维护。

学习目标

  1. 了解 uni-app 支持的平台及其差异
  2. 掌握 uni-app 的多端适配策略和方法
  3. 学会使用条件编译处理平台差异
  4. 理解如何实现响应式布局适配
  5. 掌握多端适配的最佳实践
  6. 能够开发在所有平台上都能正常运行的应用

章节小结

本章节详细介绍了 uni-app 的多端适配技巧,包括平台差异分析、适配策略和条件编译最佳实践。通过三个实用案例,展示了如何实现平台特定的登录功能、支付功能和响应式布局适配。

在进行多端适配时,开发者需要注意以下几点:

  1. 了解并识别不同平台的差异
  2. 选择合适的适配策略和方法
  3. 合理使用条件编译,避免过度使用
  4. 实现响应式布局,适配不同屏幕尺寸
  5. 根据平台性能差异,调整应用的复杂度和动画效果
  6. 在所有目标平台上测试应用,确保适配方案的有效性和稳定性
  7. 将多端适配的策略和注意事项文档化,方便团队协作和后续维护

通过掌握这些多端适配技巧,开发者可以充分发挥 uni-app 的跨平台优势,开发出在所有平台上都能正常运行并提供良好用户体验的应用。同时,多端适配也是一个持续优化的过程,开发者需要不断学习和实践,才能掌握其中的精髓。

« 上一篇 uni-app 热更新 下一篇 » uni-app 性能监控