Taro 微信小程序-通过在分包中实现自定义Tabbar的方式减少主包体积

avatar
web前端开发 @CVTE

注意:本文章主要针对的场景是:

1、tabbar使用微信默认的配置(只要tabbar在主包内都算);

2、主包超出限制大小的情况。

一、背景

由于微信小程序的包体积限制,我相信很多不断迭代的微信小程序最终都会面临一个很棘手的问题-包体积溢出的问题。

我们的项目也不例外,随着版本的不断更新迭代,我们的包体积大小目前即将出现超出微信小程序的包体积限制的严重问题。

当问题出现时我尝试去寻找一个好的方案帮助我解决目前项目中所遇到的困境,但很遗憾,在社区中提出的大部分解决方法都对我们的项目无效,主要原因是大部分的手段我们的都已经应用在了我们的项目中,也就是说目前我们的纯业务代码就已经快超出了2M限制。

tips:

目前小程序分包大小有以下限制:

  • 整个小程序所有分包大小不超过 20M
  • 单个分包/主包大小不能超过 2M

二、问题分析

参考:微信小程序分包

先看图:

image.png

项目结构简介:

1个主包 + 15个分包(不是本次讨论重点,不做过多详细介绍);

从上图可以看到我们的小程序在经过压缩后代码总体积为5410KB,主包体积为2017KB,处于体积即将超限的情况下。

主包之所以这么大,主要原因是我们采用了微信的tabbar的方案来实现我们的业务功能, 虽然使用起来很方便,但是它具有一个致命缺陷 - tabbar页面必须在主包中去实现。

而巧合的是我们由于业务需要,所以很多东西都必须在tabbar页面中去展示,这就使我们不得已在tabbar页面实现了很多的业务逻辑,直接导致了我们的主包体积余额严重不足。

主包功能分析:主要可归为两类

1、Taro产生的编译文件(这个我们无法缩减,不做讨论);

2、公共模块的实现

  • 公共组件
  • 工具函数
  • 其他的公共数据
  • tabbar页面的实现

3、小程序入口

是整个小程序的入口,主要负责处理相关数据初始化、扫码跳转处理、公众号跳转处理等方式;

tips:其中扫码 + 公众号的跳转处理指的是我们所有的二维码和公众号的初始跳转路径都必须是该页面;在该页面对于数据做处理时会根据他们携带的参数去做相应的页面跳转等功能。这样做的好处是为了便于管理,保证我们的小程序只有一个入口,这对于我们的维护成本来说其实是很重要的。

三、方案探索

结合上述的问题分析,我们得出一个结论就是:目前可以优化的点只剩下了tabbar页面;因此下面的方案我们主要围绕tabbar页面的实现来展开。

针对主包的功能,由于小程序分包限制,所以我们对它最好的定位应该是存放一些公共模块的实现,而并非是将我们的业务逻辑参杂其中。

既然有这个问题,那可能有人就好奇以下的解决方案是否更好;

1、为什么不将tabbar页面的放在分包中去呢?

看看本项目的tabbar配置,这个path确实是可以配置的,但是由于微信小程序规定了tabbar页面的实现必须在主包中去实现(参考:小程序分包限制),因此此方案不可行

image.png

2、看资料微信小程序是有提供自定义tabbar的实现的,为什么不采用自定义的方式去实现呢?

参考:自定义tabbar

其实这快的自定义tabbar的实现只是扩展了原有tabbar组件的一些个性化的功能,在使用时因为小程序的分包限制(参考:小程序分包限制)导致我们还是只能在主包中去实现tabbar组件

3、为什么不能将tabbar页面中使用的组件放在分包中,主包去引用呢?

同样参考:小程序分包限制 image.png

补充说明:个人拙见其实对于微信小程序的开发形式来说,最好的交互应该是在tabbar页面添加对应的功能入口即可。其余的功能实现都放在二级页面中,这样我们就可以将具体的业务代码放在分包中去实现了。

4、“自定义”tabbar页面的实现

这里的自定义并非上面所提到的微信小程序提供的自定义方案,而是在分包中去自定义我们的tabbar页面的实现,简单来说就是将原有的tabbar页面的业务部分全部抽离到一个新的分包,主包中只保留公共模块以及程序入口的实现即可。因为本质上tabbar页面的功能也是类似于一个菜单的实现罢了,只不过我们的要做的只是将这个菜单从原来程序自带的实现,变为我们自定义的实现而已。

四、具体实现

根据我们的项目来说实现的大致流程如下图:

image.png 注:没错就是标注的那样, 我们是在一个项目中(同样的账号体系)实现了两种不同的业务。每一个业务除了帐号体系一样,在功能和tabbar部分是完全不同的。

这里实现的tabbar与之前的不同就仅仅在于“机型判断”后的跳转页面不再是程序自带的tabbar页面,而是我们在分包之中自定义实现的tabbar页面。

自定义tabbar的实现:

注意:这里我就拿我们业务的其中一个去举例了,以下的实现都是在分包之中去实现的。 直接看图:

image.png 相关参数定义:

image.png

结合上图我们可以看到,我们其实是在分包中新增了一个关于该业务的tabbar页面,然后在该页面中去以组件的的形式去引用之前的四个相应的“tabbar页面” + 自定义的Tabbar组件,四个组件的关系通过selectTabbar值分别关联起来,这样就可以实现一个和原生体验没有差别的、真正意义上的“自定义tabbar页面”。

Tabbar组件实现代码:

function Tabbar() {
  const dispatch = useDispatch();
  // 当前选中的tabbar
  const selectTabBar: string = useSelector((state: any) => state.tabbar.selectTabBar);
  // 是否显示tabbar组件,对应原生的Taro.hideTabBar() || Taro.showTabBar()
  const isShowTabBar: boolean = useSelector((state: any) => state.tabbar.isShowTabBar);
  const dispatchSetSelectTabbar = (params: string) => {
    dispatch(setSelectTabBar(params));
  };

  const setTabBar = (tabbar: string) => {
    if (selectTabBar !== tabbar) {
      dispatchSetSelectTabbar(tabbar);
    }
  };

  return isShowTabBar ? (
    <View className="tabbar-wrapper">
      <View className="device item-tabbar" onClick={() => setTabBar(SSS_TAB_BAR.DEVICE)}>
        <Image
          src={`${TABBAR_ICON_URL[SSS_TAB_BAR.DEVICE][selectTabBar === SSS_TAB_BAR.DEVICE ? 'active' : 'normal']}`}
          mode="aspectFit"
          className="device-picture"
        />
        <View className={`${selectTabBar === SSS_TAB_BAR.DEVICE ? 'text-active' : ''} tabbar-text`}>第一个tabbar</View>
      </View>
      <View className="parent-child item-tabbar" onClick={() => setTabBar(SSS_TAB_BAR.PARENT_CHILD)}>
        <Image
          src={`${
            TABBAR_ICON_URL[SSS_TAB_BAR.PARENT_CHILD][selectTabBar === SSS_TAB_BAR.PARENT_CHILD ? 'active' : 'normal']
          }`}
          mode="aspectFit"
          className="device-picture"
        />
        <View className={`${selectTabBar === SSS_TAB_BAR.PARENT_CHILD ? 'text-active' : ''} tabbar-text`}>
          第二个tabbar
        </View>
      </View>
      <View className="app-store item-tabbar" onClick={() => setTabBar(SSS_TAB_BAR.APP_STORE)}>
        <Image
          src={`${
            TABBAR_ICON_URL[SSS_TAB_BAR.APP_STORE][selectTabBar === SSS_TAB_BAR.APP_STORE ? 'active' : 'normal']
          }`}
          mode="aspectFit"
          className="device-picture"
        />
        <View className={`${selectTabBar === SSS_TAB_BAR.APP_STORE ? 'text-active' : ''} tabbar-text`}>第三个tabbar</View>
      </View>
      <View className="my item-tabbar" onClick={() => setTabBar(SSS_TAB_BAR.MY)}>
        <Image
          src={`${TABBAR_ICON_URL[SSS_TAB_BAR.MY][selectTabBar === SSS_TAB_BAR.MY ? 'active' : 'normal']}`}
          mode="aspectFit"
          className="device-picture"
        />
        <View className={`${selectTabBar === SSS_TAB_BAR.MY ? 'text-active' : ''} tabbar-text`}>第四个tabbar</View>
      </View>
    </View>
  ) : (
    <View />
  );
}
export default Tabbar;

为了便于管理,我们又两个规定: 1、在tabbar页面中切换tabbar的选中状态,直接修改当前Tabbar组件的state值即可; 2、其他页面要修改就通过路由传参的方式去完成(页面初始化的时候会在组件内部修改state值);

上面的使用规范使得我们的tabbar选中值只在自己的组件中被修改,这样也便于我们的对于状态的维护,防止多处地方同时修改state值导致的tabbar选中异常问题出现。

五、修改风险

之前由于使用的是小程序自带的tabbar实现,所以在跳转tabbar页面时我们使用的都是Taro.switchTab()方法去实现的;

而在我们新的实现方式中我们就要修改原有的跳转方式;

为了防止用户的系统返回键导致其他异常问题,而且理论上tabbar页面也不允许返回操作,因此此处我们选用的是Taro.reLaunch()方法去替代原来的Taro.switchTab()

其余风险就是由于文件位置发生改变,所以可能会出现某些文件的引用错误(一般来说不会存在此问题,因为Taro在打包编译时会提示),为了防止错误情况需要测试过一下各个功能的主流程。

注意:此处开发最好自己整理一下修改范围提供给测试比较好。

六、实现效果

话不多说直接上图(此处是在新版本的开发过程中做的验证,因此数据和上面的数据有点不同)

原来的包体积:

image.png

现在的包体积:

image.png

从上图来看我们提取出tabbar的实现之后,主包体积直接缩减了1/4左右,主要缩减的就是我们主包的逻辑代码,这样未来主包超出限制的可能性也会大大降低,极大的提升了容错率。