支付宝扫码进入自研小程序,第二次打开出现白屏
结论
出现“第一次正常、退出后第二次扫码白屏”,通常不是扫码功能失效,而是小程序再次被唤起时复用了上一次的运行实例,业务代码却只在首次启动或页面首次加载时执行初始化。
常见原因包括:
- 初始化逻辑只写在
App.onLaunch或Page.onLoad中,没有处理App.onShow、Page.onShow。 onHide、onUnload清除了数据、定时器或监听,二次进入时没有恢复。- 二维码参数、支付单号或临时令牌只能使用一次。第二次请求失败后,页面又没有错误提示。
- 全局变量、登录状态、请求锁或加载状态没有复位。
- 二次扫码进入了不同路径,或者启动参数解析失败。
- 页面出现 JavaScript 异常、接口失败或路由失败,而界面完全依赖条件渲染,最终表现为白屏。
排查时,先用真机日志确认第二次扫码触发了哪些生命周期、收到了什么启动参数,并找到出现的第一个异常,再根据结果修复。
为什么第二次扫码不一定会重新启动
用户退出小程序后,进程不一定会立刻销毁。短时间内再次扫码时,支付宝可能只是将已有实例从后台切回前台。
生命周期可能是:
- 第一次进入:
App.onLaunch→App.onShow→Page.onLoad→Page.onShow - 退出后再次扫码:
App.onShow→ 已存在页面的Page.onShow
第二次进入时,App.onLaunch 和当前页面的 Page.onLoad 未必会再次执行。如果扫码参数读取、订单查询或页面初始化只放在这两个生命周期中,页面就可能继续使用旧状态,或者拿到空数据。
推荐的处理方式
1. 在 App.onShow 中接收每次进入参数
首次启动和从后台返回时,都要保存最新的启动参数,不能只依赖 onLaunch。
App({
globalData: {
enterOptions: null,
enterVersion: 0
},
onLaunch(options) {
this.updateEnterOptions(options);
},
onShow(options) {
this.updateEnterOptions(options);
},
updateEnterOptions(options) {
this.globalData.enterOptions = options || {};
this.globalData.enterVersion += 1;
console.info('[app] enter options:', options);
}
});
实际参数结构由二维码配置方式和支付宝提供的启动参数决定。不要默认业务参数一定在某个固定字段中,应以真机日志和当前接入方式为准。
2. 在页面 onShow 中重新校验并初始化
扫码业务通常需要在 onShow 中处理“本次进入”,同时避免 onLoad 和 onShow 重复发起请求。
Page({
data: {
loading: true,
errorMessage: '',
order: null
},
_handledEnterVersion: -1,
_requesting: false,
onShow() {
const app = getApp();
const version = app.globalData.enterVersion;
if (this._handledEnterVersion === version) {
return;
}
this._handledEnterVersion = version;
this.initialize(app.globalData.enterOptions);
},
async initialize(options) {
if (this._requesting) {
return;
}
this._requesting = true;
this.setData({
loading: true,
errorMessage: '',
order: null
});
try {
const params = this.parseEnterParams(options);
if (!params.orderId) {
throw new Error('缺少 orderId');
}
const order = await this.queryOrder(params.orderId);
this.setData({
order,
loading: false
});
} catch (error) {
console.error('[page] initialize failed:', error);
this.setData({
loading: false,
errorMessage: error.message || '页面加载失败,请重新扫码'
});
} finally {
this._requesting = false;
}
},
parseEnterParams(options) {
const query = options && options.query ? options.query : {};
return {
orderId: query.orderId || ''
};
},
queryOrder(orderId) {
return new Promise((resolve, reject) => {
my.request({
url: 'https://example.com/api/orders/detail',
method: 'GET',
data: { orderId },
success: (response) => {
if (response.status === 200 && response.data) {
resolve(response.data);
} else {
reject(new Error('订单查询失败'));
}
},
fail: (error) => {
reject(error);
}
});
});
}
});
其中的接口地址、响应结构和启动参数字段仅用于示例,需要替换成实际业务定义。
3. 页面必须提供加载和错误状态
如果页面使用 a:if="{{order}}" 之类的条件渲染,接口失败后 order 一直为空,页面就不会显示任何内容。加载中、加载失败和正常内容都应该有明确的界面状态。
<view class="page">
<view a:if="{{loading}}" class="status">
正在加载…
</view>
<view a:elif="{{errorMessage}}" class="status error">
{{errorMessage}}
</view>
<view a:else class="content">
<text>订单号:{{order.orderId}}</text>
<text>金额:{{order.amount}}</text>
</view>
</view>
即使接口发生异常,也应向用户显示错误信息,而不是留下白屏。
按顺序排查
检查生命周期
先在以下位置打印带时间的日志:
App({
onLaunch(options) {
console.info('[lifecycle] App.onLaunch', Date.now(), options);
},
onShow(options) {
console.info('[lifecycle] App.onShow', Date.now(), options);
},
onHide() {
console.info('[lifecycle] App.onHide', Date.now());
}
});
Page({
onLoad(query) {
console.info('[lifecycle] Page.onLoad', Date.now(), query);
},
onShow() {
console.info('[lifecycle] Page.onShow', Date.now());
},
onHide() {
console.info('[lifecycle] Page.onHide', Date.now());
},
onUnload() {
console.info('[lifecycle] Page.onUnload', Date.now());
}
});
如果第二次扫码只触发 App.onShow 和 Page.onShow,就说明二次初始化不能依赖首次生命周期。
对比两次启动参数
需要逐项比较:
- 进入路径是否相同。
query是否为空。- 参数是否经过 URL 编码。
- 二维码是否包含一次性 token。
- 第二次进入时是否仍停留在上一次的页面,而没有进入二维码指定的目标页面。
- 业务参数是否被全局旧数据覆盖。
日志中最好直接输出完整对象,不要只打印拼接后的字符串,否则容易漏掉嵌套字段。
检查控制台第一个异常
白屏通常只是结果,原因可能是:
- 读取了
undefined的属性。 JSON.parse收到空字符串或非法内容。decodeURIComponent处理了不合法的编码。- 请求失败后,代码仍继续读取响应数据。
setData使用了错误的数据结构。- 自定义组件在二次显示时抛出异常。
- 路由目标不存在或参数过长。
先处理第二次进入后出现的第一个异常。后面的报错往往是它引发的连锁反应。
检查请求锁和加载状态
下面的判断很容易让二次进入跳过请求:
if (app.globalData.hasInitialized) {
return;
}
如果首次进入后 hasInitialized 已被设为 true,退出时页面数据又被清空,第二次进入就会跳过初始化,最终显示空页面。
请求锁也要在成功和失败后都释放:
async loadData() {
if (this._requesting) {
return;
}
this._requesting = true;
try {
await this.fetchData();
} catch (error) {
console.error(error);
} finally {
this._requesting = false;
}
}
还要确认失败分支会把 loading 恢复为 false。
检查二维码参数是否只能使用一次
扫码支付等场景经常使用一次性订单号、授权码或临时 token。第一次扫码时,即使用户没有点击按钮,页面初始化接口也可能已经完成“领取”“锁定”或“核验”,从而改变服务器状态。
再次使用同一二维码时,服务端可能返回:
- token 已使用;
- token 已过期;
- 订单已锁定;
- 会话状态不匹配;
- 不允许重复进入。
前端需要识别这些响应并显示清楚的提示,不能继续按照成功数据渲染。
if (response.data.code === 'TOKEN_USED') {
this.setData({
loading: false,
errorMessage: '二维码已使用,请重新获取二维码'
});
return;
}
同一个二维码能否重复进入,应由业务规则决定。如果业务允许重复扫码,服务端查询接口就应保持幂等,不能因为用户打开页面就消耗凭证。
检查退出时的清理逻辑
检查 App.onHide、Page.onHide 和 Page.onUnload 是否执行了以下操作:
- 清空全局扫码参数;
- 清除用户或订单状态;
- 停止定时器后没有重新创建;
- 移除事件监听后没有重新注册;
- 中止请求后没有重置请求状态;
- 将页面状态设为空;
- 关闭 WebSocket 后没有重新连接。
清理和恢复必须成对处理。例如:
Page({
onShow() {
this.startPolling();
},
onHide() {
this.stopPolling();
},
startPolling() {
this.stopPolling();
this._timer = setInterval(() => {
this.refreshOrder();
}, 3000);
},
stopPolling() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
}
});
不建议直接依赖强制重载
重新跳转页面可以用来验证问题是否与页面状态复用有关,但不适合作为首选修复方案。频繁调用 my.reLaunch 可能遮住生命周期、状态管理或接口幂等方面的问题,也可能引起跳转循环。
如果确实需要重新进入指定页面,应先检查当前路径和参数,并增加防重复标记:
const app = getApp();
if (!app.globalData.relaunching) {
app.globalData.relaunching = true;
my.reLaunch({
url: '/pages/pay/index?orderId=123',
complete: () => {
app.globalData.relaunching = false;
}
});
}
项目能否使用 my.reLaunch,以及目标页面是否可以作为启动页面,需要根据当前支付宝小程序的运行环境和路由配置确认。
最终修复原则
扫码进入类小程序应将每次进入都当作新的业务会话:
- 在
App.onShow中获取并保存最新进入参数。 - 在目标页的
Page.onShow中判断是否需要重新初始化。 - 不要用永久性的
hasInitialized阻止后续进入。 - 请求锁、加载状态和定时器都要能够正确复位。
- 服务端接口需要明确处理二维码重复使用和过期情况。
- 遇到异常或空数据时,页面应显示可见的错误信息。
- 修复后使用真机连续扫码测试,覆盖短时间返回、长时间停留后台、重复使用二维码和网络失败等情况。
如果第二次扫码时生命周期正常,启动参数也与第一次一致,就应继续检查首次页面请求是否已经消耗二维码凭证,以及服务端对第二次请求返回了什么。
备注:内容仅供参考。
