使用分包异步化组件实现可变Tab页面
发布于 2 年前 作者 lei34 1181 次浏览 来自 分享

背景和需求

众所周知,在微信小程序内,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 回复

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

NB 大神!

👍 👍 👍

棒,学习了~

回到顶部