作者:陆晨杰
一、背景
又是一个平凡的一天,小路同学正在愉快的码代码,突然一个异常告警跳到了小路同学的眼前
点开一看,TypeError: null is not an object 是小程序的取值异常的问题,自信的小路同学觉得应该是接口返回了null,于是准备按照报错堆栈,找到对应的接口,准备对后端发起冲锋
随着目光向后一瞥,嗯?!不对,这是什么鬼?TypeError: null is not an object (evaluating '(0,X.useRouter)().params') 这不是Taro的代码获取链接参数的方法么,这方法应该有一个空对象的默认值才对啊,这不科学;避免了勿喷队友的小路同学被这个报错勾起了一点兴趣,觉得停下手头的事情,去看一看这个问题
二、参数的来源
以下的代码分析基于Taro 3.6.29 版本进行分析
方法的定义
小路同学先点开了 Taro 代码中 useRouter 的TS定义,确定了按照定义,useRouter 不应该返回 null ,那应该是Taro逻辑不定义不一致,有什么特殊场景下返回了 null ;于是开始分析 useRouter 参数的返回逻辑,尝试找出不合理的地方
typescript
// packages/taro-runtime-rn/src/hooks.ts
...
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useRouter = (dynamic = false) => {
return dynamic ? Current.router : React.useMemo(() => Current.router, [])
}
...
// packages/taro-runtime-rn/src/current.ts
...
export const Current: Current = {
app: null,
router: null,
page: null,
rnNavigationRef // RN 导航实例私有对象
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const getCurrentInstance = () => Current
小路同学找到了useRouter实现的地方,并发现了他的默认值为null,同时还有个 dynamic 参数把 router 锁住了,破案了!肯定是因为 router 没有更新,导致获取报错了
于是信心满满的小路同学将 dynamic 至为了 true,继续去码代码并等待到发布窗口后发布
参数的更新
待到发布后几天,无所事事的小路同学在检查报错的时候,突然有发现了这个异常,顿时觉得不对劲,明明已经解决了呀,于是小路同学决定继续检查一下报错原因;经过了一段时间的检索,小路同学发现了参数更新的地方;
该方法对小程序原有 Page 方法参数进行的覆写,小路同学一眼就看见了 setCurrentRouter 的方法,并且发现他会在 onLoad 中对 router 进行更新,因此更疑惑了,对啊,定义没有错,有值啊!正当一头雾水之际,发现 onHide 中会讲 router 至为null,难道破案了!吃一堑长一智的小路同学决定实践出真知,去尝试一番
typescript
export function createNativePageConfig (Component, pageName: string, data: Record<string, unknown>, react: typeof React, reactdom: typeof ReactDOM, pageConfig) {
...
function setCurrentRouter (page: MpInstance) {
const router = page.route || page.__route__ || page.$taroPath
Current.router = {
params: page.$taroParams!,
path: addLeadingSlash(router),
$taroPath: page.$taroPath,
onReady: getOnReadyEventKey(id),
onShow: getOnShowEventKey(id),
onHide: getOnHideEventKey(id)
}
if (!isUndefined(page.exitState)) {
Current.router.exitState = page.exitState
}
}
const pageObj: Record<string, any> = {
options: pageConfig,
[ONLOAD] (this: MpInstance, options: Readonly<Record<string, unknown>> = {}, cb?: TaroGeneral.TFunc) {
...
setCurrentRouter(this)
...
},
[ONUNLOAD] () {
...
resetCurrent()
...
},
[ONSHOW] (options = {}) {
hasLoaded.then(() => {
// 设置 Current 的 page 和 router
Current.page = this as any
setCurrentRouter(this)
...
})
},
[ONHIDE] () {
...
if (Current.page === this) {
Current.page = null
Current.router = null
}
...
},
}
...
}
都是异步的错
兴冲冲的小路同学立马 init 了一个模版,并尝试去复现这个异常,然而出乎意料的是,竟然可以获取到 router ,并不是这个情况导致的报错,小路同学表示大受震撼,并一筹莫展,想着直接TS ?. 一把梭,不报错就行了
typescript
const Example = () => {
const router = useRouter();
useDidHide(() => {
console.log('hide', router);
});
}
正当小路同学自暴自弃的去尝试修复时,随便一看,看见了组件渲染的地方有一个三元表达式!组件会根据灰度逻辑进行渲染!难道!于是小路同学立马开始了以下尝试

组件中会调用 useRouter 方法,并且组件需要接口返回后再能确认是否渲染,在接口请求过程中将小程序隐藏到后台,果然复现了这个问题!
三、解决问题
知道了为什么就好解决了,我们无法从 Taro 的层面来解决这个问题,于是写了一个 babel 插件,对所有的 useRouter 方法替换成了自己覆写的方法,经过测试,即使在 onHide 之后,getCurrentPages 仍可以获取参数的路径,我们可以用它来顶一下异常的场景
typescript
import { useRouter as useTaroRouter, RouterInfo, getCurrentPages } from '@tarojs/taro';
interface ReturnRouter<T> extends Partial<Omit<RouterInfo, 'params'>> {
params: T;
}
export const useRouter = <T extends Record<string, string>>(dynamic?: boolean): ReturnRouter<T> => {
const defaultParams = { params: {} as T };
try {
const taroParams = useTaroRouter(dynamic);
if (!taroParams) {
try {
const pages = getCurrentPages();
const page = pages && pages[pages?.length - 1];
const options = page?.$taroParams || page?.options;
if (options) {
return { params: options } as ReturnRouter<T>;
}
} catch {
}
return defaultParams;
}
if (!taroParams.params) {
}
return taroParams as ReturnRouter<T>;
} catch (err) {
return defaultParams;
}
};