微信小程序 Behavior 拦截 onShow 做异步守卫:definitionFilter 的三个坑
在小程序里做页面级权限守卫,最直觉的方案是在每个页面的 onShow 头部塞一段鉴权逻辑。但一旦需要守卫的页面多了,维护起来极为分散。于是自然想到用 Behavior 统一拦截:通过 definitionFilter 在编译期替换 onShow,等异步鉴权通过之后再执行原始 onShow。
思路很美,但踩下去是三个连环坑。
现象
按上述方案跑起来,日志看到鉴权接口正常返回 granted,守卫组件也正确切换到展示态——但页面内的 API 请求始终没有发出,骨架屏一直卡着。抓包确认:loadData() 根本没被调用。
根因
坑 1:originalOnShow.apply() 静默失效
definitionFilter 执行时机是编译期(Behavior({...}) 求值阶段),此刻 defFields.onShow 是一个还没有绑定任何 this 上下文的裸函数引用。apply(this, arguments) 在运行时调用时,this 指向的是 Behavior 实例而非 Page 实例——小程序框架不会替你做绑定。结果是:业务 API 调用进了 Behavior 的空壳作用域,静默无响应。
definitionFilter 在 Page 构造之前运行,originalOnShow.apply(this, ...) 中的 this 是 Behavior 对象,不是最终挂载的页面对象。API 调用、this.setData、this.route 全部指向错误上下文,且不会报错。
坑 2:快速前后台切换导致 _pendingOnShow 重复执行
onShow 会在每次页面重新可见时触发(从后台切回、从子页面返回、收起弹窗……)。如果用户在第一次鉴权还没完成时再次切到前台,第二次 onShow 会覆盖 _pendingOnShow,但第一个异步回调已经在飞行中。等两个回调先后落地,loadData() 和埋点事件各被触发两次。
版本号(计数器)是唯一可靠的解法:每次 onShow 自增一个 _checkVersion,回调拿到结果时对比版本,过期的直接丢弃。
onShow #1 → version=1 → 鉴权请求发出
用户切后台再切回
onShow #2 → version=2 → 鉴权请求发出,_pendingOnShow 被覆盖
请求 #1 回调 → resolveGuard(version=1) → 1 !== 2 → 丢弃 ✓
请求 #2 回调 → resolveGuard(version=2) → 2 === 2 → 执行 ✓
不加版本号:两个回调都执行,loadData 被调用两次。
坑 3:wx:else 让守卫形同虚设
守卫组件用三态(checking / denied / granted)控制 slot 渲染。很多人习惯写:
<view wx:if="{{state === 'checking'}}">加载中...</view>
<view wx:elif="{{state === 'denied'}}">无权限</view>
<slot wx:else></slot>wx:else 语义是"前面所有条件都不匹配时"——包括 state 为 undefined、空字符串、字段改名、Behavior 未正确挂载等情况。任意一种异常都会让受保护内容直接透出。
正解
三个坑对应三个修法:
坑 1 的修法:彻底放弃 definitionFilter 在页面级守卫的用法,改用自管理 Component 模式(独立组件持有守卫状态,通过事件通知页面执行业务逻辑)。Component 有完整的实例生命周期,不存在 this 绑定歧义。
坑 2 的修法:每次 onShow 自增版本号,回调时校验:
defFields.onShow = function () {
this._checkVersion = (this._checkVersion || 0) + 1
this._pendingOnShow = originalOnShow
this._doAsyncCheck(this._checkVersion)
}
_resolveGuard(state, version) {
if (version !== this._checkVersion) return // 过期回调,丢弃
this.setData({ _guardState: state })
if (state === 'granted' && typeof this._pendingOnShow === 'function') {
const fn = this._pendingOnShow
this._pendingOnShow = null // 防止同版本重复执行
fn.apply(this, this._pendingOnShowArgs)
}
}坑 3 的修法:slot 渲染条件必须显式写 wx:elif="{{state === 'granted'}}" ,不允许用 wx:else:
<!-- 错误:state 异常时内容透出 -->
<slot wx:else></slot>
<!-- 正确:未知态 = 空白,fail-closed -->
<slot wx:elif="{{state === 'granted'}}"></slot>- 守卫 slot 永远用
wx:elif="{{state === 'granted'}}",不用wx:else - 异步鉴权回调必须携带版本号,过期回调直接丢弃
definitionFilter适合注入初始 data / 改 template,不适合做需要正确this的生命周期代理;页面级守卫优先用 Component 自管理模式
一句话外卖
definitionFilter是编译期钩子,this不可信;异步守卫遇上可重入生命周期必须加版本号;权限渲染条件永远写显式断言,不写 else。