一、 现象:诡异的“套娃”页面
你是否遇到过这种场景:在小程序中点击一个按钮跳转页面,结果页面跳了两次,甚至多次?用户回退时,需要连续按好几次返回键才能回到首页。
这种情况在分包加载时尤为明显。
二、 诱因:分包加载的“时间差”
为了优化首屏加载速度,我们通常会采用分包策略。例如以下配置:
JSON
{
"subPackages": [
{ "root": "pages/index/", "pages": ["index"] },
{ "root": "pages/post/", "pages": ["details", "list"] }
]
}
当用户从主包跳转到 pages/post/details 时,如果该分包尚未下载,小程序会进入“异步加载”状态。在 Android 设备上,通常会显示 “模块加载中”。
问题的关键在于:
在分包下载的过程中,当前页面的逻辑层并未阻塞。如果用户因为网络慢产生焦虑并连续点击,handleArticleTap 事件会被多次触发。由于异步任务正在排队,小程序框架会在分包加载完成后,一口气执行掉所有排队的 MapsTo 指令,从而导致页面被重复打开。
三、 踩坑:为什么常规“加锁”会失效?
1. 尝试使用 success 回调
最初的想法可能是:跳转成功后解锁。
JavaScript
handleArticleTap(e) {
if (this.isNavigating) return;
this.isNavigating = true;
wx.navigateTo({
url: '/pages/post/details?pid=1',
complete: () => {
// 坑点:这里的 complete 或 success 触发时机极早
// 往往在分包还没下载完、页面还没真正拉起时就执行了
this.isNavigating = false;
}
});
}
结论: 失败。wx.navigateTo 的回调代表的是“调用指令成功”,而非“目标页面渲染完成”。
2. 尝试 onShow 生命周期
如你所发现的,利用生命周期锁死跳转状态是一个可行的思路:
JavaScript
// 发起跳转的页面
onShow() {
this.isNavigating = false; // 页面回来时解锁
},
handleArticleTap(e) {
if (this.isNavigating) return;
this.isNavigating = true;
wx.navigateTo({ url: '...' });
}
结论: 有效,但存在局限性。如果一个页面有 10 个跳转按钮,你需要在每个 Page 里重复写这些逻辑,甚至要在 onHide 或 onUnload 中小心翼翼地维护状态。
四、 进阶:更优雅的全局解决方案
在实际开发中,我们追求的是低侵入性和通用性。我们可以通过“函数节流(Throttle)”或“全局路由拦截”来一劳永逸。
方案 A:高阶函数拦截(推荐)
封装一个防重点击的工具函数,避免在每个 Page 里手动维护 lock 变量。
JavaScript
// utils.js
export const throttleNavigate = (fn, gapTime = 1000) => {
let _lastTime = null;
return function () {
let _nowTime = + new Date();
if (_nowTime - _lastTime > gapTime || !_lastTime) {
fn.apply(this, arguments); // 距离上次跳转超过1秒才允许再次执行
_lastTime = _nowTime;
}
};
};
// 页面中使用
import { throttleNavigate } from '../../utils/util';
Page({
handleArticleTap: throttleNavigate(function(e) {
const { pid } = e.currentTarget.dataset;
wx.navigateTo({ url: `/pages/post/details?pid=${pid}` });
})
})
方案 B:重写 wx.navigateTo
如果你想在不改动任何业务代码的前提下解决问题,可以尝试对全局 API 进行增强(Monkey Patch)。
JavaScript
// app.js
const originalNavigateTo = wx.navigateTo;
let isNavigating = false;
wx.navigateTo = function (options) {
if (isNavigating) return;
isNavigating = true;
const { complete } = options;
options.complete = function (res) {
// 延迟 500ms 解锁,给页面跳转预留足够的响应时间
setTimeout(() => { isNavigating = false; }, 500);
complete && complete(res);
};
return originalNavigateTo(options);
};
五、 总结
小程序分包导致的重复跳转,本质上是异步资源加载与同步用户交互之间的矛盾。
- 初级解法: 使用
onShow手动加锁。 - 中级解法: 封装
throttle装饰器函数,控制点击频率。 - 高级解法: 在框架底层或全局路由封装处进行统一拦截。
在开发过程中,建议始终保持“防重触发”的意识,不仅是为了解决分包加载问题,更是为了提升复杂网络环境下用户的交互体验。

其实这种情况不光会出现在分包中,不是分包的页面中也会出现这种情况,只要反应速度慢一点点就可以实现连击现象,不一定是两次,也许是无限次,只要点击速度够快。
这一万多个关注咋刷的