likes
comments
collection
share

微信小程序第三方库的分包异步化实践

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

背景

货拉拉搬家小程序是一款搬家服务应用,为用户提供方便、安全和可靠的搬家服务。

微信小程序第三方库的分包异步化实践

小程序官方有约束:主包大小不允许超过 2M。而我们搬家业务较为复杂,一些功能往往需要借助第三方库来实现,下方表格列举了项目中用到的部分第三方库,可以看到体积都不小:

NPM 包名用途体积
tim-wx-sdk腾讯云 IM SDK,基于 WebSocket 实现,提供在线聊天能力接近 500KB
MQTT.js基于 MQTT 协议实现,在我们业务中主要用于服务端推送实时消息给客户端186KB
lottie-miniprogram能够使用 json 动效文件方便的实现动画效果,相较于 gif 拓展性更强且体积相对更小200KB

如果是在首页(主包)中如果引入了大体积的第三方库,就会让本就不富裕的包体积雪上加霜。当主包的体积超出了 2M 限制,此时的小程序将无法上传:

微信小程序第三方库的分包异步化实践

综上,这次要做的事情就是:在不占用主包体积的前提下,主包能够使用这些第三方库。

微信小程序提供了分包异步化能力,就是为了这个场景准备的。

微信小程序第三方库的分包异步化实践

本文接下来以 MQTT.js 这个第三方库为例,来实现分包异步化引入,其他的第三方库也是同理。

开始

按照官方文档的姿势,使用require关键字就能让主包异步引入分包中的代码。

微信小程序第三方库的分包异步化实践

先注册一个名为external-library-mqtt的分包,并在对应文件夹内放置一个空的index.vueMQTT.js的源码文件:

subPackages: [
+  {
+    root: 'pages/external-library-mqtt',
+    pages: [
+      {
+        path: 'index',
+      },
+    ],
+  },
 ]

接下来对着官方文档抄,修改主包内 MQTT 的引入方式:

// src/plugins/mqtt/index.js
- import mqtt from './mqtt.min.js'

+ let mqtt
+ require('../pages/external-library-mqtt/mqtt.min.js', res => {
+   // 这里打印一下 mqtt.min.js 的源码
+   console.log('require mqtt.min.js:\n', res)
+   mqtt = res
+  }, ({mod, errMsg}) => {
+   console.error(`path: ${mod}, ${errMsg}`)
+  })

好了,先编译看看效果再说……

微信小程序第三方库的分包异步化实践

出师未捷身先死,编译报错了。因为我们使用的是第三方开发框架(uni-app),而不是微信小程序的原生语法,第三方框架大都通过 Webpack 编译,静态产物不支持CommonJS模块,所以require关键字无法编译通过。

继续看官方文档,稍微往下翻翻,还有一招:

微信小程序第三方库的分包异步化实践

通过微信小程序提供的requirePlugin方法,主包也可以异步引入插件中的代码,那么把 MQTT.js 放入插件内,一样能够满足需求。

在放手去干之前,我们先来探讨一个话题,在本文的场景下,第三方库是应该放在分包中?还是放在插件中?两者之间有什么区别?

分包 VS 插件

先来对比一下两者的差异

分包插件
单独提审+发版不需要需要
体积限制单个分包体积不能超过 2M,小程序总体积不能超过 20M单个插件体积不能超过 2M
发布限制一个小程序账号(AppID)只能发布一个插件

可以看出插件有不少限制条件;而分包本身就寄托在当前小程序内, 相比起来更加自由。

  • 随着业务发展,公司内各业务线的小程序也逐渐诞生出自己的业务插件,去提供给其他业务线接入。比如:为了获得更好的微信分享能力,出现了一些营销活动类型的插件。

  • 这些业务插件本身就已具备一定的体积,若再把大体积的第三方库也一并放入插件中,已然破坏了插件的单一职责。

  • 如果因此导致插件体积超限,难道那时要再另外申请一个小程序账号,用来发布新的插件?很明显,这不是个好选择。

基于上述的分析,本文所探讨的「第三方库异步引入」如果能闭环在自身小程序内,那是最好不过。所以还是继续探索分包异步化的实现吧。

卷土重来

回到之前编译失败的问题中来,提炼一下目标:编译时跳过require关键字的限制,让它存在于编译产物中。

替换编译产物

uni-app 是通过 Webpack 打包的,而 webpack 也提供了编译流程中的各种 hooks,可以利用其中的emit去替换编译产物中的内容。

微信小程序第三方库的分包异步化实践

还是之前的方法,将原先的require关键字替换为customRequire(名称随意即可)。

// src/plugins/mqtt/index.js
let mqtt
- require('../pages/external-library-mqtt/mqtt.min.js', res => {
+ customRequire && customRequire('../pages/external-library-mqtt/mqtt.min.js', res => {
  console.log('require mqtt.min.js:\n', res)
  mqtt = res
 }, ({mod, errMsg}) => {
  console.error(`path: ${mod}, ${errMsg}`)
 })

上述文件是在主包中引入的,经过编译后会被打包入dist/common/vendor.js中。 接着我们新建一个 Webpack plugin,用来把编译产物中的customRequire还原成require

// 自定义 webpack plugin
export class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      let content = compilation.assets['common/vendor.js'].source();
      content = content.replace(/customRequire/g, 'require');
      compilation.assets['common/vendor.js'] = {
        source() {
          return content;
        },
        size() {
          return content.length;
        },
      };
    });
  }
}

复制文件

我们回忆一下分包文件夹,此时的内容mqtt.min.js并没有被分包中任何文件引用到,所以它不会打包进编译产物中,这里利用copy-webpack-plugin将它复制进dist文件夹对应的分包目录下:

// vue.config.js
+ const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  // ...
  configureWebpack: {
    plugins: [
      new MyPlugin(),
+     new CopyWebpackPlugin([
+       {
+         from: path.join(__dirname, 'src/pages/external-library-mqtt/mqtt.min.js'),
+         to: path.join(__dirname, 'dist', process.env.NODE_ENV === 'production' ? 'build' : 'dev', process.env.UNI_PLATFORM, 'pages/external-library-mqtt'),
+       }
+     ]),
    ],
  },
}

这个时候再次编译一下代码,控制台打印出来MQTT.js的源码了,我们已成功引入了它:

微信小程序第三方库的分包异步化实践

再结合下小程序编译文件的前后对比,我们可以看到MQTT.js即分包 external-library-mqtt 已经从主包中脱离出来,主包体积也相应的减少了 186 KB,说明分包异步化此时已经成功。

微信小程序第三方库的分包异步化实践

大功告……不要大意,好像少了点什么东西。

缓存队列

之前代码中 MQTT 是使用import同步引入,调整之后变为了异步引入,如果在引入完成前,就先调用了 MQTT 提供的方法,那代码就要报错了。设计一个缓存队列来解决它:

// src/plugins/mqtt/index.js

+ const queue = []

+ const mqttReady = () => {
+   const run = () => {
+     const fn = queue.shift()
+     if (typeof fn !== 'function') {
+       return
+     }
+     fn()
+     run()
+   }
+   run()
+ }

customRequire && customRequire('../pages/external-library-mqtt/mqtt.min.js', res => {
  // ...
+ mqttReady()
 }, ...)

+ const beforehook = (fn) => {
+   return (...args) => {
+     if (!mqtt) {
+       queue.push(() => {
+         fn(...args)
+       })
+     } else {
+       fn(...args)
+     }
+   }
+ }

- const login = () => {...}
+ const login = beforehook(() => {...})

至此,分包异步引入完成。

更简单的实现方式

就像“高端的食材往往只需最朴素的烹饪方式”一样,代码功能往往也总会有更为简单粗暴的实现,Webpack 文档中有这么一个好东西:

微信小程序第三方库的分包异步化实践

可以依靠它,来实现我们的目标。

// src/plugins/mqtt/index.js
- customRequire && customRequire('../pages/external-library-mqtt/mqtt.min.js', res => {
+ __non_webpack_require__ && __non_webpack_require__('../pages/external-library-mqtt/mqtt.min.js', res => {
    // ...
 }, ({mod, errMsg}) => {
    // ...
 })

...

编译后,查看dist/common/vendor.js文件,Webpack 将 __non_webpack_require__编译成了require,省去了我们自己写 Webpack plugin 的步骤。

微信小程序第三方库的分包异步化实践

异常考虑

失败率

项目中的代码逻辑:小程序仅在冷启动(onLaunch)时才会去异步引入分包内的 MQTT.js

在神策指标面板中,以小程序冷启动的次数作为分母,require的失败回调次数作为分子,可以看到分包异步加载的失败率在 0.02 ~ 0.03% 之间,趋势比较稳定。

微信小程序第三方库的分包异步化实践

虽说失败率已经很低了,但作为技术优化来说,追求极致的路还没有走到头,我们可以引入重试机制,让失败率进一步降低。

重试机制

我们来做一套重试机制:当分包异步引入失败时,延时一定时间后再次引入。

顺便再将代码整合一下:

// src/plugins/mqtt/index.js
- __non_webpack_require__ && __non_webpack_require__('../pages/external-library-mqtt/mqtt.min.js', res => {
-  console.log('require mqtt.min.js:\n', res)
-  mqtt = res
- }, ({mod, errMsg}) => {
-  console.error(`path: ${mod}, ${errMsg}`)
- })

/**
 * MQTT.js 异步引入
 * @param {number} retry 重试次数
 * @return {Promise}
 */
+ const asyncRequire = (retry) => {
+  let _retry = retry
+
+  const loadError = async(error, resolve) => { ... }
+
+  const loadResource = (resolve) => {
+    __non_webpack_require__.async('../pages/external-library-mqtt/mqtt.min.js')
+      .then(...).catch(...)
+  }
+
+  return new Promise((resolve) => {
+    loadResource(resolve)
+  })
+ }
+
+ const { error, res } = await asyncRequire(3)
+ if (res) mqtt = res

加入了重试机制后,分包异步的失败率降至 0.003%。

项目收益

原先项目中因为腾讯云 IM SDK 体积过大(接近 500KB),所以不得不将其从主包移动至分包内,白白增加了分包体积。

有了分包异步化能力之后,我们针对此进行了改造,将腾讯云 IM SDK 从分包中移除,在主包异步引入:

微信小程序第三方库的分包异步化实践

至此,在不影响主包体积的情况下,将庞大的第三方库从分包中移除后,也能保证功能的正常使用。优化后的代码在发布后,通过前后对比,分包的下载耗时也得到了提升:

分包名称优化前平均耗时优化后平均耗时耗时缩短(百分比)
/pages/message/ (消息中心页)750ms620ms17%
/pages/orders/ (订单详情页)800ms670ms16%

详见下图(出自:小程序开发者后台-统计-性能数据面板):

微信小程序第三方库的分包异步化实践

总结

本文结合实际业务场景,针对小程序开发中“主包体积不够用”这一大痛点,以“分包异步引入”为话题进行了探索。

在第三方开发框架(uni-app/Taro 等)官方仅能支持插件异步引入的情况下,实现了另一套可行、稳定且更佳的方案,增强了小程序拓展性的同时,也提升了分包页面的载入性能,带来额外项目收益。

通过项目中埋点数据反馈,加入了后续的重试机制,这套分包异步化方案的失败率可达到万分之 0.3,能放心使用。

转载自:https://juejin.cn/post/7251833062618513466
评论
请登录