Skip to content

useRequest

Junerver Hou edited this page Sep 14, 2024 · 5 revisions

快速上手

useRequest 是一个功能丰富的异步数据管理的 Hooks,Compose 项目中的网络请求场景使用 useRequest 就够了。

useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:

  • 自动请求/手动请求
  • 轮询
  • 防抖
  • 节流
  • 错误重试
  • loading delay
  • SWR(stale-while-revalidate)
  • 缓存

默认用法

suspend fun mockRequestArticle(): MockArticle {
    delay(2000L)
    return MockArticle(Clock.System.now().toEpochMilliseconds(), NanoId.generate(200))
}

val (data, loading, error) = useRequest(
    requestFn = {
        mockRequestArticle()
    }
)

你只需要将在 requestFn 参数赋值一个lambda闭包,在这个闭包中调用一个suspend修饰的异步函数即可

在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 loading , data , error 等状态。

基础用法

useRequest 的返回值是一个 七元组,它的类型如下:

Tuple7<TData?, Boolean, Throwable?, ReqFn, MutateFn<TData>, RefreshFn, CancelFn>
  1. data 异步数据
  2. loading 状态
  3. error 错误
  4. req 请求函数
  5. mutate 修改函数
  6. refresh 刷新函数
  7. cancel 取消请求函数

我们使用 kotlin 的解构语法可以轻松的获取他们。

req 请求函数是一个同步函数,它会使用当前组件的协程作用域,你只需要把他当作同步函数在组件中调用即可。

手动触发

useRequest 的第二个参数是 optionsOf,你可以通过这个参数完成各种请求状态管理的配置。

如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 req 来触发执行。

import xyz.junerver.compose.hooks.invoke

val (data, loading, error, req) = useRequest(
    requestFn = {
        mockRequestArticle()
    },
    optionsOf = {
        manual = true
    }
)

TButton(text = "manual request") { req() }

你可以使用顶层函数 optionsOf 来方便的创建选项,这个函数支持多个hooks,在本项目中所有需要设置配置的场合都可以是用这个函数

optionsOf 函数已经废弃,请使用 optionsOf 参数传递闭包进行配置,你只需要简单的全局替换 options = optionsOf {optionsOf = {

req函数的签名为:(Array<Any?>) -> Unit,你需要手动导入import xyz.junerver.compose.hooks.invoke,这样你就可以将它如同普通函数一样使用

默认参数

上面我们局的例子的异步请求是没有参数的,但实际场景大部分请求都需要传递参数,我们可以通过 options.defaultParams 来为异步请求设置默认参数:

useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
    }
)

在闭包中我们可以通过 it[index] 对参数进行类型转换,如果你在 Jvm/Android 平台使用,我提供了一个方便的转换函数 asSuspendNoopFn

例如上面的例子,如果你可以写成这样:

useRequest(
    requestFn = NetApi::userInfo.asSuspendNoopFn(),
    optionsOf = {
        defaultParams = arrayOf("junerver")
    }
)

生命周期

useRequest 提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。

  • onBefore:请求之前触发
  • onSuccess:请求成功触发
  • onError:请求失败触发
  • onFinally:请求完成触发
useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        onBefore = {
            state += "onBefore: ${it.joinToString("")}"
        }
        onSuccess = { data, _ ->
            println("Lifecycle Lifecycle: onSuccess")
            state += "\n\nonSuccess:\nData:$data"
        }
        onError = { err, pa ->
            state += "\n\nonError: ${pa.joinToString("")}\nError: ${err.message}"
        }
        onFinally = { _, _, _ ->
            state += "\n\nonFinally!"
        }
    }
)

刷新(重复上一次请求)

useRequest 提供了 refresh 方法,使我们可以使用上一次的参数,重新发起请求。

假如在读取用户信息的场景中

  1. 我们读取了 ID 为 junerver 的用户信息 req("junerver")
  2. 我们通过某种手段更新了用户信息
  3. 我们想重新发起上一次的请求,那我们就可以使用 refresh() 来代替 req("junerver"),这在复杂参数的场景中是非常有用的

mutate 立即变更数据

useRequest 提供了 mutate, 支持立即修改 useRequest 返回的 data 参数。

val (userInfo, loading, _, _, mutate) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
    }
)

TButton(text = "changeName") {
    // 你可以在发起修改的同时,使用 mutate 进行乐观更新
    mockFnChangeName(newName)
    if (userInfo.asBoolean()) {
        mutate {
            it!!.copy(name = input)
        }
    }
}

mutate 函数的签名是:fun mutate(mutateFn: (TData?) -> TData)

取消响应

useRequest 提供了 cancel 函数,用于忽略当前异步请求返回的数据和错误

注意:调用 cancel 函数并不会立即停止 协程job 的执行(这涉及协程的取消机制)

同时 useRequest 会在以下时机自动忽略响应:

  • 组件卸载时,正在进行的 promise
  • 竞态取消,当上一次 promise 还没返回时,又发起了下一次 promise,则会忽略上一次 promise 的响应

Loading Delay

通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。

这在一些快速反回结果的场景将会很有用,简单来说,只要接口响应时间小于你设置的 loadingDelayloading 状态将会保持 fasle

val (userInfo, loading, _, request) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        loadingDelay = 1.seconds
    }
)

轮询

通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。

val (userInfo, loading) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        pollingInterval = 3.seconds
        pollingWhenHidden = true
        pollingErrorRetryCount = 5
    }
)
  • pollingInterval 轮询间隔时间
  • pollingWhenHidden 后台仍然发起轮询
  • pollingErrorRetryCount 轮询最大错误重试次数

Ready

通过设置 options.ready,可以控制请求是否发出。当其值为 false 时,请求永远都不会发出。

其具体行为如下:

  1. manual=false 自动请求模式时,每次 readyfalse 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
  2. manual=true 手动请求模式时,只要 ready=false,则通过 req 触发的请求都不会执行。

它非常适合用在链式请求场景,例如:

// 请求1
val (userInfo, userLoading) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
    }
)
// 请求2
val (repoInfo, repoLoading) = useRequest(
    requestFn = { NetApi.repoInfo(it[0] as String, it[1] as String) },
    optionsOf = {
        defaultParams = arrayOf(
            userInfo?.login,
            "ComposeHooks"
        )
        ready = userInfo.asBoolean()
    }
)

请求2请求1 成功后更新 readydefaultParams,并自动进行发出请求。

依赖刷新

通过设置 options.refreshDeps,在依赖变化时, useRequest 会自动调用 refresh 方法,实现刷新(重复上一次请求)的效果。

val (state, setState) = useGetState(0)
val (userInfo, loading, error) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        refreshDeps = arrayOf(state)
    }
)

options.refreshDeps 是一个数组,需要通过 arrayOf 传递,在一些更新数据场景它将非常好用。

防抖 & 节流

通过设置 options.debounceWait,进入防抖模式,此时如果频繁触发 req,则会以防抖策略进行请求。

val (userInfo, loading, _, req) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        debounceOptions = DebounceOptions.optionOf { wait = 3.seconds }
    }
)

详情可见 useDebounce

节流模式同理:

val (userInfo, loading, _, request) = useRequest(
    requestFn = { NetApi.userInfo(it[0] as String) },
    optionsOf = {
        defaultParams = arrayOf("junerver")
        throttleOptions = ThrottleOptions.optionOf { wait = 3.seconds }
    }
)

同时设置了防抖、节流会怎样?

会节流,这两者只会有一个生效,同时配置则节流

缓存 & SWR

如果设置了 options.cacheKeyuseRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。

你可以通过 options.staleTime 设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。

你也可以通过 options.cacheTime 设置数据缓存时间,超过该时间,我们会清空该条缓存数据。

SWR

下面的示例,我们设置了 cacheKey,在组件第二次加载时,会优先返回缓存的内容,然后在背后重新发起请求。

@Composable
private fun TestSWR() {
    val (isVisible, toggle) = useBoolean(true)
    Column {
        TButton(text = "show/hide") {
            toggle()
        }
        if (isVisible) {
            SWR()
        }
        HorizontalDivider(modifier = Modifier.fillMaxWidth())
        if (isVisible) {
            SWR(true)
        }
    }
}

@Composable
private fun SWR(useCache: Boolean = false) {
    val (data, loading) = useRequest(
        requestFn = {
            mockRequestArticle()
        },
        optionsOf = {
            if (useCache) cacheKey = "test-swr-key"
        }
    )
    Column(modifier = Modifier.height(210.dp)) {
        Text(text = "cache: $useCache", color = Color.Red)
        Text(text = "Background loading: $loading")
        if (data.asBoolean()) {
            Text(text = "$data")
        }
    }
}

运行可以发现,组件使用cacheKey的组件在退回上一页或者切换显示隐藏时都能首先使用缓存数据显示、然后后台发起请求更新,可以优化体验。

数据保持新鲜

通过设置 staleTime,我们可以指定数据新鲜时间,在这个时间内,不会重新发起请求。

@Composable
private fun StaleTime(cacheKey: String) {
    val (data, loading) = useRequest(
        requestFn = {
            mockRequestArticle()
        },
        optionsOf = {
            this.cacheKey = cacheKey
            staleTime = 5.seconds
        }
    )
    Column(modifier = Modifier.height(210.dp)) {
        Text(text = "statleTime: 5s", color = Color.Red)
        Text(text = "Background loading: $loading")
        if (data.asBoolean()) {
            Text(text = "$data")
        }
    }
}

在设定的 staleTime 之内,即使切换隐藏\显示,都不会发出新的请求。直到时间超出,才会在后台发起请求,更新数据。

数据共享

同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性:

  • 请求共享:相同的 cacheKey 同时只会有一个在发起请求,后发起的会共用同一个异步请求的 Deferred
  • 数据同步:当某个 cacheKey 发起请求时,其它相同 cacheKey 的内容均会随之同步

删除缓存

ahooks 提供了一个 clearCache 方法,可以清除指定 cacheKey 的缓存数据。

import xyz.junerver.compose.hooks.userequest.utils.clearCache

@Composable
fun TestStaleTime() {
    val (isVisible, toggle) = useBoolean(true)
    val cacheKey = "test-stale-key"
    Column {
        Text("↓ The following two components use the same 'cacheKey' and they will share the data")
        Row {
            TButton(text = "show/hide") {
                toggle()
            }
            TButton(text = "clearCache") {
                // 通过调用top-level函数 `clearCache` 可以移除指定key的缓存,该函数可以接收多个key
                clearCache(cacheKey)
            }
        }
        if (isVisible) {
            // 相同 cacheKey 的数据全局同步
            StaleTime(cacheKey)
            StaleTime(cacheKey)
        }
    }
}

错误重试

通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。

val (mockInfo, stuLoading, err) = useRequest(
    requestFn = {
        mockRequest(it[0] as String, it[1] as String)
    },
    optionsOf = {
        defaultParams = arrayOf("1", "2")
        retryCount = 5
        retryInterval = 2.seconds
        onError = { _, _ ->
            count += "${Clock.System.now().epochSeconds}\n"
        }
    }
)
  • retryCount 错误重试次数。如果设置为 -1,则无限次重试
  • retryInterval 重试时间间隔。 如果不设置,默认采用简易的指数退避算法,取 (1.seconds * 2f.pow(count).toInt()).coerceAtMost(30.seconds)也就是第一次重试等待 2s,第二次重试等待 4s,以此类推,如果大于 30s,则取 30s