小程序坑位记录:wx.navigateTo 与 wx.reLaunch 的执行冲突

现象描述

在开发微信小程序时发现一个很有意思的 Edge Case:

当我在 Page A 调用 wx.navigateTo 跳转到 Page B,并在 Page BonLoad(或 onShowonReady)生命周期中立刻执行 wx.reLaunch 时,会导致页面彻底白屏。

等待约 10 秒左右,控制台会先后抛出两个 Timeout 错误:

  1. navigateTo:fail timeout
  2. reLaunch:fail timeout

130d47b4238d0508310c15ad39e3140b

逻辑分析

根据表现来看,这应该是小程序底层路由状态机的一个死锁现象。

  1. 路由锁冲突navigateTo 在执行过程中会占用路由锁,直到目标页面生命周期初始执行完成,才算一次完整的跳转切换。
  2. 重置请求被挂起:在 onLoad 中执行 reLaunch 时,由于前一个 MapsTo 的任务流尚未完全 Close,reLaunch 无法立刻获取路由控制权,进入等待队列。
  3. 循环等待reLaunch 的指令理论上应该销毁当前页面栈,但它又在等待 navigateTo 确认“跳转已完成”;而 navigateTo 因为页面即将被销毁(由于 reLaunch 的介入),无法正常进入 Ready 状态。

结果就是两个 API 互相卡住,直到触发底层设定的超时阈值。

实验结论

在页面尚未完成 navigateTo 的跳转之前,不要尝试立即进行全局路由重置。

如果业务上确实需要在进入页面后立即根据逻辑(比如权限校验失败)重定向回首页,目前最简单的绕过方案是给 reLaunch 加一个稍长的 setTimeout,这样几乎就不会有问题了:

// Page B.js
onLoad() {
  // 确保 navigateTo 的任务流先走完
  setTimeout(() => {
    wx.reLaunch({
      url: '/pages/index/index'
    });
  }, 500);
}

更优雅的方案

我相信绝大多数程序员,在看到 setTimeout 这种硬等的解决方案的时候都会有一些不适,所以这里也提供一个更优雅的方案。

我们可以尝试对 wx 对象进行一层代理,通过一个全局的 isRouting 标志位来管理路由状态。

  1. 核心思路:拦截与队列 我们可以重写 wx.navigateTo 和 wx.reLaunch。

当 navigateTo 开始时,设置 isRouting = true。

当跳转完成(success 或 complete 回调触发)时,释放锁。

如果执行 reLaunch 时发现锁未释放,则将任务挂起,等待锁释放后再执行。

  1. 代码实现

你可以将这段逻辑封装在 app.js 或者其他能在全局优先加载的 js 中:

const originalNavigateTo = wx.navigateTo;
const originalReLaunch = wx.reLaunch;

let isRouting = false; // 路由锁定标志位

// 劫持 navigateTo
wx.navigateTo = function (options) {
  isRouting = true;
  const { complete } = options;
  
  options.complete = function (...args) {
    isRouting = false; // 页面入栈流程结束,释放锁
    complete && complete.apply(this, args);
  };
  
  return originalNavigateTo.call(this, options);
};

// 劫持 reLaunch
wx.reLaunch = function (options) {
  if (isRouting) {
    // 如果当前正在路由中,则递归等待锁释放
    console.warn('[Router] Detected routing conflict, queuing reLaunch...');
    return setTimeout(() => wx.reLaunch(options), 50); 
  }
  return originalReLaunch.call(this, options);
};