- 使用分包异步化组件实现可变Tab页面
背景和需求 众所周知,在微信小程序内,TabBar 页面必须放主包内,这固然是为了用户体验做出的限制,但是也限制了开发者,如果想要实现不同的客户可以定制不同的TabBar页面,而很多页面又是分散到不同分包内的,那我们能选择的方案也就是在所有可作为TabBar页面上放置自定义TabBar组件,而后根据客户的不同配置,展示不同的TabBar 选项,当客户点击Tab时,使用[代码]navigateTo[代码]或[代码]redirectTo[代码]进行切换页面。 但这个方案存在明显的问题,首先如果使用[代码]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" } /* 其他页面信息 */ } [代码] 生成 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' }}" /> <!-- 其他页面节点 --> [代码] 生成容器页面 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", /* 其他页面 */ } } [代码] 编写容器页面 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[代码],要在生产环境中使用还需要做很多的验证工作,也希望微信官方能尽早修改开发者工具中的问题。
2021-11-29 - IOS scroll-view中的自定义组件fixed问题
这个是正常现象,因为 iOS 下加了 -webkit-overflow-scrolling: touch,这个会产生滚动惯性,体验更好,但会改变 fixed 的行为,建议不在 scroll-view 里有 fixed 元素
2020-04-23 - swiper内嵌套可横向滚动块,swiper禁止滑动,滚动块可滑动,怎么阻止滑动冒泡?
swiper内嵌套可横向滚动块,swiper禁止滑动,滚动块可滑动,怎么阻止滑动冒泡?
2020-12-10 - swiper swiper-item内容如果添加一个横向滑动的区域,怎么不行啊?
swiper swiper-item内容如果添加一个横向滑动的区域,怎么不行啊?直接就与swiper的滑动冲突了,有人遇到这样的需求没?顶部导航可以点击切换,页面还可以滑动页面切换,所以我用了导航是 scroll-view,内容是swiper加scroll-view,但是如果内容里面再出现横向滑动就不行了呢,跟轮播的冲突了 [图片]
2021-01-05