ARTICLE · 1346755
三端一致的 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 不管发请求,发请求还是走实例,所以拦截器和默认值照样生效。而页面里必须处理、又容易写漏的有如下几点:
三态: loading/data/error竞态丢弃:给每次 run编号,回来的结果对不上最新编号就丢掉卸载取消: onUnmounted里 abort重跑先取消:再次 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 偷偷决定。
接入需要三步
建实例。如果是多域名,可以按照域名构建多实例。 挂拦截器。把 setupInterceptors抽成函数,在实例文件末尾调一次。页面里用 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















































































