引言

底层实现有时候不必循规蹈矩,能解放业务层才是关键。如果底层为了守规矩反而破坏了业务层的规矩才是得不偿失的添乱行为,行话叫过度设计。
其实这也是所谓封装的原则之一,从函数到模块到组件到页面再到工程的设计都应当秉持着为更上一层服务的原则,尽可能地把下一层的脏话累活揽到自己的作用域下,屏蔽到角落里。
说到这,突然想到一个词:金玉其外 败絮其中……

问题背景

项目使用了 hox + useRequest,详情见此文:hox + useRequest:异步数据流管理从未如此轻松自然!
然而,上文中的轻松自然是依赖于 1.x 的 useRequest,该版本默认使用的请求库是 umi-request。如此工程可以基于 umi-requestmiddleware 来配置一些全局的请求特性,如:

  • 错误提示
  • loading 提示
  • mock 数据
  • 返回数据格式化
  • 鉴权
  • 等…

做了以上工作后,理论上能够实现所有的请求错误(http 或业务异常)都由「上层」来处理,在业务层完全不需要关心请求的异常情况,只需要去读取 datarun().then() 就好:

1
2
3
4
5
6
const { run } = useRequest('users', { manual: true })

async function fetchUsers(params) {
await run(params)
tip('请求成功') // 能走到这一行就一定代表请求成功了,失败的情况完全不必关心
}

不难发现要实现上面的操作必须得在请求错误时不让 run 进行 resolve。 之前想当然地以为在「上层」抛出一个异常就会打断 resolve(即使触发了 run 的 catch 也无所谓,虽然控制台会报错, 但流程上还是符合预期的)

然而我错了,run 方法内部做了 catch,请求结束后对于上层来说会始终可以 then 到。

于是搜索到相关的 issues

绿框部分深得我心!这不就是我要的滑板鞋么?!

项目并没有独立安装 useRequest,而是使用的 umi hooks,那就整个一起更新吧!来拥抱 ahooks!这个里面集成的就是最新的 useRequest!

然后,然后项目就崩了。。

原因是 2.x 的 useRequest 的默认请求库不再是 umi-request,而是 Fetch。那么理所当然之前配置给 umi-request 的那些所谓「上层」配置都没卵用了。 不过没关系,看看能不能基于新版 useRequest 的 options 迁移配置。答案当然是:不能!(不然还水毛线文章。。

原因是 useRequest 提供的「钩子」有限,很多请求特性很难基于它们实现。想想也是, useRequest 的定位本就是一个方便管理请求状态的 hooks,更多请求层的东西应该交给专业的请求库来做,那么就是 requestMethod 配置闪亮登场的时候了!该配置项可以设置 useRequest 的默认请求库。

那么,当前项目最简单的兼容方案肯定就是把 requestMethod 设置成 umi-request 了,这样理论上就和之前的 1.x 就没啥区别了。说干就干,在入口组件处通过 UseRequestProvider 注入全局配置。

1
<UseRequestProvider value={options}>{children}</UseRequestProvider>

就这?

当然不!尝试刷新页面,请求异常依旧。。。甚至和注入全局配置之前没啥两样。多年的写 bug 经验告诉我应该是我传入的配置没有生效,于是在 options.requestMethod 中尝试 log,运行后果然并没有 log。又尝试在那个报错的请求处单独配置 requestMethod,成功 log!说明问题还是在全局注入这里。

可是明明我就是在入口文件的根结点注入的!翻了 UseRequestProvider 的源码,也的的确确是个普普通通的 ContextProvider,而 Context 注入不成功的原因基本就只有一个:Consumer 不是 Provider 的子节点

于是开始定位第一个异常请求的发起方,果然发起方比较可疑,是一个 hox model。多年的白嫖经验又告诉我此时去 hox 仓库中搜索 context 肯定有小可爱已经提了 issue

ok!问题定位到了!解决它吧!

解决方案

在官方方案(hox 2.x)之前,快速的解决方案应该就以下两种:

  1. 编译期方案:通过 WebPack.NormalModuleReplacementPlugin 插件替换 useRequest 的某些模块实现本地的快速 fix。
  2. 运行时方案:使用一些黑科技修改 useRequest 的某些模块或者业务层统一使用再包装过一层的 useRequest。

我选择了使用黑科技。原因是编译期方案过重,包装一层又会导致其他小伙伴在使用的时候得从一个新的路径导入模块,不够顺手。我这边也希望尽可能地不去干涉项目原本的开发方式。

那么问题就成了如何在运行时去修改 useRequest 的某些模块?

首先想尝试的是直接修改 ahooks 的导出:

1
2
3
4
5
import * as ahooks from 'ahooks'

// ahooks.useRequest = myUseRequest

// Object.definObject.defineProperty(ahooks, 'useRequest', { get: myUseRequest });

它们会分别报错:

  1. Cannot set property useRequest of #which has only a getter
  2. Cannot redefine property: useRequest

传统黑科技被 webpack 的导出模块安排地死死地。。(实现地真严谨啊,不过这里记得是 strict 模式下 es 模块才会这么严格,不过也懒得再去找这条路径的突破口了

再想!useRequest 在不配置 requestMethod 的情况下,默认使用的是 Fetch。那可不可以。。。

1
window.fetch = () => {}

理论上可以,但实现起来难免得去翻 useRequest 的源码,兼容它调用 fetch 的行为基础上再去支持 umi-request,麻烦且 ugly。

再想!useRequest 既然是读取的 Context 注入的全局配置,这个过程里还能不能找点口子了?首先一定还有一个 Context 模块,即使我这边不 Provide,肯定他那边也是要 Consume 的。而一个没有被 Provide 的 Context,Consume 的时候拿到的一定是 Context 的默认值,而这个默认值一定藏在某个地方,如果我能找到它,我就可以嘿嘿嘿。。。于是:

1
2
3
import UseRequestContext from '@ahooksjs/use-request/es/configContext'

console.log(UseRequestContext)

逮到!凭我多年的改 bug 经验,这两个值一定就是 Context 注入值的缓存,且 _currentValue2 代表当前值,_currentValue 是之前值,用于浅比较更新。

那么接下来就好说了:

1
2
3
4
5
6
7
8
9
10
11
12
import umiRequest from 'umi-request'
import UseRequestContext from '@ahooksjs/use-request/es/configContext'

const requestMethod = (options) => {
if (typeof options === 'string') return umiRequest(options)
const { url, ...umiRequestOptions } = options
return umiRequest(url, umiRequestOptions)
}

UseRequestContext._currentValue2 = {
requestMethod,
}

刷新浏览器,成。。。功了一半?!

发现第一次请求是成功地走了 requestMethod,后面又开始走默认 fetch 了!猜测可能和 Context 的更新机制有关,手动改了一把 configContext 的模块,令其有默认值后再输出:

那就抄呗!把 _currentValue_currentValue2 都赋值一遍后,搞定!


 评论