小程序页面跳转也是一个很容易踩到坑的开发流程,本文介绍页面跳转相关的一些好用的实践和封装的组件库。

# 小程序页面跳转

一个小程序可以有很多页面,每个页面承载不同的功能,页面之间可以互相跳转。我们知道,小程序分为渲染层和逻辑层,渲染层中包含了多个 WebView,每个 WebView 对应到我们的小程序里就是一个页面 Page,每一个页面都独立运行在一个页面层级上,如图:

# 页面栈管理

一个小程序拥有多个页面,我们知道wx.navigateTo可以推入一个新的页面。我们看看小程序示例小程序里,在首页使用2次wx.navigateTo后,页面层级会有三层:

我们把这样的一个页面层级称为页面栈。在小程序中所有页面的路由全部由框架进行管理,框架以栈的形式维护了当前的所有页面。小程序中页面栈最多十层,也就是十个 WebView,如果超过了十个之后,就没法再打开新的页面了。

对于每一个新的页面层级,渲染层都需要进行一些额外的准备工作。在小程序启动前,客户端会提前准备好一个页面层级用于展示小程序的首页。除此以外,每当一个页面层级被用于渲染页面,客户端都会提前开始准备一个新的页面层级,使得每次调用wx.navigateTo都能够尽快展示一个新的页面。

# 页面间跳转

小程序启动时仅有一个页面层级,而在小程序运行中,页面之间的跳转表现包括这些:

API 页面表现 页面栈表现
wx.navigateTo 保留当前页面,跳转到应用内的某个页面 创建一个新的页面层级
wx.navigateBack 关闭当前页面,返回上一页面或多级页面 销毁一个页面层级
wx.redirectTo 关闭当前页面,跳转到应用内的某个页面 将当前页面层级重新初始化。重新传入页面的初始数据、路径等,视图层清空当前页面层级的渲染结果然后重新渲染页面
wx.reLaunch 关闭所有页面,打开到应用内的某个页面 销毁所有页面层级,再创建一个新的页面层级
wx.switchTab 关闭其他所有非 tabBar 页面,跳转到 tabBar 页面 销毁所有非 tabBa 页面层级,打开tabBar页面层级

上面提到了 tabBar。除了普通的页面跳转,小程序里还支持配置 tabBar。tabBar 就是类似客户端 APP 底部的 tab 切换,为了获得更好的体验,小程序提供了这样的全局组件,在 app.json 文件中设置 tabBar,因此我们小程序会区分 tabBar 页面和非 tabBar 页面。tabBar 页面之间的切换都只会有一个层级,而跳转到非 tabBar 页面之后,就有了页面层级和页面栈的管理。

# 页面跳转技巧

小程序的 JS 脚本是运行在 JsCore 的线程里,小程序的每个页面各自有一个 WebView 线程进行渲染,所以小程序切换页面时,小程序逻辑层的 JS 脚本运行上下文依旧在同一个 JsCore 线程中。

因为在同一个 JsCore 线程中,我们就会有一些问题会遇到,也可以有一些小技巧来处理。

# 判断跳转来源

由于切换页面后,业务逻辑依然在同一个 JsCore 线程中。因此,即使是小程序页面被关闭 unload 之后,如果有原本在执行的逻辑,会继续执行完毕。

在这样的情况下,如果有重定向、跳转等逻辑,在跳转之后后续的逻辑依然会继续执行,这时候如果还有其他的跳转逻辑,可能会导致页面连续跳转,严重的话跳转参数丢书会导致白屏。

为了防止用户自行返回等操作,可以添加当前页面的条件判断是否要执行,页面栈可以通过getCurrentPages拿到,例如我们可以添加这样的方法处理:

// 处理是否有当前路由
function matchOriginPath(originPageUrl) {
  let currentPages = getCurrentPages();
  const currentPage = currentPages[currentPages.length - 1].route;
  // 判断是否设置了特定页面才进行跳转
  // 如果设置了,判断当前页面是否特定页面,是才跳转
  // 用于判断当前页面是否已经被跳转走(用户手动关闭等)
  const isMatch = !originPageUrl || (originPageUrl && currentPage.indexOf(originPageUrl) > -1);
  // 如果设置了,当页面路径不匹配,则进行报错提示
  if (!isMatch) {
    console.error(
      "matchOriginPath do not match",
      `currentPage: ${currentPage}, originPageUrl: ${originPageUrl}`
    );
  }
  return isMatch;
}

通过这样的检查方式,我们可以通过传参来判断下是否要检查:

/**
 * 跳转到页面
 * @param {object} url 要跳转的页面地址
 * @param {object} originPageUrl 原始页面地址,用于判断来源是否符合
 */
export function navigateTo(url: string, originPageUrl?: string) {
  // 不符合源页面条件则不跳转
  if (!matchOriginPath(originPageUrl!)) {
    logger.RUN("navigateTo", "originPageUrl != currentPage, return");
    return Promise.resolve();
  }
  console.log(url);
  wx.navigateTo({ url });
}

这样,我们跳转的时候可以添加参数,预防页面非预期的跳转:

navigateTo({url: '/pages/pageB'}, '/pages/pageA');
// 后面的逻辑在页面跳转之后,不会再生效
navigateTo({url: '/pages/pageC'}, '/pages/pageA');

# 跳转传参

小程序提供的跳转相关 API,需要在 url 后面添加参数的方式来传参,但有些时候我们不仅仅需要携带简单的字符串或者数字,我们还可能需要携带一个较大的对象数据。那么这种情况下,由于小程序页面切换依然在同一个 JsCore 上下文,我们可以通过共享对象的方式来传递。

共享对象需要在公共库中存储一个当前跳转的传参内容,同时为了避免页面同时跳转导致传参内容不匹配,我们可以通过一个随机 ID 的方式来标记:

export function getRandomId() {
  // 时间戳(9位) + 随机串(10位)
  return (Date.now()).toString(32) + Math.random().toString(32).substring(2);
}

跳转的时候,我们可以根据 url 传参的方式,还是共享对象传参的方式,来进行不同的判断处理:

let globalPageParams = undefined; // 全局页面跳转参数
let globalPageParamsId: any = undefined; // 全局页面跳转参数Id,用于标识某一次跳转的数据

// 跳转时参数处理
function mangeUrl(url, options) {
  const { urlParams, pageParams } = options;

  // url参数处理
  if (urlParams) {
    url = addUrlParams(url, urlParams);
  }

  // 页面参数处理
  if (pageParams) {
    globalPageParams = objectCopy(pageParams);
    // 获取随机 ID
    globalPageParamsId = getRandomId();
    // 将随机 ID 带入 url 参数中,可用来获取全局参数
    url = addUrlParams(url, { randomid: globalPageParamsId });
  } else {
    globalPageParams = undefined;
    globalPageParamsId = undefined;
  }
  return url;
}

这样,我们的跳转方法可以这么处理:

/**
 * 跳转到页面
 * @param {object} url 要跳转的页面地址
 * @param {object} options 要携带的参数信息
 * @param {object} originPageUrl 原始页面地址,用于判断来源是否符合
 */
export function navigateTo(url, options = {}, originPageUrl) {
  url = mangeUrl(url, options);

  // 不符合源页面条件则不跳转
  if (!matchOriginPath(originPageUrl!)) {
    logger.RUN("navigateTo", "originPageUrl != currentPage, return");
    return Promise.resolve();
  }
  wx.navigateTo({ url });
}

# 结合 Component 自动取参

我们有介绍过如何使用 Component 来开发页面,使用 Component 有几个好处:

  1. 可以通过 Behavior 来拓展组件的通用能力。
  2. 可以直接通过定义 properties 来获取页面参数。

页面如何使用 Behavior 看看官方文档 (opens new window):事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用Component构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应json文件中包含usingComponents定义段。

这里配合跳转传参,我们可以省略很多的逻辑。例如我们有一个结果页面,页面展示直接从 url 中取值(如果使用 Page 的 query 方式获取参数,需要自己进行 decodeURIComponent 才能使用,而使用组件的 properties 则不需要):

Component({
  // 其他配置省略
  properties: {
    type: String,  // 结果类型,成功-success,失败-warn
    title: String, // 主要文案
    info: String // 辅助文案
  }
});

我们这样进行跳转:

navigateTo("/pages/result/result", {
  // 直接带入参数,result组件可通过properties直接拿到
  urlParams: {
    type: "success",
    title: "操作成功",
    info: "成功就是这么简单"
  }
});

则可以直接在模板中显示:

<!-- 使用的 weui 组件库 -->
<view class="page">
    <weui-msg type="{{type}}" title="{{title}}">
        <view slot="desc">{{info}}</view>
    </weui-msg>
</view>

如果是通过页面传参的方式,则需要通过随机 ID 来获取对应的参数:

// 通过随机 ID 获取对应参数
export function getPageParams(randomId) {
  if (globalPageParamsId === randomId) {
    return globalPageParams || {};
  }
  return {};
}

组件中可以通过 properties 来获取随机 ID,然后获取对应参数:

Component({
  // 其他配置省略
  properties: {
    randomid: String,  // 随机 ID
  },
  methods: {
    onLoad() {
      // 获取参数
      const params = getPageParams(this.data.randomid);
      // 处理参数
    }
  }
});

具体的代码实现,可参考项目代码:wxapp-typescript-demo (opens new window)

# 参考

# 结束语

使用自定义组件的方式来开发页面,简直不能太赞啦。通过 Behavior 来各种拓展组件的能力,同时还能简化一些取参的逻辑,还是特别方便的。

部分文章中使用了一些网站的截图,如果涉及侵权,请告诉我删一下谢谢~
温馨提示喵