使用分包异步化组件实现可变Tab页面

发布于 4 年前作者 lei341798 次浏览最后编辑 4 年前来自 share

背景和需求

众所周知,在微信小程序内,TabBar 页面必须放主包内,这固然是为了用户体验做出的限制,但是也限制了开发者,如果想要实现不同的客户可以定制不同的TabBar页面,而很多页面又是分散到不同分包内的,那我们能选择的方案也就是在所有可作为TabBar页面上放置自定义TabBar组件,而后根据客户的不同配置,展示不同的TabBar 选项,当客户点击Tab时,使用navigateToredirectTo进行切换页面。

但这个方案存在明显的问题,首先如果使用navigateTo进行切换,会有很明显的页面切换动画,很容易到达10层页面栈限制(当然这个可以使用无限路由方案进行缓解,但是无限路由是一种万不得已且体验很差的路由方案),且由于页面未进行销毁,内存占用会比较大,容易造成卡顿;如果使用redirectTo进行切换,页面节点状态无法保存(如滚动位置),页面数据倒是可以使用全局状态管理库进行保存,但是每次在切换 Tab 都会有明显的数据重新加载的动画效果。

曙光

在微信小程序支持分包异步化之前,对于上面的问题一直没有好的解决方案,支持分包异步化之后,我们可以将一些组件放入分包内异步加载,这一定程度上解决了主包过大的问题。同时也让我们看到了希望,我们可以将很多组件放入分包内进行异步加载,主包空间空了出来,可以放更多的页面,但不是所有页面都能放入主包,那还有其他方案吗?

我们想,既然组件能从分包异步加载,那页面可以吗?

我们知道,在微信小程序内,通常都会使用Page进行声明页面,但也可以用Component声明页面,也就是说 Component 声明的组件可以当成页面用,那反过来,Page 声明的页面可以当成组件用吗?

答案是可以,但是当这样使用的时候,页面的生命周期方法不会被执行,且实例对象上不存在options(页面路由参数),route(当前页面路由地址)等数据,那我们就不能愉快地玩耍了吗?

No!

没有页面该有的属性?那我们就拿到实例对象给他补上去!

生命周期方法不执行?那我们就拿到实例对象后自己去调用!

解决思路

要将现有页面作为组件加载,那我们必须要有一个容器页面,去承载真实页面,在容器页面中去补上已经作为组件的真实页面缺失的属性,在对应的生命周期方法中调用真实页面的生命周期钩子。

我们第一步就需要创建一个容器页面出来,我们可以选择手动创建,也可以自动化创建, 但是已有项目来说,手动创建太费时,且每增加新页面都要修改容器页面代码,故此不考虑。

自动化构建容器页面包含如下步骤:

读取 app.json,获取所有分包页面路径
读取分包页面对应的json文件,将其中内容记录到 `` tab-bar-page-config.js `` 中,因为我们需要在运行时读取真实页面的标题,背景色等信息,而微信小程序不支持从js中读取json文件,故需要将json内容提前读取出来,为了减少数据量,记录时可以将`` usingComponents ``等无需运行时使用的数据去掉。效果如下:
// tab-bar-page-config.js
module.exports = {
  "/pack_a/page_1": {
    "navigationBarTitleText": "页面标题",
    "navigationBarBackgroundColor": "#ffffff"
  },
  "/pack_b/page_2": {
    "navigationBarTitleText": "页面标题",
    "navigationBarBackgroundColor": "#ffffff"
  }
  /* 其他页面信息 */
}
  1. 生成 wxml 文件,效果如下:
<pack_a_page_1 id="pack_a_page_1" wx:if="{{ pagePath === '/pack_a/page_1' }}" />
<pack_b_page_2 id="pack_b_page_2" wx:elif="{{ pagePath === '/pack_b/page_2' }}" />
<!-- 其他页面节点 -->
  1. 生成容器页面 json 文件,效果如下:
{
  "usingComponents": {
    "pack_a_page_1": "/pack_a/page_1",
    "pack_b_page_2": "/pack_b/page_2",
    /* 其他页面 */
  },
  "componentPlaceholder": {
    "pack_a_page_1": "view",
    "pack_b_page_2": "view",
    /* 其他页面 */
  }
}
  1. 编写容器页面 js 逻辑,大体如下:
Page({
  data: {
    // 当前真实页面的路径
    pagePath: '',
  },
  // 真实页面的实例
  pageInstance: null,

  onLoad() {
    // 根据网络接口返回数据,得到当前容器页面应当显示的真实页面路径
    this.setData({
      pagePath: someDataFromNet.pagePath,
    });
  },

  onShow() {
    this.pageInstance?.onShow?.();
  },

  onReady() {
    this.pageInstance?.onReady?.();
  },

  /* 其他生命周期 */
})

我们现在面临一个问题,那就是我们是使用分包异步化组件进行加载真实页面,那真实页面是什么时候加载成功的呢?我们知道当组件加载成功后,会执行组件的 lifetimes.attached 生命周期, 那既然页面可以当成组件用,那页面是否也有这个生命周期呢?通过查阅文档,我们知道了可以在页面中使用Behavior, 我们可以通过Behavior中定义 lifetimes.attached,在其中通过 this.triggerEvent('pageattached') 去通知容器页面,现在我们的 wxml 需要做一些修改,如下:

<pack_a_page_1 wx:if="{{ pagePath === '/pack_a/page_1' }}" bind:pageattached="onPageAttached" />
<pack_b_page_2 wx:elif="{{ pagePath === '/pack_b/page_2' }}" bind:pageattached="onPageAttached" />
<!-- 其他页面节点 -->

js 中也要增加 onPageAttached 方法,如下:

Page({
  /* 其他生命周期方法 */

  onPageAttached() {
    const route = this.data.pagePath.slice(1);
    const id = route.replace(/\//g, '_');
    const page = this.selectComponent(`#${route}`);
    page.route = route;
    page.options = {};
    // 补全其他信息,

    // 调用对应生命周期方法,
    page.onLoad?.(page.options);
    // 由于组件可能加载得比较晚,容器页面的 onShow 和 onReady 已经执行过了,这里需要手动执行一遍真实页面的 onShow 和 onReady
    // 还需要额外做一些判断,避免 onShow 连续执行多遍
    page.onShow?.(page.options);
    page.onReady?.(page.options);
  },
})

好了,准备工作基本上做完,现在就差给所有页面加上我们之前写的Behavior了,如果项目一开始就封装了BasePage之类的方法,我们只需要在 BasePage 将这个Behavior加到BasePage中就行,如果没有的话,可以通过改写Page去实现,这里就不举例了。

现在我们按照上面的步骤生成5个容器页面并且加入到 app.json 中了,然后开始下一步了,
等等。。。5个容器页面?生成的代码有5份!不行,这样会平白占用很多主包空间的,我们需要做一些优化:将生成5个容器页面优化成生成一个容器组件,然后在5个容器页面内去引用该组件,并修改上面的一些逻辑,这样生成的代码就基本上少了1/5,还是很可观的。

现在还差封装switchTab方法了,在其中将url替换成容器页面的地址,然后记录该容器页面需要展示的真实页面地址,在容器页面中加载对应的真实页面即可。亦可改写 wx.switchTab 去调用我们封装的 switchTab 方法,在此就不举例了。

好了,现在基础步骤已完成,就差看效果了。

咦,好像还差某些东西,页面标题呢?怎么不能下拉刷新了?这个页面好像是没有顶部导航栏的呀。

我们一个一个来。

标题及背景颜色等



还记得之前生成的 `` tab-bar-page-config.js `` 吗?我们在其中记录了页面的一些信息,现在,我们需要在运行时去调用微信API设置标题,颜色等信息。解决。
下拉刷新



微信没有提供是否启用下拉刷新的API,所以我们只能给所有容器页面都加上下拉刷新,然后 `` onPullDownRefresh `` 中判断如果当前真实页面没有启用下拉刷新,就调用`` wx.stopPullDownRefresh ``停止下拉刷新,否则就调用真实页面的`` onPullDownRefresh ``钩子。额。。。勉强算解决吧。
顶部导航栏



微信同样没提供是否启用顶部导航栏的API,故只能将5个容器页面分成2类,2个是不带顶部导航的,剩下3个是带顶部导航的,在我们封装的 `` switchTab `` 中增加判断要跳转的页面是否是包含顶部导航的,分别落到不同的容器页面上即可。解决。

至此,动态Tab页面基本上实现了,还有些样式上的兼容问题,如:某个页面的wxss声明了

page {
  backgroud: 'red';
}

那容器页面内的所有页面都会被影响,对此我们只能在页面的wxss中不使用标签选择器,实际上在微信开发者工具中,使用标签选择器是会报警告的,但是口头约束是没有用的,还是会有人会写,故我们引入了postcss,编写插件使在构建时将标签选择器去掉,并且报出警告。

至此,功能基本完成,需要做的就是验证哪些功能出现了问题,做出相应的修改。

结尾

分包异步化作为一个新出现的特性,还存在一些不稳定,如在开发者工具中,经常出现加载失败的问题,ios 真机调试报错等问题,且要求的最低SDK版本为2.17.3,要在生产环境中使用还需要做很多的验证工作,也希望微信官方能尽早修改开发者工具中的问题。

10 回复
ping13
ping131 楼4 年前

sweet

jun65
jun652 楼4 年前

棒棒棒

gangyang
gangyang3 楼4 年前

企业微信小程序不支持分包异步化??

min58
min584 楼4 年前

nice!

wangyong
wangyong5 楼4 年前

受益匪浅

lei14
lei146 楼4 年前

excellent

fanglong
fanglong7 楼4 年前

🐂

chao20
chao208 楼4 年前

NB 大神!

mcheng
mcheng9 楼4 年前

👍 👍 👍

xiaxue
xiaxue10 楼19 天前

棒,学习了~