libin1991 / libin_Blog

爬虫-博客大全
https://libin.netlify.com/
124 stars 17 forks source link

小程序无限层级路由方案(无框架依赖) #680

Open libin1991 opened 5 years ago

libin1991 commented 5 years ago

背景

  • 小程序历史栈最多只支持10层
  • 当小程序业务比较复杂时,就很容易超过10层。
  • 当超过10层后,有的机型是点击无反应,有的机型会出现一些未知错误

为了解决这些问题,我们引入了无限层级路由方案。

方案

首先声明一下,最初方案并不是我提出的,是我司内部一位清华学霸提出的。但他们是基于wepy框架做的处理,由于我们用的是mpvue,所以对这个方案上做了修改,同时不依赖于框架。

虽然是改造版,但原理是一样的,下面我来介绍一下修改后的方案。

几个关键点:

  1. 9层(含9层)以内时:走小程序自己的历史栈就ok了,跳转时候更新一下逻辑栈,这没啥可说的
  2. 从9层跳转10层:需要把第9层重定向到中转页,再由中转页跳转到10层
  3. 10层以后跳转:在navigateTo方法中处理,到10层之后,再跳转就第10层页面一直做redirectTo(重定向)操作了
  4. 10层以上返回:会返回到中转页,由中转页判断,具体返回到哪个页面,然后navigateTo(跳转)过去
  5. 从10层返回到9层:返回到中转页,将中转页redirectTo(重定向)到第9层页面
  6. 9层内的返回:直接返回就好了,返回时候不会更新逻辑栈,但没有关系,因为只有中转页才会用到逻辑栈
  7. 逻辑栈更新机制
    1. 跳转、返回中转页时更新
    2. navigateTo时更新
    3. redirectTo时更新
    4. reLaunch时更新
    5. navigateBack时更新

图示:

  1. 用户操作
  2. 小程序历史栈
  3. js逻辑栈:自行维护的js路由栈
  4. “中”表示中转页
  5. “1 2 3 4 5 6 7 8 9 A B C”表示不同的页面路径
用户操作 小程序历史栈 js逻辑栈 后续操作
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 跳转页面9
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 跳转页面A
1 2 3 4 5 6 7 8 9 A 1 2 3 4 5 6 7 8 中 A 1 2 3 4 5 6 7 8 9 A 跳转页面B
1 2 3 4 5 6 7 8 9 A B 1 2 3 4 5 6 7 8 中 B 1 2 3 4 5 6 7 8 9 A B 跳转页面C
1 2 3 4 5 6 7 8 9 A B C 1 2 3 4 5 6 7 8 中 C 1 2 3 4 5 6 7 8 9 A B C 返回
1 2 3 4 5 6 7 8 9 A B 1 2 3 4 5 6 7 8 中 B 1 2 3 4 5 6 7 8 9 A B 返回
1 2 3 4 5 6 7 8 9 A 1 2 3 4 5 6 7 8 中 A 1 2 3 4 5 6 7 8 9 A 返回
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 返回(逻辑栈不更新)
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 返回(逻辑栈不更新)
1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 7 8 9 返回(逻辑栈不更新)
1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 7 8 9 返回(逻辑栈不更新)
1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 6 7 8 9 跳转页面6
1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6 跳转页面7
1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 7 ...

到这里细心的读者可能已经发现:

之前跳转操作和10层以上的返回操作都会更新逻辑栈,到了10层以内的返回操作就不会更新逻辑栈了。

原因:

这块也是我们对原有方案的主要改造点。因为到了10层以内,所有的返回和跳转都由微信系统历史栈接管了。

我们只要保证用户在通过api进行跳转操作时更新就可以了。而且,自己维护的逻辑路由栈实际上只有中转页才会用到。

这样也就不用在每个页面都要注册onUnload钩子去实时更新返回时的路由信息了。把更新路由信息的逻辑都放到了api调用这一层。业务开发时完全不用关心。

示意代码

lib/navigator/Navigator.js (自己封装的跳转方法, History.js代码省略了)

...
import History from '@/lib/navigator/History'
const MAX_LEVEL = 10 // 小程序支持打开的页面层数
export default class Navigator {
  // 中转页面路径
  static curtainPage = '/pages/curtain/curtain/main'
  // 最大页数
  static maxLevel = MAX_LEVEL
  // 逻辑栈
  static _history = new History({
    routes: [{ url: '' }],
    correctLevel: MAX_LEVEL - 2
  })

  ...

  /**
   * 打开新页面
   * @param {Object} route 页面配置,格式同wx.navigateTo
   */
  @makeMutex({ namespace: globalStore, mutexId: 'navigate' }) // 避免跳转相关函数并发执行
  static async navigateTo (route) {
    console.log('[Navigator] navigateTo:', route)
    // 更新逻辑栈
    Navigator._history.open({ url: route.url })

    let curPages = getCurrentPages()
    // 小于倒数第二层时,直接打开
    if (curPages.length < MAX_LEVEL - 1) {
      await Navigator._secretOpen(route) // 就是调用wx.navigateTo
    // 倒数第二层打开最后一层
    } else if (curPages.length === MAX_LEVEL - 1) {
      const url = URL.setParam(Navigator.curtainPage, { url: route.url })
      await Navigator._secretReplace({ url })  // wx.redirectTo 到中转页,再由中转页跳转到第10层页面
    // 已经达到最大层数,直接最后一层重定向
    } else {
      await Navigator._secretReplace(route)    // wx.redirectTo 第10层页面直接重定向
    }
  }

  /**
   * 完整历史记录
   * @return {Array}
   */
  static get history () {
    return Navigator._history.routes
  }

  /**
   * 更新路由
   * @param {Object} config 自定义配置,可配置项参见 _config 相关字段及注释
   */
  static updateRoutes (routes = []) {
    this._history._routes = routes
  }

  ...
}
复制代码

中转页代码 /pages/curtain/curtain/index.vue

<template>
  <div class="main"></div>
</template>
<script>
import Navigator from '@/lib/navigate/Navigator'
// query参数
let opts = null
// 是否为返回操作
let isBack = false

export default {
  onLoad (options) {
    // 缓存参数
    opts = options
    // 执行onLoad生命周期,认为是跳转或者重定向操作
    isBack = false
  },
  onShow () {
    // 跳转、重定向操作时,逻辑栈的状态会在跳转函数里更新
    if (!isBack) {
      const url = decodeURIComponent(opts.url)
      // 再返回时认为是返回操作
      isBack = true
      // 跳转操作
      if (opts.type === 'navigateTo') {
        Navigator._secretOpen({ url })      // 相当直接执行wx.navigateTo,不会更新逻辑栈
      // 重定向
      } else if (opts.type === 'redirectTo') {
        Navigator._secretReplace({ url })   // 相当直接执行wx.redirectTo,不会更新逻辑栈
      }
    // 返回操作
    } else {
      // 获取逻辑栈
      let routes = Navigator.history
      // 如果10层之外的返回,用navigateTo操作
      // 如果是10层返回到9层,用redirectTo操作
      const operation = (routes.length === Navigator.maxLevel) ? 'redirectTo' : 'navigateTo'
      // 获取要返回的页面路由
      let preRoute
      if (operation === 'navigateTo') {
        // 移除逻辑层中后两个元素:
        // 移除最后一个是因为要返
        // 移除倒数第二个是因为,跳转到倒数第二个页面时会重新插入逻辑栈
        preRoute = routes.splice(routes.length - 2, 2)[0]
      } else {
        // 重定向时只移除最后一个元素
        preRoute = routes[routes.length - 2]
        routes.splice(routes.length - 1, 1)
      }
      // 更新逻辑栈
      Navigator.updateRoutes(routes)
      // 执行自己包装的跳转、重定向方法,该操作会更新逻辑栈
      Navigator[operation](preRoute)
    }
  }
}
</script>
<style lang="scss">
  .main {
    background-color: $white-color;
  }
</style>
复制代码

原理就是这样,但是有几点需要注意:

  • 业务代码中需要调用自己封装的跳转方法

切记不要直接调用wx的api,也不要使用组件,这样是没法更新js逻辑栈的,正确跳转方式如:Navigator.navigateTo({ url: 'xxx' })。

  • 跳转时要及时更新js逻辑栈(更新时机如上所述),因为这会直接影响中转页的跳转逻辑

这个方案最大的优点在于不用监听页面卸载时对逻辑栈的更新,无需在每个页面里加入更新逻辑栈代码。

OK,这次就介绍这么多,有问题或者有更好的方案,可以留言沟通,大家相互学习。