夜雨聆风学习资料网

ARTICLE · 1346755

三端一致的 uni-app x 请求库:axios 写法,多实例、拦截器、可中断、上传下载

三端一致的 uni-app x 请求库:axios 写法,多实例、拦截器、可中断、上传下载

最近我发布了 uView Pro X 组件库,这是支持 uni-app x 蒸汽模式的新版组件库。发布后一直在迭代,现在基本稳定,目前最新版本 v1.0.26。

uView Pro X 1.0.0 发布:80+ 精选组件,全面支持 uni-app x 蒸汽模式

上一代 uView Pro(只支持 uni-app)里的 http 封装用起来很顺手,搬到 uni-app x 项目里是不能用的:UTS 不是 TypeScript,axios 那套写法在这里行不通。

于是我按 axios 的骨架重写了一份,放进 uView Pro X,导出名还是 http。装了这个库就不用再装别的包;要对接多个后端,各自 create 一个实例就行。拦截器、超时、状态码判定、取消、上传下载进度、useRequest 三态都在里面,失败统一 reject 一个 HttpError。

下面先看一下用法。

先来看看基本使用

先建全局实例,实例放在一个文件里,主要声明一些 base 信息,全项目共用:

// api/request.utsimport { http } from'@/uni_modules/uview-pro-x';importtype { HttpInstance } from'@/uni_modules/uview-pro-x';const headers = new UTSJSONObject();headers['content-type'] = 'application/json';exportconst request: HttpInstance = http.create({    baseURL: 'https://api.example.com',    timeout: 15000,    headers});

到这一步就能发请求了。

假如一个页面要加载用户信息,写全了大概这样:

<template>    <view class="page">        <text v-if="loading">加载中…</text>        <text v-else-if="error != null">{{ errorText }}</text>        <text v-else>{{ userName }}</text>        <ux-button text="刷新" type="primary" @click="run()" />    </view></template><script setup lang="uts">import { useRequest } from '@/uni_modules/uview-pro-x';import type { HttpCallConfig } from '@/uni_modules/uview-pro-x';import { request } from '@/api/request';type User = { id: string; name: string };const { loading, data, error, run } = useRequest<User>(async (ctx) => {    const res = await request.get('/user/1', null, { signal: ctx.signal } as HttpCallConfig);    return res as User;}, { immediate: true });const userName = computed((): string => {    const user = data.value;    return user == null ? '' : user.name;});const errorText = computed((): string => {    const err = error.value;    return err == null ? '' : err.message;});</script>

模板里直接写 loading、error,不用 .value,Vue 会解包。要写对应文案就放进 computed,省掉模板里的非空断言,UTS 也不支持 !。

是不是很简洁?其实还有很多用法,比如多域名配置,请求、响应拦截器配置,中断请求等。

设计上的几个选择

实例是派生出来的

http 本身就能用,http.create(config) 从它派生出新的。派生时深合并父级默认值,所以只写和父级不一样的那几项。

如果是多后端的项目按后端拆实例就行,业务一个、文件一个,各自带自己的 baseURL 和超时。请求前不必判断域名,拦截器里也不必按 url 前缀分流。

要注意的是派生实例只继承默认值,不继承拦截器。这不是遗漏,axios 也是这么设计的:拦截器常常依赖实例自己的上下文,比如某个服务要加专属签名。

实际写法是把 setupInterceptors(instance) 抽成函数,每个实例调一次,就不会漏。

// api/interceptors.utsimporttype { HttpError, HttpResponse, HttpInstance, ResolvedHttpConfig } from'@/uni_modules/uview-pro-x';exportfunctionsetupInterceptors(instance: HttpInstance): void{    instance.interceptors.request.use((config: ResolvedHttpConfig): ResolvedHttpConfig | null => {const token = uni.getStorageSync('token') asstring;if (token != '') {            config.headers['authorization'] = 'Bearer ' + token;        }return config;    });    instance.interceptors.response.use((response: HttpResponse): any => {return response.data;    }, (error: HttpError): HttpError => {return error;    });}

响应拦截器里的 return response.data 是整个库唯一解包的地方,页面拿到的就是业务数据,不用每次都 res.data.data。

拦截器用数组,不用单槽

单槽的写法问题很集中:后加的会把先注册的顶掉,谁都不报错,先加的那个就不生效,有时排查起来很费时间。

这里的 use 是往数组里追加,按注册顺序执行。请求链里任何一个返回 null,这次请求就中断,以 ERR_CANCELED reject。

响应链有个不太一样的约定:每个回调收到的是同一个 HttpResponse 对象,最后一个非 null 的返回值才是最终结果。

不做值链式传递,是刻意的,UTS 的类型断言在链式传递里很容易崩,尤其是加密编译之后。想看响应的原始结构,随便哪个回调里都能拿到 status 和 headers。

request.interceptors.response.use((response: HttpResponse): any => {if (response.status == 204) {returnnull;    }return response.data;});

取消请求需要自己兜底

uni.request 返回的是 RequestTask,没有 Promise,也没有 signal。现在的做法是:每次请求创建一个控制器,abort() 时先自己 reject 一个 ERR_CANCELED,再调 task.abort()。

uni 在 abort 之后不保证触发 fail 回调,只调 task.abort() 的话,Promise 会一直挂着,loading 永远转圈。加上“先 settle 再 abort”,最坏情况也不会漏。

const controller = http.abortController();request.get('/slow', null, { signal: controller.signal } as HttpCallConfig)    .catch((e: any) => {if (http.isCancel(e)) return;  // 取消不是错误,别弹 toast// 真实失败    });controller.abort();

isCancel 的参数类型是 HttpError | null,内部只读 code,不做类型判断。这一点后面会说到原因。

错误只有四种 code

失败一律 reject 一个 HttpError,目前库内声明的 code 只有四种取值:

  • ERR_BAD_RESPONSE:有 HTTP 响应,但状态码没通过 validateStatus(默认 2xx)。status 是真实状态码,原始响应在 response 里。
  • ERR_NETWORK:没拿到响应,断网、超时、域名解析失败都归这里,原始错误码在 errCode。
  • ERR_CANCELED:主动取消,或者请求拦截器返回了 null。
  • ERR_INTERCEPTOR:拦截器自己抛的错误。

页面里判断用 code,建议不要匹配 message,因为文案会改,message 也会跟着变,而 code 不会变。

catch (e: any) {const err = e as HttpError;if (err.code == 'ERR_NETWORK') {// 提示检查网络,给个重试按钮    } elseif (err.code == 'ERR_BAD_RESPONSE') {// err.status 是 404 / 500 之类的真实状态码    } elseif (err.code == 'ERR_CANCELED') {// 静默    }}

useRequest 只做四件事

这个 Hook 不管发请求,发请求还是走实例,所以拦截器和默认值照样生效。而页面里必须处理、又容易写漏的有如下几点:

  1. 三态:loading / data / error
  2. 竞态丢弃:给每次 run 编号,回来的结果对不上最新编号就丢掉
  3. 卸载取消:onUnmounted 里 abort
  4. 重跑先取消:再次 run() 时先把上一次撤掉

搜索框边输入边请求、列表筛选快速切换,都会碰到“旧请求比新请求晚回来”的问题。这时靠 loading 判断是挡不住的,需要靠序号来判断。

const { loading, data, error, run, cancel } = useRequest<Order[]>(async (ctx) => {const list = await request.get('/orders', params, { signal: ctx.signal } as HttpCallConfig);return list as Order[];});// 筛选条件变了,重跑一次,旧的自动作废constonFilterChange = (): void => {    run();};

immediate 默认 false,如果一进页面就发请求就要显式传 { immediate: true },或者在 onMounted 里 run()。默认不跑,是为了让“什么时候发请求”由页面决定,而不是被 Hook 偷偷决定。

接入需要三步

  1. 建实例。如果是多域名,可以按照域名构建多实例。
  2. 挂拦截器。把 setupInterceptors 抽成函数,在实例文件末尾调一次。
  3. 页面里用 useRequest。模板写三态,需要文案就放 computed,别在模板里直接写 data.value.name。

不想用 Hook 也可以,直接 await request.get(...),三态就需要自己管,不强制配套使用。

上传和下载

upload 和 download 是实例上的方法,走同一套默认值、同一个请求拦截器、同一个 validateStatus,失败也还是 HttpError,错误提示可以只写一份。

const result = await http.upload({    url: '/upload',    filePath: tempPath,    name: 'file',    onProgress: (p) => {        progress.value = p.progress;  // 0-100    }});

几个细节:上传会自动剔除 content-type,让平台自己生成带 boundary 的 multipart 头,手动设置了反而会坏。上传返回的 data 是字符串,业务要 JSON 就自己 JSON.parse 一次。下载不传 filePath 时落到平台临时目录,返回 tempFilePath。

我踩过的一些坑

  • e is HttpError 不要写。App 端这么写没事,H5 转译后 is 变成未定义标识符,只有走进那个分支才报 ReferenceError: is is not defined。改成 const err = e as HttpError 再读字段,问题就没了。这个坑最难查。
  • as Promise<T> 也不要写。编译期不报错,运行时行为不一定对。API 层用 await 拿结果再做字段映射,比断言稳。
  • GET 和 HEAD 的 data 会被忽略,传了还会告警。查询参数放 params,从 axios 迁过来的代码最容易在这里漏。
  • validateStatus 只接受函数。要放行所有状态码,传 (): boolean => true;传 null 不行,UTS 里可选字段的 null 和“没传”区分不开。
  • abort 过的 signal 不能复用。同一个控制器第二次 abort(),请求立刻 reject,每次新请求都要新建控制器。

有几件事它不做

  • 没有拦截器 eject。要去除某个拦截器,要在它内部加判断。
  • 不支持异步拦截器。需要等异步结果,去业务层做。
  • 没有自动重试。重试策略和业务强相关,放在 API 层自己写更合适。
  • 不接管状态管理。store 只存业务数据,loading、error 交给 useRequest。

最后

使用 uView Pro X,你在项目里发一个请求,代码大概需三行:await request.get(...),外面套一层 useRequest,模板写三态。App、H5、小程序跑的是同一份代码,没有条件编译。

token 从哪取、404 怎么提示、页面退出时请求停不停、旧响应回来会不会盖掉新的,这些现在都由库处理,开发者专注业务就行。

可去官方文档查看更加详细的使用方法(支持一键复制代码):x.uviewpro.cn/zh/request/http.html

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料