框架

微信小程序开发框架的目标是通过尽可能简单、高效的方式让开发者可以在微信中开发具有原生 APP 体验的服务。

整个微信小程序框架系统分为两部分:逻辑层(App Service)和 视图层(View)。微信小程序提供了自己的视图层描述语言 WXMLWXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,让开发者能够专注于数据与逻辑。

响应的数据绑定

框架的核心是一个响应的数据绑定系统,可以让数据与视图非常简单地保持同步。当做数据修改的时候,只需要在逻辑层修改数据,视图层就会做相应的更新。

通过这个简单的例子来看:

在开发者工具中预览效果

<!-- This is our View -->
<view> Hello {{name}}! </view>
<button bindtap="changeName"> Click me! </button>
// This is our App Service.
// This is our data.
var helloData = {
  name: 'Weixin'
}

// Register a Page.
Page({
  data: helloData,
  changeName: function(e) {
    // sent data change to view
    this.setData({
      name: 'MINA'
    })
  }
})
  • 开发者通过框架将逻辑层数据中的 name 与视图层的 name 进行了绑定,所以在页面一打开的时候会显示 Hello Weixin!
  • 当点击按钮的时候,视图层会发送 changeName 的事件给逻辑层,逻辑层找到并执行对应的事件处理函数;
  • 回调函数触发后,逻辑层执行 setData 的操作,将 data 中的 nameWeixin 变为 MINA,因为该数据和视图层已经绑定了,从而视图层会自动改变为 Hello MINA!

页面管理

框架 管理了整个微信小程序的页面路由,可以做到页面间的无缝切换,并给以页面完整的生命周期。开发者需要做的只是将页面的数据、方法、生命周期函数注册到 框架 中,其他的一切复杂的操作都交由 框架 处理。

基础组件

框架 提供了一套基础的组件,这些组件自带微信风格的样式以及特殊的逻辑,开发者可以通过组合基础组件,创建出强大的微信小程序

丰富的 API

框架 提供丰富的微信原生 API,可以方便的调起微信提供的能力,如获取用户信息,本地存储,支付功能等。

场景值

基础库 1.1.0 开始支持,低版本需做兼容处理。

场景值用来描述用户进入微信小程序的路径。完整场景值的含义请查看场景值列表。

由于Android系统限制,目前还无法获取到按 Home 键退出到桌面,然后从桌面再次进微信小程序的场景值,对于这种情况,会保留上一次的场景值。

获取场景值

开发者可以通过下列方式获取场景值:

  • 对于微信小程序,可以在 ApponLaunchonShow,或wx.getLaunchOptionsSync 中获取上述场景值。
  • 对于小游戏,可以在 wx.getLaunchOptionsSync 和 wx.onShow 中获取上述场景值

返回来源信息的场景

部分场景值下还可以获取来源应用、公众号或微信小程序的appId。获取方式请参考对应API的参考文档。

场景值 场景 appId含义
1020 公众号 profile 页相关微信小程序列表 来源公众号
1035 公众号自定义菜单 来源公众号
1036 App 分享消息卡片 来源App
1037 微信小程序打开微信小程序 来源微信小程序
1038 从另一个微信小程序返回 来源微信小程序
1043 公众号模板消息 来源公众号
1069 移动应用 来源App

逻辑层 App Service

微信小程序开发框架的逻辑层使用 JavaScript 引擎为微信小程序提供开发 JavaScript 代码的运行环境以及微信小程序的特有功能。

逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈。

开发者写的所有代码最终将会打包成一份 JavaScript 文件,并在微信小程序启动的时候运行,直到微信小程序销毁。这一行为类似 ServiceWorker,所以逻辑层也称之为 App Service。

JavaScript 的基础上,我们增加了一些功能,以方便微信小程序的开发:

  • 增加 AppPage 方法,进行程序注册和页面注册。
  • 增加 getAppgetCurrentPages 方法,分别用来获取 App 实例和当前页面栈。
  • 提供丰富的 API,如微信用户数据,扫一扫,支付等微信特有能力。
  • 提供模块化能力,每个页面有独立的作用域。

注意:微信小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 windowdocument 等。

注册微信小程序

每个微信小程序都需要在 app.js 中调用 App 方法注册微信小程序实例,绑定生命周期回调函数、错误监听和页面不存在监听函数等。

详细的参数含义和使用请参考 App 参考文档 。

// app.js
App({
  onLaunch (options) {
    // Do something initial when launch.
  },
  onShow (options) {
    // Do something when show.
  },
  onHide () {
    // Do something when hide.
  },
  onError (msg) {
    console.log(msg)
  },
  globalData: 'I am global data'
})

整个微信小程序只有一个 App 实例,是全部页面共享的。开发者可以通过 getApp 方法获取到全局唯一的 App 实例,获取App上的数据或调用开发者注册在 App 上的函数。

// xxx.js
const appInstance = getApp()
console.log(appInstance.globalData) // I am global data

注册页面

对于微信小程序中的每个页面,都需要在页面对应的 js 文件中进行注册,指定页面的初始数据、生命周期回调、事件处理函数等。

使用 Page 构造器注册页面

简单的页面可以使用 Page() 进行构造。

代码示例:

//index.js
Page({
  data: {
    text: "This is page data."
  },
  onLoad: function(options) {
    // 页面创建时执行
  },
  onShow: function() {
    // 页面出现在前台时执行
  },
  onReady: function() {
    // 页面首次渲染完毕时执行
  },
  onHide: function() {
    // 页面从前台变为后台时执行
  },
  onUnload: function() {
    // 页面销毁时执行
  },
  onPullDownRefresh: function() {
    // 触发下拉刷新时执行
  },
  onReachBottom: function() {
    // 页面触底时执行
  },
  onShareAppMessage: function () {
    // 页面被用户分享时执行
  },
  onPageScroll: function() {
    // 页面滚动时执行
  },
  onResize: function() {
    // 页面尺寸变化时执行
  },
  onTabItemTap(item) {
    // tab 点击时执行
    console.log(item.index)
    console.log(item.pagePath)
    console.log(item.text)
  },
  // 事件响应函数
  viewTap: function() {
    this.setData({
      text: 'Set some data for updating view.'
    }, function() {
      // this is setData callback
    })
  },
  // 自由数据
  customData: {
    hi: 'MINA'
  }
})

详细的参数含义和使用请参考 Page 参考文档 。

在页面中使用 behaviors

基础库 2.9.2 开始支持,低版本需做兼容处理。

页面可以引用 behaviors 。 behaviors 可以用来让多个页面有相同的数据字段和方法。

// my-behavior.js
module.exports = Behavior({
  data: {
    sharedText: 'This is a piece of data shared between pages.'
  },
  methods: {
    sharedMethod: function() {
      this.data.sharedText === 'This is a piece of data shared between pages.'
    }
  }
})
// page-a.js
var myBehavior = require('./my-behavior.js')
Page({
  behaviors: [myBehavior],
  onLoad: function() {
    this.data.sharedText === 'This is a piece of data shared between pages.'
  }
})

具体用法参见 behaviors 。

使用 Component 构造器构造页面

基础库 1.6.3 开始支持,低版本需做兼容处理。

Page 构造器适用于简单的页面。但对于复杂的页面, Page 构造器可能并不好用。

此时,可以使用 Component 构造器来构造页面。 Component 构造器的主要区别是:方法需要放在 methods: { } 里面。

代码示例:

Component({
  data: {
    text: "This is page data."
  },
  methods: {
    onLoad: function(options) {
      // 页面创建时执行
    },
    onPullDownRefresh: function() {
      // 下拉刷新时执行
    },
    // 事件响应函数
    viewTap: function() {
      // ...
    }
  }
})

这种创建方式非常类似于 自定义组件 ,可以像自定义组件一样使用 behaviors 等高级特性。

具体细节请阅读 Component 构造器 章节。

页面路由

在微信小程序中,所有页面的创建、销毁及状态转换都由页面路由来表达和进行控制。以下内容会简单介绍微信小程序的页面路由相关逻辑。

路由的时机

路由会以事件形式表示,由微信客户端下发给微信小程序基础库,下发后客户端和基础库将分别同时处理这一次路由事件。路由事件的发起可以大致分为以下两类:

  1. 通过用户的操作(如按下返回按钮)发起。通过这种方式发起时,路由事件将直接由客户端下发到基础库执行;

  2. 由开发者通过 API(如 wx.navigateTo)或者组件(如 <navigator>)发起。通过这种方式发起时,基础库将首先向客户端发起路由请求,客户端确认路由可以被执行后,再将路由事件下发到基础库。其中,如果路由被确定执行,API 的 success 回调函数或组件的 success 事件将被触发,否则将触发 fail

当一次路由被确定执行(API 或组件通知 success)时,没有操作可以取消这一次路由。

当多次路由被连续发起时,如果当前的路由事件还未处理完毕,后续的路由事件将等待当前路由处理,并排队依次执行,直到所有待处理的路由都被执行完毕。

一个简单的例子:用户点击返回按钮触发了 navigateBack,微信小程序在页面栈当前栈顶页的 onUnload 中调用 wx.redirectTo并不能 将当前正在被销毁的页面重定向为一个新页面,而是会先完成页面返回,再将页面返回后的新栈顶页重定向到新的页面。

页面栈

目前,微信小程序的页面会被组织为一个页面栈加若干不在栈中的悬垂页面的组合形式。其中,页面栈按顺序存放了通过跳转依次打开的页面,而当前已经创建但非活跃的 tabBar 页面及处于画中画模式(如 videolive-player 等)中的页面将以悬垂页面的形式存在。

全局接口 getCurrentPages 可以用来获取当前页面栈。

微信小程序冷启动完成后,在整个微信小程序存活过程中(除去某次路由执行到一半的中间状态外),页面栈中都将存在至少一个页面。

页面栈的具体行为可以参见下面具体路由行为中的详细描述。

路由的监听及响应

页面生命周期函数

每个微信小程序页面都有若干生命周期函数,如 onLoad, onShow, onRouteDone, onHide, onUnload 等。它们可以在页面注册时定义,并会在相应的时机触发。所有生命周期函数及它们各自的含义和触发时机可以参见 Page 接口,下面的内容也将详细说明每个路由将如何触发页面的生命周期函数。

页面路由监听

从基础库版本 3.5.5 开始,基础库提供了一组针对路由事件的监听函数。相比页面生命周期函数,它们能更好地针对某次路由进行响应。详见 页面路由监听。

路由类型

微信小程序目前的路由类型可以大致分为以下七种:

1. 微信小程序启动

  • openType: appLaunch

微信小程序启动路由 appLaunch 表示一个新的微信小程序启动,并加载第一个页面。appLaunch 在每个微信小程序实例中会且仅会出现一次,且每个微信小程序实例启动时的第一个路由事件必定为 appLaunch

触发方式

appLaunch 仅能由微信小程序冷启动被动触发,不能由开发者主动触发,启动后也不能通过其他用户操作触发。

页面栈及生命周期处理

由于 appLaunch 必定是启动时的第一个路由,而路由前没有任何页面存在,此时页面栈必定为空。appLaunch 会创建路由事件指定的页面,并将其推入页面栈作为栈中唯一的页面。在这个过程中,这个页面的 onLoad, onShow 两个生命周期将依次被触发。

2. 打开新页面

  • openType: navigateTo

打开新页面路由 navigateTo 表示打开一个新的页面,并将其推入页面栈。

触发方式

  1. 调用 API wx.navigateTo, Router.navigateTo
  2. 使用组件 <navigator open-type="navigateTo"/>
  3. 用户点击一个视频小窗(如 video

navigateTo 的目标必须为非 tabBar 页面。

页面栈及生命周期处理

navigateTo 事件发生时,页面栈当前的栈顶页面将首先被隐藏,触发 onHide 生命周期;之后框架将创建路由事件指定的页面,并将其推入页面栈作为新的栈顶。在这个过程中,这个新页面的 onLoad, onShow 两个生命周期将依次被触发。

作为一种特殊情况,如果 navigateTo 事件发生时,页面栈当前的栈顶页面满足小窗模式逻辑,或事件由用户点击视频小窗发起,那么页面栈及生命周期的的处理会有所不同。

3. 页面重定向

  • openType: redirectTo

页面重定向路由 redirectTo 表示将页面栈当前的栈顶页面替换为一个新的页面。

触发方式

  1. 调用 API wx.redirectTo, Router.redirectTo
  2. 使用组件 <navigator open-type="redirectTo"/>

redirectTo 的目标必须为非 tabBar 页面。

页面栈及生命周期处理

redirectTo 事件发生时,页面栈当前的栈顶页面将首先被弹出并销毁,在此过程中,这个栈顶页面的 onUnload 生命周期将被触发;之后框架将创建路由事件指定的页面,并将其推入页面栈作为新的栈顶。在这个过程中,这个新页面的 onLoad, onShow 两个生命周期将依次被触发。

4. 页面返回

  • openType: navigateBack

页面返回路由 navigateBack 表示将页面栈当前的栈顶的若干个页面依次弹出并销毁。

触发方式

  1. 调用 API wx.navigateBack, Router.navigateBack
  2. 使用组件 <navigator open-type="navigateBack"/>
  3. 用户按左上角返回按钮,或触发操作系统返回的动作(如按下系统返回键、屏幕边缘向内滑动等)
  4. 用户点击一个视频小窗(如 video

如果页面栈中当前只有一个页面,navigateBack 调用请求将失败(无论指定的 delta 是多少);

如果页面栈中当前的页面数量少于调用时指定的 delta + 1(即调用后页面数量将少于一个),navigateBack 将弹出到只剩页面栈当前的页面栈底的页面为止(即至少保留一个页面)。

页面栈及生命周期处理

navigateBack 事件发生时,页面栈当前的栈顶页面将被弹出并销毁,并触发这个页面的 onUnload 生命周期;以上操作将被重复执行多次,直到弹出的页面数量等于指定的页面数量,或当前页面栈中只剩下一个页面。之后,页面栈新的栈顶页面的 onShow 生命周期将被触发。

一种特殊情况是,如果 navigateBack 发生时,页面栈当前的栈顶页面满足小窗模式逻辑,或事件由用户点击视频小窗发起,那么页面栈及生命周期的的处理会有所不同。

5. Tab 切换

  • openType: switchTab

Tab 切换路由 switchTab 表示切换到指定的 tab 页面。

触发方式

  1. 调用 API wx.switchTab, Router.switchTab
  2. 使用组件 <navigator open-type="switchTab"/>
  3. 用户点击 Tab Bar 中的 Tab 按钮

switchTab 的目标必须为 tabBar 页面。

页面栈及生命周期处理

由于 navigateToredirectTo 不能指定 tabBar 页面作为目标,因此当一个 tabBar 页面出现在页面栈中时,它必定为页面栈的第一个页面(即栈底页面);同时,框架会保证任一 tabBar 页面在微信小程序中最多同时存在一个页面实例。switchTab 的行为主要基于这两点进行。

switchTab 事件发生时,如果当前页面栈中存在多于一个页面,页面栈当前的栈顶页面将被弹出并销毁,并触发这个页面的 onUnload 生命周期;以上操作将被重复执行多次,直到页面栈中只剩下一个页面。之后,根据页面栈中仅剩的页面进行不同的处理:

  • 如果这个页面即为目标 tabBar 页面:
    • 如果路由事件开始时页面栈中存在多于一个页面(即目标 tabBar 页面不是栈顶页面),触发目标 tabBar 页面的 onShow 生命周期;
    • 否则(路由事件开始时目标 tabBar 页面是栈顶页面),不触发任何生命周期,直接结束;
  • 否则(该页面不为目标 tabBar 页面时):
    1. 将这个页面从页面栈中弹出;
    2. 如果这个页面为其他 tabBar 页面,该页面成为悬垂页面,并:
      • 如果路由事件开始时页面栈中只有一个页面(即该 tabBar 页面是栈顶页面),触发它的 onHide 生命周期;
      • 否则(路由事件开始时该 tabBar 页面不是栈顶页面),不触发它的任何生命周期;
    3. 否则(这个页面为非 tabBar 页面时),销毁该页面,触发 onUnload 生命周期;
    4. 如果目标 tabBar 页之前已经被创建过(现在是一个悬垂页面),将其推入页面栈,触发 onShow 生命周期;
    5. 否则(目标 tabBar 页不存在实例),创建目标 tabBar 页并推入页面栈,依次触发 onLoad, onShow 生命周期。

页面路由监听

基础库 3.5.5 开始支持,低版本需做兼容处理。

这篇指南主要说明从基础库版本 3.5.5 起可用的 页面路由事件监听函数 的使用方法。如果需要了解页面路由的类型及逻辑等基本信息,可以参考 页面路由。

由于每次路由可能触发多个页面的多个页面生命周期,因此当某个页面的某个生命周期被触发时,微信小程序往往比较难判断它被触发的原因,从而难以做出一些针对路由(而非针对页面)的响应。一个例子是当微信小程序进行重加载 reLaunch 路由时,微信小程序可能需要重设一些全局状态来保证后续逻辑正常工作,或者模拟近似于重新启动的效果。然而从页面生命周期来反向推测 reLaunch 是比较难的,因为即使某一瞬间当前所有页面都被销毁,也不一定是由 reLaunch 引起的(也可能是在仅有单个页面的情况下进行了重定向 redirectTo)。这套接口可以帮助处理这样的场景。

所有监听及触发时序

页面路由监听 触发时机 每次路由中的触发次数
wx.onBeforeAppRoute 路由事件下发到基础库,基础库执行路由逻辑前触发 一次
wx.onAppRoute 路由事件下发到基础库,基础库执行路由逻辑后触发 一次
wx.onAppRouteDone 路由对应的动画(页面推入、推出等)完成时触发 一次
wx.onBeforePageLoad 路由引发的页面创建之前触发 不限
wx.onAfterPageLoad 路由引发的页面创建完成后触发 不限
wx.onBeforePageUnload 路由引发的页面销毁之前触发 不限
wx.onAfterPageUnload 路由引发的页面销毁完成后触发 不限

例如,在一次 redirectTo 中,监听和处理逻辑将按以下顺序触发:

  1. wx.onBeforeAppRoute
  2. wx.onBeforePageUnload
  3. 旧页面 onUnload 生命周期
  4. 旧页面销毁,此过程中页面本身及页面中所有自定义组件的 detached 生命周期被递归触发
  5. 旧页面弹出页面栈,此时开始 getCurrentPages 接口不再能获取到旧页面
  6. wx.onAfterPageUnload
  7. wx.onBeforePageLoad
  8. 创建新页面,此过程中页面本身及页面中所有自定义组件的 created 生命周期被递归触发
  9. 新页面压入页面栈,此时开始 getCurrentPages 接口可以获取到新页面
  10. 挂载新页面,此过程中页面本身及页面中所有自定义组件的 attached 生命周期被递归触发
  11. 新页面 onLoad 生命周期
  12. 新页面 onShow 生命周期
  13. wx.onAfterPageLoad
  14. wx.onAppRoute
  15. (新页面推入动画完成时)wx.onAppRouteDone

对于其他路由,可以结合 页面路由 中的具体路由逻辑进行类推。

路由事件 ID

为了在多次监听回调中识别同一个路由事件,框架会为每一次独立的路由事件生成一个在微信小程序实例中唯一的 ID,称为 路由事件 ID。在所有页面路由监听函数中,事件参数中都将携带一个字符串 routeEventId,表示这个路由事件 ID。微信小程序可以通过读取回调中的 routeEventId,来将同一个路由在不同时间节点触发的不同回调进行关联。例如:

const redirectToContext = {};
wx.onBeforeAppRoute(res => {
  if (res.openType === "redirectTo") {
    redirectToContext[res.routeEventId] = { startTime: new Date() };
  }
});
wx.onBeforePageUnload(res => {
  const context = redirectToContext[res.routeEventId];
  if (context !== undefined) {
    context.from = res.page.is;
    context.data = res.page.data;
  }
});
wx.onAfterPageLoad(res => {
  const context = redirectToContext[res.routeEventId];
  if (context !== undefined) {
    console.log(
      `A "redirectTo" route replaced page "${context.from}" to "${
        res.page.is
      }", which is started at ${context.startTime.toString()}`
    );
    res.page.setData(context.data);
    delete redirectToContext[res.routeEventId];
  }
});

这个例子中,我们通过 routeEventId 关联了一次 redirectTo 中的页面创建和页面销毁:在页面销毁时记录了旧页面的数据,并将其应用到了新页面上。

可能的用例

  1. 进行路由上报,方便还原用户使用路径:

    wx.onAppRoute(res => {
      myReportAppRoute(res.timeStamp, res.openType, res.path, res.query);
    });
    
  2. 微信小程序冷启动或热启动时,重置所有状态:

    wx.onBeforeAppRoute(res => {
      if (["appLaunch", "reLaunch", "autoReLaunch"].includes(res.openType)) {
        myGlobalState.reset();
      }
    });
    

    这可以解决一些常见情景,例如微信小程序当前在后台,用户扫码热启动,触发 autoReLaunch 时进行状态清理。

  3. 新页面创建前先进行网络请求,使页面首屏创建和等待网络请求并行进行:

    const pageRequestData = {};
    wx.onBeforePageLoad(res => {
      pageRequestData[res.routeEventId] = new Promise((resolve, reject) => {
        wx.request({
          url: `https://mysite.wechat.qq.com/page-data?path=${res.path}&param=${res.query.param}`,
          success(res) {
            resolve(res);
          },
          fail(res) {
            reject(res);
          }
        });
      });
    });
    wx.onAfterPageLoad(res => {
      pageRequestData[res.routeEventId]
        .then(data => {
          res.page.setData(data);
        })
        .catch(err => {
          console.error("page data init error", err);
        });
    });
    

    当页面比较复杂时,页面创建需要一定时间。这个做法能充分利用页面的创建时间来等待网络请求返回,从而更快地将业务数据应用到页面上,展示给用户。

路由事件重写

从基础库 3.8.0 起,微信小程序可以在路由事件下发到基础库但还未进行实际处理之前,改变这次路由事件的目标页面路径及参数。这有一点类似 HTTP 协议中 URL 重定向的效果,但为了不与现有的 页面重定向 redirectTo 混淆,我们将这种新的特性称为 路由重写(Route rewrite)

为了更好地理解这个特性,你可能需要先了解 路由事件 的相关机制

兼容性

目前支持:

  • 微信安卓客户端 8.0.57 及以上版本
  • 微信 iOS 客户端 8.0.61 及以上版本

更多平台适配正在进行中。

另外,有一些 目前已知的问题,也请留意。

在不兼容的客户端或基础库版本上,可以使用 wx.redirectTo 进行回退兼容,具体参考下面用法中的代码示例。

基本用法

例如,我们可以通过这样的方式将所有跳转到页面 A 的路由都重写到页面 B:

// 添加路由事件处理前的监听
wx.onBeforeAppRoute(res => {
  // 监听触发时,判断事件是否需要重写
  if (res.path === '/pages/A/A') {
    // 重写路由事件
    wx.rewriteRoute({
      url: '/pages/B/B',
      success(res) {
        console.info('Rewrite successfully from A to B')
      },
      fail(res) {
        console.error('Rewrite failed, reason: ' + res.errMsg)
        // 由于兼容性问题或场景不适用等原因重写失败,回退
        wx.redirectTo({
          url: '/pages/B/B',
          complete: console.info
        })
      }
    })
    return
  }
})

在这个例子中,如果有一个目标为 /pages/A/A 的路由事件(例如 navigateTo)下发到基础库,wx.onBeforeAppRoute 监听被触发,wx.rewriteRoute 执行重写后,navigateTo 的目标将变为 /pages/B/B。最终会有一个 B 页面被实例化并压入页面栈。

调用时机

在上面的例子中,路由重写接口 wx.rewriteRoutewx.onBeforeAppRoute 监听中执行。这是因为路由重写只能在路由事件下发到基础库,并且该路由事件还未被执行任何处理之前进行。换句话说,如果这次路由事件已经产生了实际影响(例如路由使旧页面被弹出销毁或者新页面被渲染),那我们就不能再重写这次路由事件了。因此目前有且只有 wx.onBeforeAppRoute 一个时机可以进行路由事件的重写,并且路由重写必须在这个监听的回调中 同步 进行。在 wx.onBeforeAppRoute 的回调以外的地方进行重写或者在回调中异步进行重写会导致重写失败。

目标限制

由于路由重写是改变一个已有路由事件的目标路径,不能改变这个事件的事件类型,因此路由重写需要保证重写后新的目标路径和事件类型是匹配的。例如:switchTab 的目标必须是一个 Tab Bar 页面,因此重写也不能将 switchTab 事件重写到非 Tab Bar 页面。

常见用例

此处的代码片段仅做简单的场景演示

  1. 页面未找到的情况下,回到微信小程序主页
    wx.onBeforeAppRoute(res => {
      if (res.notFound) {
        wx.rewriteRoute({
          url: '/pages/index/index?from-not-found=' + encodeURIComponent(res.path),
        })
      }
    })
    
  2. 线下活动结束后,活动页面下线,用户扫描线下旧物料时引导到新活动页;或者线下物料中写错了路径 / 参数,微信小程序中进行兼容:
    wx.onBeforeAppRoute(res => {
      if (res.path === '/pages/old-or-wrong/activity/page') {
        wx.rewriteRoute({
          url: '/pages/new/activity/page',
          preserveQuery: true,
        })
      }
    })
    
  3. 进入新任务页面时,判断用户是否有上次未完成的任务,继续处理:
    wx.onBeforeAppRoute(res => {
      if (res.path === '/pages/task/new-task') {
        const unfinishedTaskId = globalStatus.unfinishedTaskId
        if (typeof unfinishedTaskId === 'string') {
          wx.rewriteRoute({
            url: '/pages/task/perform-task?taskId=' + unfinishedTaskId,
          })
        }
      }
    })
    
  4. 微信小程序从首页下拉冷启动时,读取 storage 中存储的不同用户身份(例如顾客与商家、学生与家长等),跳转到不同的首页
    wx.onBeforeAppRoute(res => {
      if (res.openType === 'appLaunch') {
        const enterOptions = wx.getEnterOptionsSync()
        if (enterOptions.scene === 1089) {
          const userRole = wx.getStorageSync('user-role')
          if (userRole === 'customer') {
            wx.rewriteRoute({ url: '/pages/customer-index/index' })
          } else if (userRole === 'merchant') {
            wx.rewriteRoute({ url: '/pages/merchant-index/index' })
          } else { /* do nothing */ }
        }
      }
    })
    

对比页面重定向

从最终结果上来看,路由重写与页面重定向 redirectTo 都能达到类似的效果(例如在上面的例子中,最终结果都是新建了一个页面 B 的实例作为栈顶),但二者在执行原理和过程上仍有一定的差别。

页面重定向与原路由事件(例如上例中的 navigateTo)是按顺序排队执行,也就是在执行完页面 A 的渲染任务(例如准备页面渲染环境,实例化页面,处理页面栈逻辑压入页面,渲染页面,触发对应的生命周期等)之后,再处理 redirectTo;而路由重写会在原路由事件下发后处理,框架会直接执行页面 B 的渲染任务。在这个流程中,页面 A 没有被实际实例化或渲染过,因此只渲染了一次页面(redirectTo 实际渲染了两个页面),流程更快、更简单,也更能充分发挥 WebView 预加载 的效果。

当然,也不是所有的 redirectTo 都可以被替换为 rewriteRoute,例如需要由用户选择重定向目标或者从其他页面返回后重定向等情况;另一方面,rewriteRoute 作为一个新能力,并非所有的用户的运行环境都支持路由重写。开发者可以在适用 rewriteRoute 的场景和环境下使用 rewriteRoute,而如果场景不合适或者当前运行环境不支持,则回退使用 redirectTo 进行重定向。

常见问题

  1. 为什么我请求后台接口之后再执行 rewriteRoute 会失败?

    目前微信小程序提供的网络请求接口都是异步接口,发起网络请求之后,JS 运行时会在等待服务器响应时执行其他任务。因此,请求后台并等待后台接口返回时,路由事件实际上已经被处理和执行了。

    理论上,我们也可以使路由事件的处理和执行等待网络请求返回。在等待期间,由于路由事件尚未处理,用户会持续停留在上一个页面(页面跳转的情况下)或者看到白屏(微信小程序启动的情况下),而这段时间的长短取决于网络请求的耗时,从而可能导致用户操作打开或跳转后持续没有响应。为了回避这种情况导致的体验恶化,现阶段我们只处理同步进行的路由重写。

  2. 为什么 wx.rewriteRoute 不像 navigateTo 一样可以直接调用,而是要放在监听的回调中?

    因为相比于 wx.navigateTo 是一次路由请求,对应的 navigateTo 是一种路由事件类型,rewriteRoute 实际上并不是一种路由类型,它的作用是对一次已经存在的路由事件进行一些操作。onBeforeAppRoute 监听会在路由事件下发时触发,在这个回调中我们才能准确地对路由事件进行判断和处理。

常见失败及对应原因

  • not supported

    当前客户端平台或版本不支持路由重写能力

  • rewriteRoute is only allowed in a onBeforeAppRoute callback

    在不正确的时机调用 wx.rewriteRoute(见上方 调用时机)

  • rewriteRoute can only be called once in a route event, this page hash been rewritten to "XXX"

    多次重写了同一个路由事件。每一个路由事件只能被重写一次,可以先计算好最终的目标路径再调用。如果确实需要进行连续的重写,应该等待重写后的路由事件重新触发 onBeforeAppRoute 监听回调,再进行重写

  • a "navigateBack" event is not allowed to be rewritten

    页面返回 navigateBack 事件是不能被重写的(因为目标页面是已经存在的原有页面)

  • rewriting a "XXX" event to to a non-tab page("YYY") is not allowed

  • rewriting a "XXX" event to a tab page("YYY") is not allowed

    重写后的目标页面与路由事件类型不匹配(见上方 目标限制)

  • rewriting a route event that belongs to XXX is not allowed.

    微信小程序不能重写目标为插件页面的路由事件,反之插件也不能重写目标为微信小程序或其他插件的路由事件

已知问题

  1. 目前仅能将路由事件重写到同一分包中的页面,暂不能重写到其他分包的页面。这个问题将在后续的客户端版本中修复

模块化

可以将一些公共的代码抽离成为一个单独的 js 文件,作为一个模块。模块只有通过 module.exports 或者 exports 才能对外暴露接口。

注意:

  • exportsmodule.exports 的一个引用,因此在模块里边随意更改 exports 的指向会造成未知的错误。所以更推荐开发者采用 module.exports 来暴露模块接口,除非你已经清晰知道这两者的关系。
  • 微信小程序目前不支持直接引入 node_modules , 开发者需要使用到 node_modules 时候建议拷贝出相关的代码到微信小程序的目录中,或者使用微信小程序支持的 npm 功能。
// common.js
function sayHello(name) {
  console.log(`Hello ${name} !`)
}
function sayGoodbye(name) {
  console.log(`Goodbye ${name} !`)
}

module.exports.sayHello = sayHello
exports.sayGoodbye = sayGoodbye

​在需要使用这些模块的文件中,使用 require 将公共代码引入

var common = require('common.js')
Page({
  helloMINA: function() {
    common.sayHello('MINA')
  },
  goodbyeMINA: function() {
    common.sayGoodbye('MINA')
  }
})

文件作用域

在 JavaScript 文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。

通过全局函数 getApp 可以获取全局的应用实例,如果需要全局的数据可以在 App() 中设置,如:

// app.js
App({
  globalData: 1
})
// a.js
// The localValue can only be used in file a.js.
var localValue = 'a'
// Get the app instance.
var app = getApp()
// Get the global data and change it.
app.globalData++
// b.js
// You can redefine localValue in file b.js, without interference with the localValue in a.js.
var localValue = 'b'
// If a.js it run before b.js, now the globalData shoule be 2.
console.log(getApp().globalData)