uni-app 小程序分包加载

核心知识点

1. 分包加载原理

小程序分包加载是一种优化小程序性能的技术,主要原理包括:

  • 主包:包含默认启动页面、TabBar 页面以及所有分包都需要用到的公共资源和代码
  • 分包:根据功能模块划分为不同的包,按需加载
  • 加载机制:首次启动只加载主包,进入分包页面时才加载对应分包
  • 体积限制:主包体积不超过 2MB,单个分包体积不超过 2MB,总体积不超过 20MB

2. 分包配置方法

在 uni-app 中,分包配置主要通过 pages.json 文件实现:

  • subPackages:配置分包路径和页面
  • preloadRule:配置分包预加载规则
  • independent:配置独立分包

3. 分包优化策略

有效的分包优化策略包括:

  • 合理划分模块:根据功能相关性和使用频率划分分包
  • 公共资源管理:将公共组件和资源放在主包中
  • 预加载优化:根据用户行为预加载可能需要的分包
  • 体积监控:定期检查各包体积,避免超限

实用案例分析

案例:实现小程序分包加载

1. 项目结构设计

假设我们有一个电商应用,包含以下功能模块:

  • 主包:首页、分类、购物车、我的(TabBar 页面)
  • 分包 1:商品详情、订单相关功能
  • 分包 2:用户中心、设置相关功能
  • 分包 3:活动、营销相关功能

2. 分包配置实现

pages.json 配置文件

{
  "pages": [
    // 主包页面
    {"path": "pages/index/index", "style": {}}, // 首页
    {"path": "pages/category/category", "style": {}}, // 分类
    {"path": "pages/cart/cart", "style": {}}, // 购物车
    {"path": "pages/user/user", "style": {}} // 我的
  ],
  "subPackages": [
    // 分包 1:商品与订单
    {
      "root": "pages/goods",
      "pages": [
        {"path": "detail/detail", "style": {}},
        {"path": "order/confirm", "style": {}},
        {"path": "order/list", "style": {}},
        {"path": "order/detail", "style": {}}
      ]
    },
    // 分包 2:用户中心
    {
      "root": "pages/profile",
      "pages": [
        {"path": "settings/settings", "style": {}},
        {"path": "address/list", "style": {}},
        {"path": "address/edit", "style": {}},
        {"path": "coupon/list", "style": {}}
      ]
    },
    // 分包 3:活动营销
    {
      "root": "pages/activity",
      "pages": [
        {"path": "seckill/seckill", "style": {}},
        {"path": "group/group", "style": {}},
        {"path": "coupon-center/coupon-center", "style": {}}
      ]
    }
  ],
  "preloadRule": {
    // 进入首页时预加载商品分包
    "pages/index/index": {
      "network": "all",
      "packages": ["pages/goods"]
    },
    // 进入分类页时预加载商品分包
    "pages/category/category": {
      "network": "all",
      "packages": ["pages/goods"]
    },
    // 进入购物车时预加载订单分包
    "pages/cart/cart": {
      "network": "all",
      "packages": ["pages/goods"]
    },
    // 进入我的页面时预加载用户中心分包
    "pages/user/user": {
      "network": "all",
      "packages": ["pages/profile"]
    }
  },
  "tabBar": {
    "color": "#999",
    "selectedColor": "#007AFF",
    "borderStyle": "black",
    "backgroundColor": "#fafafa",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/home.png",
        "selectedIconPath": "static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/category/category",
        "text": "分类",
        "iconPath": "static/tabbar/category.png",
        "selectedIconPath": "static/tabbar/category-active.png"
      },
      {
        "pagePath": "pages/cart/cart",
        "text": "购物车",
        "iconPath": "static/tabbar/cart.png",
        "selectedIconPath": "static/tabbar/cart-active.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "static/tabbar/user.png",
        "selectedIconPath": "static/tabbar/user-active.png"
      }
    ]
  },
  "globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "电商应用",
    "navigationBarBackgroundColor": "#f8f8f8",
    "backgroundColor": "#f8f8f8"
  }
}

3. 分包页面跳转实现

首页跳转到商品详情 (pages/index/index.vue):

<template>
  <view class="index-container">
    <view class="goods-list">
      <view v-for="goods in goodsList" :key="goods.id" class="goods-item">
        <image :src="goods.image" class="goods-image"></image>
        <view class="goods-info">
          <text class="goods-name">{{ goods.name }}</text>
          <text class="goods-price">¥{{ goods.price }}</text>
          <button @click="goToDetail(goods.id)" type="primary" size="mini">查看详情</button>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      goodsList: [
        { id: 1, name: '商品1', price: 99, image: 'https://via.placeholder.com/200' },
        { id: 2, name: '商品2', price: 199, image: 'https://via.placeholder.com/200' },
        { id: 3, name: '商品3', price: 299, image: 'https://via.placeholder.com/200' }
      ]
    };
  },
  methods: {
    goToDetail(goodsId) {
      uni.navigateTo({
        url: '/pages/goods/detail/detail?id=' + goodsId
      });
    }
  }
};
</script>

<style scoped>
.index-container {
  padding: 20rpx;
}

.goods-list {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
}

.goods-item {
  display: flex;
  background-color: #fff;
  border-radius: 10rpx;
  padding: 20rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.goods-image {
  width: 160rpx;
  height: 160rpx;
  border-radius: 5rpx;
}

.goods-info {
  flex: 1;
  margin-left: 20rpx;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.goods-name {
  font-size: 32rpx;
  font-weight: 500;
  margin-bottom: 10rpx;
}

.goods-price {
  font-size: 36rpx;
  color: #ff4d4f;
  margin-bottom: 10rpx;
}
</style>

商品详情页 (pages/goods/detail/detail.vue):

<template>
  <view class="detail-container">
    <view class="goods-image">
      <image :src="goodsDetail.image" class="image"></image>
    </view>
    <view class="goods-info">
      <text class="goods-name">{{ goodsDetail.name }}</text>
      <text class="goods-price">¥{{ goodsDetail.price }}</text>
      <text class="goods-desc">{{ goodsDetail.description }}</text>
    </view>
    <view class="action-bar">
      <button @click="addToCart" type="default">加入购物车</button>
      <button @click="buyNow" type="primary">立即购买</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      goodsDetail: {
        id: '',
        name: '',
        price: 0,
        image: '',
description: ''
      }
    };
  },
  onLoad(options) {
    this.getGoodsDetail(options.id);
  },
  methods: {
    getGoodsDetail(id) {
      // 模拟获取商品详情
      this.goodsDetail = {
        id: id,
        name: '商品' + id,
        price: 99 * id,
        image: 'https://via.placeholder.com/400',
description: '这是商品' + id + '的详细描述,包含商品的各种参数和特点。'
      };
    },
    addToCart() {
      uni.showToast({ title: '已加入购物车', icon: 'success' });
    },
    buyNow() {
      uni.navigateTo({
        url: '/pages/goods/order/confirm?id=' + this.goodsDetail.id
      });
    }
  }
};
</script>

<style scoped>
.detail-container {
  padding-bottom: 100rpx;
}

.goods-image {
  width: 100%;
  height: 500rpx;
  background-color: #f5f5f5;
}

.image {
  width: 100%;
  height: 100%;
}

.goods-info {
  padding: 20rpx;
  background-color: #fff;
  margin-bottom: 20rpx;
}

.goods-name {
  font-size: 36rpx;
  font-weight: bold;
  margin-bottom: 20rpx;
}

.goods-price {
  font-size: 40rpx;
  color: #ff4d4f;
  margin-bottom: 20rpx;
}

.goods-desc {
  font-size: 28rpx;
  color: #666;
  line-height: 1.5;
}

.action-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  background-color: #fff;
  padding: 20rpx;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.action-bar button {
  flex: 1;
  margin: 0 10rpx;
}
</style>

订单确认页 (pages/goods/order/confirm.vue):

<template>
  <view class="confirm-container">
    <view class="order-info">
      <text class="title">订单确认</text>
      <view class="goods-item">
        <text class="goods-name">商品{{ goodsId }}</text>
        <text class="goods-price">¥{{ price }}</text>
      </view>
      <view class="address-info">
        <text class="label">收货地址</text>
        <view class="address-detail">
          <text>请选择收货地址</text>
          <uni-icon type="arrowright" size="24"></uni-icon>
        </view>
      </view>
      <view class="payment-info">
        <text class="label">支付方式</text>
        <view class="payment-options">
          <view class="option" :class="{ active: paymentMethod === 'wechat' }" @click="paymentMethod = 'wechat'">
            <text>微信支付</text>
            <uni-icon v-if="paymentMethod === 'wechat'" type="checkbox-filled" color="#007AFF"></uni-icon>
            <uni-icon v-else type="checkbox" color="#999"></uni-icon>
          </view>
          <view class="option" :class="{ active: paymentMethod === 'alipay' }" @click="paymentMethod = 'alipay'">
            <text>支付宝</text>
            <uni-icon v-if="paymentMethod === 'alipay'" type="checkbox-filled" color="#007AFF"></uni-icon>
            <uni-icon v-else type="checkbox" color="#999"></uni-icon>
          </view>
        </view>
      </view>
    </view>
    <view class="total-bar">
      <text class="total-label">合计:</text>
      <text class="total-price">¥{{ price }}</text>
      <button @click="submitOrder" type="primary">提交订单</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      goodsId: '',
      price: 0,
      paymentMethod: 'wechat'
    };
  },
  onLoad(options) {
    this.goodsId = options.id;
    this.price = 99 * this.goodsId;
  },
  methods: {
    submitOrder() {
      uni.showToast({ title: '订单提交成功', icon: 'success' });
      setTimeout(() => {
        uni.navigateTo({
          url: '/pages/goods/order/list/list'
        });
      }, 1500);
    }
  }
};
</script>

<style scoped>
.confirm-container {
  padding-bottom: 120rpx;
}

.order-info {
  background-color: #fff;
  margin-bottom: 20rpx;
  padding: 20rpx;
}

.title {
  font-size: 36rpx;
  font-weight: bold;
  margin-bottom: 30rpx;
}

.goods-item {
  display: flex;
  justify-content: space-between;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #eee;
}

.goods-name {
  font-size: 32rpx;
}

.goods-price {
  font-size: 32rpx;
  color: #ff4d4f;
}

.address-info,
.payment-info {
  margin-top: 30rpx;
}

.label {
  font-size: 28rpx;
  color: #666;
  margin-bottom: 15rpx;
  display: block;
}

.address-detail {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx;
  background-color: #f5f5f5;
  border-radius: 5rpx;
}

.payment-options {
  display: flex;
  flex-direction: column;
  gap: 20rpx;
}

.option {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx;
  background-color: #f5f5f5;
  border-radius: 5rpx;
}

.option.active {
  background-color: #e6f7ff;
}

.total-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  background-color: #fff;
  padding: 20rpx;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.total-label {
  font-size: 32rpx;
}

.total-price {
  font-size: 36rpx;
  color: #ff4d4f;
  font-weight: bold;
  margin: 0 20rpx;
  flex: 1;
}

.total-bar button {
  flex: 0 0 200rpx;
}
</style>

4. 独立分包配置

独立分包是一种特殊的分包,可以独立于主包运行,主要用于:

  • 快速启动:用户可以直接进入独立分包,无需加载主包
  • 减少主包体积:将一些不依赖主包的功能模块配置为独立分包

pages.json 中配置独立分包

{
  "subPackages": [
    {
      "root": "pages/independent",
      "pages": [
        {"path": "login/login", "style": {}}
      ],
      "independent": true
    }
  ]
}

独立分包登录页面 (pages/independent/login/login.vue):

<template>
  <view class="login-container">
    <view class="login-form">
      <image src="../../static/logo.png" class="logo"></image>
      <view class="form-item">
        <input v-model="form.username" placeholder="请输入用户名" />
      </view>
      <view class="form-item">
        <input v-model="form.password" type="password" placeholder="请输入密码" />
      </view>
      <button @click="login" type="primary">登录</button>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      form: {
        username: '',
        password: ''
      }
    };
  },
  methods: {
    login() {
      // 登录逻辑
      uni.showToast({ title: '登录成功', icon: 'success' });
      setTimeout(() => {
        // 登录成功后跳转到主包页面
        uni.switchTab({ url: '/pages/index/index' });
      }, 1500);
    }
  }
};
</script>

<style scoped>
.login-container {
  padding: 40rpx;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #f5f5f5;
}

.login-form {
  width: 100%;
  max-width: 500rpx;
  background-color: #fff;
  padding: 40rpx;
  border-radius: 10rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}

.logo {
  width: 160rpx;
  height: 160rpx;
  margin: 0 auto 40rpx;
  display: block;
}

.form-item {
  margin-bottom: 30rpx;
}

.form-item input {
  border: 1rpx solid #ddd;
  border-radius: 5rpx;
  padding: 20rpx;
  font-size: 32rpx;
}

.login-form button {
  margin-top: 20rpx;
}
</style>

学习目标

通过本章节的学习,你应该能够:

  1. 理解分包加载原理:掌握小程序分包加载的工作机制和体积限制
  2. 掌握分包配置方法:能够在 uni-app 中正确配置分包和预加载规则
  3. 实现分包优化:根据项目特点合理划分分包,优化应用性能
  4. 使用独立分包:了解独立分包的使用场景和配置方法
  5. 监控分包体积:定期检查分包体积,避免超限问题

总结

小程序分包加载是解决小程序体积过大、提升启动速度的有效方法。通过合理划分主包和分包,按需加载资源,可以显著提升用户体验。

在实际项目中,我们需要注意以下几点:

  1. 合理划分模块:根据功能相关性和使用频率划分分包,避免分包过大或过小
  2. 优化预加载策略:根据用户行为路径配置预加载规则,提升页面切换速度
  3. 公共资源管理:将公共组件和资源放在主包中,避免重复加载
  4. 独立分包使用:对于一些不依赖主包的功能,如登录页面,可以配置为独立分包
  5. 体积监控:定期检查各包体积,及时优化,避免超限

通过本章节的学习和实践,你应该能够熟练掌握 uni-app 小程序分包加载的配置和优化方法,为用户提供更快速、更流畅的应用体验。

« 上一篇 uni-app 云开发实战 下一篇 » uni-app 小程序性能优化