Search K
Appearance
👍欢迎大家积极投稿交流👍
如果文档内容陈旧或者链接失效,请发现后及时同步,我将尽快修改
👇微信👇

Appearance
unibest 官方模板库中提供了三种请求库
简单版本http,路径(src/http/http.ts),对应的示例在 src/api/foo.tsalova 的 http,路径(src/http/request/alova.ts),对应的示例在 src/api/foo-alova.tsvue-query, 路径(src/utils/request.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹鉴于功能的实用性,我选择了 alova 作为默认的请求库,其他两个都已删除,如果有需要,安装官方模板,然后将需要的文件复制到项目中进行修改。alova唯一的弊端是包相对比较大,如果对包大小有要求,建议自行改用 简单版本http。
alova 可以用在服务端和客户端,类似于 axios,当然在客户端中可以使用在 PC、H5、小程序等平台,并且不会因框架而受到限制。简单版本http。针对弊端中的2和3,在封装请求的时候还沿用之前的请求封装策略,在请求之前和之后分别做对应的逻辑处理,不使用官方提供的Token认证拦截器及身份认证配置,源码在下方
文件位置http/tools/enum.ts
// **请根据业务修改**
// 后端响应字段
export const RES_CODE = 'code' // 对应业务逻辑自定的状态码
export const RES_MSG = 'msg' // 对应业务逻辑自定的提示
export const RES_DATA = 'data' // 对应业务逻辑自定的返回数据
export const TOKEN_KEY = 'userToken' // 缓存用户token到本地的键名
export const AUTHORIZATION: 'Authorization' | string = 'token' // token命名,请求header中的键名,值为 Authorization 时,会自动补全 Bearer
export const TIMEOUT = 30 * 1000 // 请求超时时间
export const RETRY_COUNT = 1 // 请求失败、登录失效等,尝试重新请求次数限制
export enum ResultEnum {
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
Success0 = 1, // 成功
Success200 = 200, // 成功
Error = 0, // 错误
Unauthorized = 301, // 未授权
AuthFailure = 302, // 授权失效
Forbidden = 403, // 禁止访问(原为forbidden)
NotFound = 404, // 未找到(原为notFound)
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
RequestTimeout = 408, // 请求超时(原为requestTimeout)
InternalServerError = 500, // 服务器错误(原为internalServerError)
NotImplemented = 501, // 未实现(原为notImplemented)
BadGateway = 502, // 网关错误(原为badGateway)
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
}
ResultEnum为接口返回状态码的枚举,一般常用Success0、Error、Unauthorized、AuthFailure,根据实际状态码修改,其他作为备用,目前只有Unauthorized、AuthFailure两个状态码,在静默授权的模式下会自动调用重新登录接口,其他都是显示错误提示,如果使用登录页面的方式,请前往文件http/alova.ts中搜索Unauthorized,将清除登录、重新登录、重新发起请求的逻辑修改为跳转登录页面,记得在跳转登录页面下边添加return逻辑,否则后续的失败提示依旧会执行。
文件位置http/alova.ts,目前已封装常规的拦截器,功能包含:
实时日志功能meta中配置domain,非业务主域名,不处理状态码,直接返回,需要在实际使用中自行处理请求额外可用参数说明:
// meta 可用字段信息
const meta = {
useAuth: true, // 是否需要登录认证,默认为 true
alert: true, // 提示错误信息
report: true, // 上报错误日志
domain: '', // 动态域名
retryCount: 0, // 当前已重试的次数
}⚠️双token功能移除
主要用于处理一些特殊的业务场景,如:需要同时获取业务token和刷新token,但是刷新token只有在业务token失效时才会返回,所以需要使用双token模式来获取最新的业务token。
根据目前后端的业务逻辑,统一使用单token,如果失效,重新执行静默登录,无需使用双token模式来获取最新的业务token,所以该模式移除,如果有需要,自行查阅官方模板进行添加。
核心拦截的逻辑,请查看文件http/alova.ts,下方展示的源码可能以后会出现一定的差异,可以简单了解下里边的业务逻辑。
import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import VueHook from 'alova/vue'
import { API_HOST } from '@/config/constant'
// import { LOGIN_PAGE } from '@/router/config'
import { useAppStore, useUserStore } from '@/store'
import { useLogger } from '@/utils/logger'
import Queue from '@/utils/queue'
import tips from '@/utils/tips'
import {
AUTHORIZATION,
ContentTypeEnum,
RES_CODE,
RES_DATA,
RES_MSG,
ResultEnum,
RETRY_COUNT,
ShowMessage,
TIMEOUT,
} from './tools/enum'
const queue = new Queue()
const logger = useLogger()
// meta 可用字段信息
// const meta = {
// useAuth: true, // 忽略登录拦截器,默认为 true
// alert: true, // 提示错误信息
// report: true, // 上报错误日志
// domain: '', // 动态域名
// authRole: 'refreshToken', // 刷新token标识
// retryCount: 0, // 当前已重试的次数
// }
/**
* alova 请求实例
*/
const alovaInstance = createAlova({
baseURL: API_HOST,
...AdapterUniapp(),
timeout: TIMEOUT,
statesHook: VueHook,
beforeRequest: async (method) => {
const appStore = useAppStore()
// 设置默认 Content-Type
method.config.headers = {
ContentType: ContentTypeEnum.JSON,
Accept: 'application/json, text/plain, */*',
Appid: appStore.accountInfo?.miniProgram?.appId ?? '',
...method.config.headers,
}
const { config } = method
// 处理动态域名
if (config.meta?.domain) {
method.baseURL = config.meta.domain
logger.log('当前域名', method)
return
}
// 是否需要认证
const useAuth = config.meta?.useAuth ?? true
const userStore = useUserStore()
// 设置token
let token = userStore.userToken
if (useAuth) {
try {
token = await queue.push(userStore.getUserToken)
// await sleep(3000)
} catch (error) {
logger.addFilterMsg('GetTokenFail')
logger.error('获取token失败 error', config, error)
tips.confirm('获取token失败,请稍后再试')
throw new Error('[接口错误]:获取token失败')
}
}
if (token) {
method.config.headers[AUTHORIZATION] =
(AUTHORIZATION === 'Authorization' ? 'Bearer ' : '') + token
} else if (useAuth) {
logger.addFilterMsg('GetTokenFail')
logger.error('token 不能为空,已取消请求', config)
throw new Error('[请求错误]:未登录')
}
},
responded: {
onSuccess: async (response, method) => {
const { requestType, meta } = method.config
const {
statusCode,
data: rawData,
errMsg,
} = response as UniNamespace.RequestSuccessCallbackResult
// 处理特殊请求类型(上传/下载)
if (requestType === 'upload' || requestType === 'download') {
return response
}
// 处理 HTTP 状态码错误
if (statusCode !== 200) {
logger.addFilterMsg('HttpRequestError')
logger.error('Http Request Response Server error', response)
const errorMessage =
ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
console.error('errorMessage===>', errorMessage)
if (meta?.alert !== false) {
tips.confirm({
content: errorMessage,
})
}
throw new Error(`${errorMessage}:${errMsg}`)
}
const report = meta?.report !== false
let __response: any
try {
// 内容 超出 5KB 会出现警告
const _response = JSON.stringify(rawData)
const toLog = _response.length > 1024 * 4
__response = JSON.parse(_response)
if (toLog) {
__response.data = _response.slice(0, 1024 * 3)
}
} catch (err) {}
// 外部域名,不走内部判断,直接返回请求结果
if (meta?.domain) {
report && logger.info('Http Request Response Success', __response)
return rawData
}
// 处理业务逻辑
const code = rawData[RES_CODE]
const msg = rawData[RES_MSG]
const data = rawData[RES_DATA]
// 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
if ([ResultEnum.Success0, ResultEnum.Success200].includes(code)) {
// 处理成功响应,返回业务数据
report && logger.info('Http Request Response Success', rawData)
return { code, msg, data, res: rawData }
}
// ****** 接口请求错误逻辑 ******
const retryCount = meta?.retryCount || 0
// 未授权或者授权失效,自动重新静默登录,如果需要跳转登录页面,自行修改逻辑
// 还有重试次数时允许重新登录,避免无效的重复登录
if (
[ResultEnum.Unauthorized, ResultEnum.AuthFailure].includes(code) &&
retryCount < RETRY_COUNT
) {
// 登录失效,执行静默登录,并尝试重新请求
const userStore = useUserStore()
try {
// 退出登录,通过队列获取最新的token
await userStore
.logout()
.then(() => queue.push(userStore.getUserToken))
} catch (error) {
// 登录接口异常,抛出失败
return Promise.reject(error)
}
// 如果不是使用静默登录,则跳转登录页面
// uni.reLaunch({ url: LOGIN_PAGE })
}
// 重新获取数据,控制 变量 retry 防止死循环
if (retryCount < RETRY_COUNT) {
if (!method.config.meta) {
method.config.meta = {}
}
method.config.meta.retryCount = retryCount + 1
return alovaInstance.Request({
url: method.url,
method: method.type,
...method.config,
})
}
// 其他错误码,提示用户
if (meta?.alert !== false) {
const errorMessage = msg || ShowMessage(code) || `HTTP请求错误[${code}]`
tips.confirm({
content: errorMessage,
})
}
// 上报错误日志
logger.addFilterMsg('HttpRequestError')
logger.error('Http Request Response error', response)
throw new Error(`请求错误[${code}]:${msg}`)
},
// 请求失败的拦截器
// 请求错误时将会进入该拦截器。
// 第二个参数为当前请求的method实例,你可以用它同步请求前后的配置信息
onError: (response, method) => {
const { statusCode, errMsg } = response
const { meta } = method.config
if (!statusCode) {
throw new Error('请求取消')
}
// 上报错误日志
logger.addFilterMsg('HttpRequestError')
logger.error('Http Request Server error', response)
const errorMessage =
ShowMessage(statusCode) || `HTTP响应异常[${statusCode}]`
if (meta?.alert !== false) {
tips.confirm({
content: errorMessage,
})
}
throw new Error(`${errorMessage}:${errMsg}`)
},
// 请求完成的拦截器
// 当你需要在请求不论是成功、失败、还是命中缓存都需要执行的逻辑时,可以在创建alova实例时指定全局的`onComplete`拦截器,例如关闭请求 loading 状态。
// 接收当前请求的method实例
onComplete: async (method) => {
// 处理请求完成逻辑
// console.log('服务器请求结束', method)
},
},
})
export const http = alovaInstance请求失败分两种情况:
token 失效封装的逻辑有对应的处理方案,统一在接口请求异常时处理,在判断请求失效的逻辑中添加 retryCount < RETRY_COUNT 是为了避免因过度使用静默登录导致频率超限所导致无效的请求
如果有登录页,可以注释重新登录逻辑,开启跳转登录页面逻辑,开启该方案,重试逻辑会失效
重新授权逻辑处理完毕后,处理重试逻辑判断,避免出现死循环
// ****** 接口请求错误逻辑 ******
const retryCount = meta?.retryCount || 0
// 未授权或者授权失效,自动重新静默登录,如果需要跳转登录页面,自行修改逻辑
// 还有重试次数时允许重新登录,避免无效的重复登录
if (
[ResultEnum.Unauthorized, ResultEnum.AuthFailure].includes(code) &&
retryCount < RETRY_COUNT
) {
// 登录失效,执行静默登录,并尝试重新请求
const userStore = useUserStore()
try {
// 退出登录,通过队列获取最新的token
await userStore.logout().then(() => queue.push(userStore.getUserToken))
} catch (error) {
// 登录接口异常,抛出失败
return Promise.reject(error)
}
// 如果不是使用静默登录,则跳转登录页面
// uni.reLaunch({ url: LOGIN_PAGE })
// return
}
// 重新获取数据,控制 变量 retry 防止死循环
if (retryCount < RETRY_COUNT) {
if (!method.config.meta) {
method.config.meta = {}
}
method.config.meta.retryCount = retryCount + 1
return alovaInstance.Request({
url: method.url,
method: method.type,
...method.config,
})
}get和post传参有区别,详情可查看method详解
import { http } from '@/http/alova'
// post传参 和 无需token的接口
export function fetchLogin(data: { code: string }) {
return http.Post<IResData<IUserLoginRes>>('/user/login', data, {
meta: {
useAuth: false, // 忽略 token 验证拦截器
},
})
}
// get极简写法
export function fetchHomeData() {
return http.Get<IResData<IHomeData>>('/home/data')
}
// post传参极简写法
export function fetchHomeData() {
return http.Post<IResData<IHomeData>>('/home/data')
}
// get 传参 和 额外参数
export function fetchHomeData(params) {
return http.Get<IResData<IHomeData>>('/home/data', {
params,
meta: {
alert: false, // 特殊接口不需要或者页面使用已自行处理弹框逻辑,alert 要设置 false
timeout: 10 * 1000, // 单独设置超时时间,未设置使用全局配置,默认30s
},
})
}在 alova 中,默认是不开启缓存的,可以针对某个接口开启缓存,例如:
import { http } from '@/http/alova'
// 内存模式(默认),刷新即失效
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
// 单位为毫秒,当设置为`Infinity`,表示数据永不过期,设置为0或负数时表示不缓存
cacheFor: 10 * 60 * 1000, // 缓存 10 分钟
})
}
// 手动设置模式为 内存模式,与上边表现一致
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
mode: 'memory', // 设置缓存模式为内存模式
cacheFor: 10 * 60 * 1000, // 缓存 10 分钟
})
}
// 手动设置模式为 持久化模式
// 持久化模式默认使用 localStorage
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
mode: 'restore', // 设置缓存模式为持久化模式
cacheFor: 10 * 60 * 1000, // 缓存 10 分钟
})
}
// 如果使用持久化模式,在缓存有效期内数据发生变动,需要改变时,可以设置 缓存标签
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
mode: 'restore', // 设置缓存模式为持久化模式
cacheFor: 10 * 60 * 1000, // 缓存 10 分钟
// 新增或修改tag参数,已缓存的数据将失效
// 建议使用版本号的形式管理,例如:v1、v2、v3...
tag: 'v1',
})
}
// 过期时间类型:绝对时间
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
mode: 'restore', // 设置缓存模式为持久化模式
cacheFor: new Date('2030-01-01');
})
}
// 当天有效,该场景使用较多
export function fetchAreaList() {
return http.Get<IResData<IAreaList>>('/area_list', {
mode: 'restore', // 设置缓存模式为持久化模式
cacheFor: new Date().setHours(24, 0, 0, 0),
})
}全局设置缓存模式,在创建实例化的时候设置,因弊端太多,不建议使用
const alovaInstance = createAlova({
// ...
cacheFor: {
// 统一设置POST的缓存模式
POST: {
mode: 'restore',
expire: 60 * 10 * 1000,
},
// 统一设置HEAD请求的缓存模式
HEAD: 60 * 10 * 1000,
},
})SessionStorage 存储适配器示例
const sessionStorageAdapter = {
set(key, value) {
sessionStorage.setItem(key, JSON.stringify(value))
},
get(key) {
const data = sessionStorage.getItem(key)
return data ? JSON.parse(data) : data
},
remove(key) {
sessionStorage.removeItem(key)
},
clear() {
sessionStorage.clear()
},
}
const alovaInstance = createAlova({
// ...
l1Cache: sessionStorageAdapter, // l1缓存
l2Cache: sessionStorageAdapter, // l2缓存
})注意
因缓存适配器只能是 alova 实例级别的全局配置,并不能单独给某一个接口设置不同的缓存适配器。
import { fetchHomeData } from '@/api/home'
const getHomeData = async () => {
fetchHomeData.then({data, code, msg, res} => {
/**
* 返回字段说明
* @param data 业务数据
* @param code 状态码
* @param msg 状态描述
* @param res 原始数据,包含状态码、状态描述、业务数据,如果部分接口有其他返回字段,需要在 res 中获取
* data 即可满足大部分需求
*/
console.log('首页数据', code, msg, data, res)
})
}import { useRequest } from 'alova/client'
import { fetchHomeData } from '@/api/home'
// 基础使用
const { { data, code, msg, res } = data } = useRequest(fetchHomeData);
console.log('首页数据', data)
// 含状态使用
const { { data, code, msg, res } = data, loading, error } = useRequest(fetchHomeData, {
// 请求响应前,data的初始值,可以是任意类型,根据实际情况设置
initialData: []
});
console.log('首页数据', data, loading, error)
// 手动请求
const { data, send } = useRequest(fetchHomeData, {
immediate: false
});
// 手动触发请求
send()
// 手动请求传参
const { data, send } = useRequest((params) => fetchHomeData(params), {
immediate: false
});
// 手动触发请求
send({ id: 1 })
// 中断请求
const {
// ...
// abort函数用于中断请求
abort
} = useRequest(fetchHomeData);
abort();列表接口数据获取,含分页、下拉刷新,上拉加载更多、分类切换触发更新,模板源码位置在subpackages/example/request/list.vue
<script setup lang="ts">
import { usePagination } from 'alova/client'
import { fetchGoodsList } from '@/api/home'
import tips from '@/utils/tips'
defineOptions({
name: 'RequestPage',
})
definePage({
style: {
navigationBarTitleText: '分页请求示例',
enablePullDownRefresh: true,
},
})
// 分类ID
const classifyId = ref(0) // 修改分类ID,watchingStates 监听到变化会自动重新获取数据,页码也会重置,无需额外处理
const classifyTabs = ref([
{
id: 0,
title: '有数据',
},
{
id: 1,
title: '无数据',
},
])
// 获取列表数据
const {
loading,
page,
error,
isLastPage,
data: goodsList,
send,
reload,
refresh,
} = usePagination(
// Method实例获取函数,它将接收page和pageSize,并返回一个Method实例
(page, pageSize) => {
return fetchGoodsList({
page,
page_size: pageSize,
total: 56,
classify_id: classifyId.value,
})
},
{
total: ({ data }) => data.total, // 返回总条数数据
data: ({ data }) => data.list,
append: true, // 追加数据模式
immediate: false, // 默认不获取数据
initialData: {
total: 0,
data: [],
},
initialPage: 1, // 初始页码
initialPageSize: 20, // 初始每页数量
watchingStates: [classifyId],
async middleware(_, next) {
// 当是第一页时,开启loading提示
if (page.value === 1) {
tips.loading('加载中...')
}
// 请求之前
await next()
// 请求之后
tips.hideLoading()
},
},
)
// 加载更多状态
const loadState = computed(() => {
if (loading.value) {
return 'loading'
}
if (isLastPage.value) {
return 'finished'
}
if (error.value) {
return 'error'
}
return 'loading'
})
onMounted(() => {
// 手动获取数据
send()
})
// 加载更多
onReachBottom(() => {
// 请求中或者没有数据,终止请求
if (loading.value || isLastPage.value) {
return
}
// 翻到下一页,page 值更改后将自动发送请求
page.value++
})
// 下拉刷新
onPullDownRefresh(() => {
// 请求中终止下拉刷新
if (loading.value) {
return
}
// 从第一页开始重新加载列表,并清空缓存
reload().then(() => {
uni.stopPullDownRefresh()
})
})
</script>
<template>
<wd-sticky>
<wd-tabs v-model="classifyId" custom-class="w-100vw!">
<block v-for="item in classifyTabs" :key="item.id">
<wd-tab :title="item.title" />
</block>
</wd-tabs>
</wd-sticky>
<!-- 商品列表 -->
<template v-if="goodsList.length">
<view class="goods-list">
<view
v-for="(item, index) in goodsList"
:key="index"
class="goods-item"
hover-class="view-hover"
:hover-stay-time="150"
>
<view
class="flex items-center justify-center border-1 border-gray-200 border-solid px-4 py-4"
>
<view class="h-14 w-16">
<image :src="item.image" alt="" class="h-full w-full" />
</view>
<view class="flex flex-1 flex-col justify-between text-4">
<view class="line-clamp-2 text-ellipsis">
{{ item.title }}
</view>
<view class="text-gray-500">
{{ item.create_time }}
</view>
</view>
</view>
</view>
</view>
</template>
<!-- 数据为空 -->
<view
v-if="goodsList.length === 0 && loadState === 'finished'"
class="pb-20 pt-20"
>
<wd-status-tip
image="https://wot-ui.cn/assets/content.png"
tip="空空如也~"
/>
</view>
<!-- 加载更多 -->
<wd-loadmore
v-else
custom-class="loadmore pb-4"
:state="loadState"
:loading-props="{ size: '30rpx' }"
@reload="refresh"
/>
</template>你可以为所有的 useHook 设置请求中间件来自由控制请求行为,提供了强大的、几乎能控制一个请求的所有行为的能力,无论简单还是复杂的请求策略,可能你都会用上它,接下来我们看下它到底有什么神通。
请求中间件是一个异步函数,以下是一个简单的请求中间件,它在请求前和请求后分别打印了一些信息,没有改变任何请求行为。
useRequest(todoList, {
async middleware(_, next) {
console.log('before request')
await next()
console.log('after requeste')
},
})当你不希望发出请求时,可不调用 next来忽略本次请求,就好像从来没有发起过请求一样。例如在useWatcher中某个监听字段变化时不发出请求。
useWatcher(() => todoList(), [state1], {
middleware: async (_, next) => {
if (state1 === 'a') {
return next()
}
},
})中间件函数的返回值将作为本次请求的响应数据参与后续的处理,如果中间件没有返回任何数据但调用了 next,则会将本次请求的响应数据参与后续处理。
// 转换响应数据并返回
useRequest(todoList, {
async middleware(_, next) {
const result = await next()
result.code = 500
return result
},
})
// 将会以字符串abc作为响应数据
useRequest(todoList, {
async middleware(_, next) {
await next()
return 'abc'
},
})
// 通过中间件修改请求参数或者添加参数
useRequest(todoList, {
// 通过中间件修改请求参数
// get 和 post 获取参数的位置不同
async middleware(context, next) {
// 获取其他接口数据
const { data } = await todoConfig()
// POST 请求
const params = (context.method.data ?? {}) as Record<string, any>
// GET 请求
// const params = (context.method.config.params ?? {}) as Record<string, any>
context.method.data = {
...params,
id: data.id,
}
await next()
},
})其他更多更强大的用法请参考alova.js 中间件。