前言
因为公司的小程序项目是使用uniapp脚手架编译的,因为我们在测试阶段和生产阶段基本不会出现,但是用户基数过多的时候就会两遍产生质变,以下内容是我踩过的坑得出的一些结论(使用ai润色了一下)
微信小程序端“白屏”很多时候不是渲染问题,而是以下几类原因导致 JS 逻辑中断或卡死:
- 路由重定向循环、并发跳转、tabBar 跳转方式错误
- 请求层抛异常未捕获(Unhandled Rejection),页面逻辑直接断掉
- 首屏直接落到分包页/鉴权页/深链页,启动竞态导致白屏
- 小程序退到后台太久,回来状态脏了(token/用户态)继续跑导致异常
本文按排查优先级给一套稳定方案,适配 Vue3 + Pinia。
先把白屏变“可见错误”:全局异常捕获(必做)
App.vue(Vue3 写法)
import { onLaunch, onShow } from '@dcloudio/uni-app'
onLaunch(() => {
// Vue3 没有 Vue.config.errorHandler,这里主要补齐小程序错误与未处理 Promise
// #ifdef MP-WEIXIN
wx.onError((msg) => {
console.error('[wx.onError]', msg)
})
wx.onUnhandledRejection((res) => {
console.error('[wx.onUnhandledRejection]', res)
})
// #endif
})
onShow(() => {
// 可选:记录每次回前台
})
</script>
说明:Vue3 侧的 errorHandler 更建议在 main.ts 用 app.config.errorHandler 做(下面会给)。
main.ts(推荐补全 Vue 错误捕获)
import App from './App.vue'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
app.config.errorHandler = (err, instance, info) => {
console.error('[VueError]', err, info)
}
return { app, pinia }
}
(一)排查页面重定向问题:防并发、防循环、统一入口
1.1 常见坑点(白屏高发)
- tabBar 页面用了
navigateTo(会 fail) - A → redirectTo B,B 又 redirectTo A(循环)
- onLoad/onShow 同时触发登录校验 + 活动跳转(并发跳转)
- 启动时频繁
reLaunch/redirectTo(竞态)
1.2 路由统一封装(Vue3 直接用函数)
/src/utils/router.ts
const LOCK_KEY = '__router_lock__'
let lastUrl = ''
let lastAt = 0
const TAB_BAR_PAGES = [
'/pages/index/index',
// '/pages/home/index',
]
function isTabBar(url: string) {
const path = url.split('?')[0]
return TAB_BAR_PAGES.includes(path)
}
export function safeGo(options: { url: string; type?: 'navigateTo' | 'redirectTo' | 'reLaunch' }) {
const { url, type = 'navigateTo' } = options
const now = Date.now()
// 300ms 内重复跳同一地址,直接拦截
if (now - lastAt < 300 && url === lastUrl) {
console.warn('[router] blocked duplicate:', url)
return
}
const app = getApp<{ globalData: Record<string, any> }>()
if (app.globalData[LOCK_KEY]) {
console.warn('[router] locked, ignore:', url)
return
}
app.globalData[LOCK_KEY] = true
lastUrl = url
lastAt = now
const done = () => {
app.globalData[LOCK_KEY] = false
}
if (isTabBar(url)) {
uni.switchTab({ url, complete: done })
return
}
uni[type]({
url,
complete: done,
fail: (e) => {
console.error('[router] fail:', type, url, e)
done()
uni.reLaunch({ url: '/pages/index/index' }) // 兜底回首页
},
})
}
后面文章里涉及跳转,统一用 safeGo(),你会发现白屏概率会明显下降。
(二)排查 http 抛出异常接收:请求层不要随意 throw,统一兜底结构
2.1 推荐:request 永远 resolve(ok/data/msg/code)
/src/utils/http.ts
const BASE_URL = 'https://api.xxx.com'
export type HttpResult<T = any> = {
ok: boolean
data: T | null
msg: string
code: number
}
export function request<T = any>(opt: {
url: string
method?: UniApp.RequestOptions['method']
data?: any
header?: any
timeout?: number
}): Promise<HttpResult<T>> {
return new Promise((resolve) => {
uni.request({
url: BASE_URL + opt.url,
method: opt.method || 'GET',
data: opt.data || {},
header: opt.header || {},
timeout: opt.timeout || 15000,
success: (res) => {
const status = res.statusCode || 0
const body: any = res.data || {}
// 按你们后端协议调整:例如 code === 0 成功
if (status >= 200 && status < 300) {
if (body.code === 0) {
resolve({ ok: true, data: body.data ?? null, msg: body.msg || 'ok', code: body.code })
} else {
resolve({ ok: false, data: null, msg: body.msg || '业务异常', code: body.code ?? -2 })
}
} else {
resolve({ ok: false, data: null, msg: `HTTP ${status}`, code: status })
}
},
fail: (err) => {
console.error('[http.fail]', err)
resolve({ ok: false, data: null, msg: err.errMsg || '网络异常', code: -1 })
},
})
})
}
2.2 页面调用必须兜底(不要默认成功)
import { request } from '@/utils/http'
const res = await request<{ list: any[] }>({ url: '/list' })
if (!res.ok) {
uni.showToast({ title: res.msg, icon: 'none' })
return
}
list.value = res.data?.list || []
这样你几乎不会再遇到“请求失败直接白屏”,因为页面逻辑永远能走到兜底分支。
(三)进入页面一定先进入首页:缓存记录再重定向(首屏稳定策略)
你的目标:无论从哪里打开小程序,先落首页渲染,再二跳到目标页。
这能解决:分包未加载、鉴权竞态、启动时多处跳转造成的白屏。
3.1 Pinia 用户态(用于清理等)
/src/stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: uni.getStorageSync('userInfo') || null as any,
}),
actions: {
setToken(t: string) {
this.token = t
uni.setStorageSync('token', t)
},
setUserInfo(info: any) {
this.userInfo = info
uni.setStorageSync('userInfo', info)
},
reset() {
this.token = ''
this.userInfo = null
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
},
},
})
3.2 App 启动记录目标页,并强制 reLaunch 首页 App.vue
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { useUserStore } from '@/stores/user'
const EXPIRE_MS = 30 * 60 * 1000 // 30分钟
function buildTargetFromLaunchOptions(): string {
// #ifdef MP-WEIXIN
const opts = wx.getLaunchOptionsSync?.() || {}
if (!opts.path) return ''
const path = '/' + opts.path
const query = opts.query || {}
const qs = Object.keys(query).length
? '?' + Object.keys(query).map(k => `${k}=${encodeURIComponent(query[k])}`).join('&')
: ''
const full = path + qs
if (full.startsWith('/pages/index/index')) return ''
return full
// #endif
return ''
}
function clearUserData() {
const user = useUserStore()
user.reset()
uni.removeStorageSync('pending_redirect')
}
onLaunch(() => {
// 1) 记录目标页
const target = buildTargetFromLaunchOptions()
if (target) uni.setStorageSync('pending_redirect', target)
// 2) 强制先落首页
uni.reLaunch({ url: '/pages/index/index' })
})
onHide(() => {
uni.setStorageSync('last_hide_time', Date.now())
})
onShow(() => {
const last = Number(uni.getStorageSync('last_hide_time') || 0)
if (!last) return
const gap = Date.now() - last
if (gap > EXPIRE_MS) {
console.warn('[expire] too long:', gap)
clearUserData()
uni.reLaunch({ url: '/pages/index/index' })
}
})
</script>
3.3 首页负责“二跳重定向”(只执行一次) /pages/index/index.vue
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app'
import { safeGo } from '@/utils/router'
onShow(() => {
const target = uni.getStorageSync('pending_redirect')
if (!target) return
// 先清掉,避免循环
uni.removeStorageSync('pending_redirect')
// 延迟一帧,确保首屏先渲染出来
setTimeout(() => {
safeGo({ url: target, type: 'redirectTo' })
}, 0)
})
</script>
这套模式非常“抗白屏”:启动无论多复杂,先给用户一个能渲染的首页,再做业务跳转。
四)退出时长处理:超过时长清除用户数据(Pinia 版)
上面 App.vue 已经实现了“退后台超时清理”。这里补充两个经验点:
4.1 不建议清 uni.clearStorageSync()
会误删配置/引导标记/AB 实验等数据。只清用户相关即可(token、userInfo、pending_redirect)。
4.2 如果要“页面级超时”
对敏感页面可叠加页面级超时(业务更安全),但全局超时依然建议保留。
最后:一套白屏排查优先级(直接照这个顺序查)
- 路由:tabBar 跳转方式正确吗?有无重定向循环?有无并发跳转?
- 请求:有没有 throw 未 catch?有没有 unhandledRejection?请求失败有没有兜底?
- 首屏策略:是否允许启动直接落到深层页?建议“先首页渲染,再二跳”。
- 后台超时:用户退后台太久回来,是否需要清理 token/用户态避免脏状态引发白屏?