likes
comments
collection
share

分包异步化在货拉拉微信小程序中的实践

作者站长头像
站长
· 阅读数 10

背景

货拉拉微信小程序经过多年迭代,代码包越来越大,主包大小接近 2M,增加一些大的业务功能,就有可能超过大小限制,无法上传代码。

分包异步化在货拉拉微信小程序中的实践

本文适用于使用第三方框架开发的微信小程序,例如:Uniapp、Taro 等。如果使用微信原生语法开发,可以参考本文思路,以及微信官方文档教程。

本文着重关注优化主包大小,分包还不太可能超过 2M。

理论

微信官方建议性能优化中关于代码体积优化有以下 4 个方向:

1.合理使用分包加载

非主包资源都可以放到分包中,保证主包资源最快时间让用户可以访问,分包的资源可以按照重要性开启分包预下载

2.避免非必要的全局自定义组件和插件

如题,非必要(组件功能单一、只有 1 个页面引入等)就不要把组件写在主包,拆进分包。多个分包都同时引用的资源,放在分包里则不合适,分包没法直接访问另一个分包的资源,除非两个分包都已经加载过了。这个问题可以使用「分包异步化」解决,这是本文重点,下文再表。

3.控制代码包内的资源文件

图片、字体文件建议尽量都走 CDN,小程序的 WXSS 中图片资源没法访问本地路径,也是建议把图片资源放在 CDN 上,这些就是替换路径的工作,很简单。

4.及时清理无用代码和资源

删代码,还有比这更开心的事情吗。

分析

货拉拉小程序在迭代中有持续优化的动作,页面和组件能分包的基本都拆出去了,剩下都是一些拆不动的。

分包异步化在货拉拉微信小程序中的实践

信息点:

  1. 图片资源可以迁 CDN,预计可以 -178KB。
  2. vendor.js 大,主要包含全局函数库 JS 以及主包用到的 JS 资源。

分包异步化在货拉拉微信小程序中的实践

由上图简单分析可以得出,需要移除 tim-wx.js,这是腾讯 IMSDK,主包依赖,需要在主包进行 SDK 初始化登录,无法拆进分包。

解决方案

基于以上分析,2 个关键信息:

  1. 为使用 IMSDK 提供的登录 API,腾讯 IMSDK 必须在小程序主包初始化
  2. IMSDK 大小是 420KB,代码已经是压缩过的,无法 Tree Shaking。

方案 1:分包异步化

在小程序中,不同的分包对应不同的下载单元;因此,除了非独立分包可以依赖主包外,分包之间不能互相使用自定义组件或进行 require。「分包异步化」特性将允许通过一些配置和新的接口,使部分跨分包的内容可以等待下载后异步使用,从而一定程度上解决这个限制。

如果小程序使用的是原生语法,可以使用「方案 1 」。如使用三方框架构建,例如:Uniapp、Taro 等,暂无法直接使用「方案 1」 ,或者说使用方式太过繁琐,不建议在生产使用。

跟着微信官方文档编写Demo:

  1. 首先把 tim-wx-sdk 放进分包里面
// subPackageA/index.js
import TIM from 'tim-wx-sdk';
export default TIM;

2.封装一下 utils,在主包使用

// utils/im.js
let TIM = {};
require
  .async('subPackageA/index')
  .then((mod) => {
    TIM = mod;
  })
  .catch(({ mod, errMsg }) => {
    console.error(`subPackageA path: ${mod}, ${errMsg}`);
  });
// 导出该 SDK
export { TIM };

3.在主包使用,因为 require.async 是异步函数,要注意使用 TIM 的时机,必须是异步回调之后才能执行,运行 Demo 的时候可以先延时用来验证。

// app.js
import { TIM } from '@/utils/im';
// 先延时等待 require.async 运行结束
setTimeout(() => {
  TIM.login({
    userID: 'xxxxx',
    userSig: 'userSig',
  });
}, 3000);

4.运行,毫无意外,它报错了。

分包异步化在货拉拉微信小程序中的实践

根本原因: module.exports、require 属于 CommonJS 语法,CommonJSNode.js 采用的模块化规范,而浏览器环境下不支持该语法,所以没有这些变量。所以,Uniapp 编译打包的时候会把 require 编译成 __webpack_require__,而微信小程序运行宿主环境并没有 __webpack_require__ 这个方法,所以报错。

require 微信小程序环境是支持直接运行的,不需要经过编译,我们只需要让 Uniapp 框架不编译 require.async 这个函数即可,经过编写各种 demo 和 Issues 查询,得出以下结论:

  1. Uniapp 官方编译脚本还未计划支持该小程序特性。
  2. non_webpack_require 方案也涉及到打包脚本的一些改动,理解起来也很复杂,不适宜在生产环境中大改特改。

三方框架无法直接使用分包异步化,微信原生语法无影响。如一定要在第三方框架中使用「异步化」这个特性,可以使用「方案 2」。

方案 2:分包插件异步化

微信小程序提供插件能力,具体功能可以参考官方文档requirePlugin 是微信官方提供给插件之间互相调用的一个方法。

requirePlugin 官方用法示例:

// 使用回调函数风格的调用
requirePlugin(
  'live-player-plugin',
  (livePlayer) => {
    console.log(livePlayer.getPluginVersion());
  },
  ({ mod, errMsg }) => {
    console.error(`path: ${mod}, ${errMsg}`);
  },
);
// 或者使用 Promise 风格的调用
requirePlugin
  .async('live-player-plugin')
  .then((livePlayer) => {
    console.log(livePlayer.getPluginVersion());
  })
  .catch(({ mod, errMsg }) => {
    console.error(`path: ${mod}, ${errMsg}`);
  });

如上,加载插件用的是 requirePluginWebpack 打包不会编译这个方法。

回溯一下「方案一」中遇到的问题是:require 会被 Webpack 打包,编译成 __webpack_require__,所以造成小程序宿主环境没有 require 分包的能力。

「方案二」的解决思路:微信官方提供的 requirePluginWebpack 不会进行编译,则可以正常访问小程序宿主环境的的 requirePlugin API,从而达成异步加载异步的目的。

  1. 既然要插件,先去微信官方注册一个插件,这部分可以搜官方文档,主要代码如下:
// xxx-plugin/index.js
// 插件代码只是加载 SDK,并且导出
import TIM from 'tim-wx-sdk';
module.exports = {
  TIM,
};

2.在分包页面中引入插件。

{
  "plugins": {
    "xxx-plugin": {
      "version": "dev-01055b63731de071ffb850464bd5c7b1",
      "provider": "xxx-plugin appid"
    }
  }
}

3.上面的 utils 封装改一下。

// utils/im.js
let TIM = null;
requirePlugin
  .async('xxx-plugin')
  .then(({ TIM: modTIM }) => {
    TIM = modTIM;
  })
  .catch(({ mod, errMsg }) => {
    console.error(`direct-service-plugin path: ${mod}, ${errMsg}`);
  });
// 暴露出去
export { TIM };

4.完事,好起来了。

顶层 await

这篇文章写得很清楚了,就不搬了。

「方案 2」确实是能够正常解决加载问题,但是引入了新的问题,上面的写法 requirePlugin.async 是个异步函数,调用 TIM 时机不同获取到的值不一样,使用 utils/im 的时候,还需要判断一下是否存在,还需要等待它加载。

import { TIM } from '@/uitls/im';
// 用的时候
TIM && TIM.login();

那比较好一点的方法是加 Promise,每个使用的地方等待一下

1.加载插件的封装

// @/utils/async-load.ts
/**
 * 加载插件的方法
 * @param pluginName
 * @returns Promise<any>
 */
export async function loadPluginPackage(pluginName: string): Promise<any> {
  try {
    // @ts-ignore
    const mod = await requirePlugin.async(pluginName);
    return mod;
  } catch ({ mod, errMsg }) {
    console.error(
      `loadPluginPackage '${pluginName}' errpr path: ${mod}, ${errMsg}`,
    );
    return {};
  }
}

2.使用的时候

// app.js
;(async init() {
  const { TIM } = await loadPluginPackage('xxx-plugin')
  TIM.login({
    userID: 'xxxxx',
    userSig: 'userSig',
  })
})()

这样还是有问题,如果有多个地方都是用这个 JS,每个地方都要写一下加载插件的方法,可以在小程序启动的时候做一次加载就可以,后面所有用到的地方都用同一个 Promise 就行。

3.更建议的方式

// utils/im.js
let TIM = {};
import { loadPluginPackage } from '@/utils/async-load';
const TIMSdk = loadPluginPackage('xxx-plugin');
TIMSdk.then((mod) => {
  TIM = mod.TIM;
});
// 导出出去
export { TIM, TIMSdk };

TIMSdk,是一个默认执行一次的 Promise,加载过一次之后,后续调用 TIMSdk 拿到的都是同一个结果。

使用方式

// app.js
import { TIM, TIMSdk } from '@/utils/im';
/**
 *
 * IM初始化
 */
const init = async () => {
  // 要用的时候 await 一下即可
  await TIMSdk;
  const tim = TIM.create({
    SDKAppID: config.SDKAppID,
  });
};
init();

这样用起来很不方便,而且如果使用的地方如果二次封装,也很麻烦,目前没有很好的方法,顶层 await 是为了解决这个问题的,这个提案目前我们还用不上。

4.完美方式,顶部直接await,暂时用不上

// app.js
const { TIM } = await loadPluginPackage('xxx-plugin');

基础库兼容性

分包异步化在货拉拉微信小程序中的实践

「分包异步化」这个功能需要小程序基础库 2.11.2 及以上才支持,可以按照官方推荐,设置最低基础库。

如果目前线上基础库设置比这个低,需要拉一下现在线上的基础库分布,看一下提高基础库会影响多少人,如果比例很小,可以强行设置,提升用户体验

总结

小程序此次优化结果:

分包异步化在货拉拉微信小程序中的实践

主包大小从 1.83M 降到 1.36M,大小减少 25%。

一定要注意基础库的依赖,要用这个方案,必须要把最低基础库限制拉到 2.11.2

控制小程序代码包大小主要几个手段:

  1. 静态资源,能走 CDN 的,全部走 CDN。

  2. 能分包的页面或者组件,全部放到分包里面去,主包只留不能拆分的,提升分包加载速度可以使用开启分包预下载

  3. 如果资源一定要在主包引用且大小不可控,那就使用「分包异步化」或者「分包插件异步化」来处理。「分包异步化」和「分包插件异步化」两者的选择建议:

    • 如果用的是第三方编译的小程序框架,例如: Uniapp,用不上「分包异步化」,等三方官方支持
    • 分包插件异步化和分包异步化写法差不多,坏处是需要发一个微信小程序插件,好处是小程序是跨端编译到其它端也可以走插件这一套逻辑。