一次useRouter空值的奇妙追踪

作者:陆晨杰

一、背景

又是一个平凡的一天,小路同学正在愉快的码代码,突然一个异常告警跳到了小路同学的眼前

点开一看,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;
  }
};
相关推荐
Dream耀1 小时前
FitKick 电商APP项目总结二
前端·javascript·react.js
Spider_Man4 小时前
AI图片识别英语学习神器开发实录——让单词和图片一起飞!
前端·react.js·llm
胡西风_foxww4 小时前
Jotai:React轻量级状态管理新选择
前端·react.js·前端框架·状态管理·jotai
Java陈序员4 小时前
免费看片!一个开箱即用的、跨平台的影视聚合播放器!
react.js·docker·next.js
sophie旭4 小时前
《深入浅出react》总结之 11. 2. 2 renderWithHooks 执行函数
前端·react.js·源码
labixiong4 小时前
你真的了解位运算吗?从基础概念到前端框架中的应用
前端·vue.js·react.js
sophie旭5 小时前
《深入浅出react》总结之 11. 2. 3 Hooks 初始化流程- useState
前端·react.js·源码
江城开朗的豌豆6 小时前
告别Class组件!用useEffect玩转React生命周期
前端·javascript·react.js
江城开朗的豌豆6 小时前
React状态管理:从Context到Redux,我的选型心得
前端·javascript·react.js