前端组知识库

👍欢迎大家积极投稿交流👍

如果文档内容陈旧或者链接失效,请发现后及时同步,我将尽快修改

👇微信👇

图片
Skip to content

请求配置

unibest 官方模板库中提供了三种请求库

  • 菲鸽简单封装的 简单版本http,路径(src/http/http.ts),对应的示例在 src/api/foo.ts
  • alova 的 http,路径(src/http/request/alova.ts),对应的示例在 src/api/foo-alova.ts
  • vue-query, 路径(src/utils/request.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹

鉴于功能的实用性,我选择了 alova 作为默认的请求库,其他两个都已删除,如果有需要,安装官方模板,然后将需要的文件复制到项目中进行修改。alova唯一的弊端是包相对比较大,如果对包大小有要求,建议自行改用 简单版本http

为何选用 alova

alova 官方文档

优势

  1. alova 可以用在服务端和客户端,类似于 axios,当然在客户端中可以使用在 PC、H5、小程序等平台,并且不会因框架而受到限制。
  2. 支持全局配置,比如:baseURL、超时时间、请求头、请求拦截、响应拦截等常规配置。
  3. 自带缓存策略,对于常年不变的接口比如:地址列表、分类列表,可以设置缓存,首次请求从接口获取,后续从缓存中取,如果缓存过期,会重新请求接口。
  4. 自带数据请求状态管理,比如:loading、error、success 等状态,无需手动管理。
  5. 分页请求策略,自动处理分页数据(是否使用追加模式)及页码参数、可以配置预加载相邻页数据,无需用户等待。
  6. 监听筛选条件变化,自动触发请求,对搜索类列表使用非常方便。
  7. 表单提交策略,可以很方便地实现表单草稿、多页面(多步骤)表单,除此以外还提供了表单重置等常用功能。
  8. ......优势还有很多,更多的用法查阅官方文档,上边说的这几项优势基本能涵盖大部分的业务场景。

弊端

  1. 包大小相对较大,特别是在小程序环境,对包的大小非常敏感,需要对主包有很严格的控制,如果担心或者有很大介意的话,请更换 简单版本http
  2. Token认证拦截器策略,按照官方文档说明可以实现如 登录、刷新token、退出、访客 相关的身份认证,经过多轮且全面的测试,有以下问题:
    1. 登录身份表示:如果因调用静默登录过于频繁,会导致在微信端出现登录超限的问题,接口如果不做好全面处理,容易出现死循环,且无法中断
    2. 授权重试:主要是因 1 的问题导致,会出现无限重试,无法中断
    3. 访客模式:如果同一接口,有token和无token返回内容不同时,就无法实现,访客模式下header中无法携带token参数
  3. 请求重试策略,需要额外使用 hook,且无法全局配置,需要在使用时通过对应 hook 来实现,虽然效果更好更智能,但使用繁琐

改进

针对弊端中的2和3,在封装请求的时候还沿用之前的请求封装策略,在请求之前和之后分别做对应的逻辑处理,不使用官方提供的Token认证拦截器身份认证配置,源码在下方

请求配置

文件位置http/tools/enum.ts

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 为接口返回状态码的枚举,一般常用 Success0ErrorUnauthorizedAuthFailure,根据实际状态码修改,其他作为备用,目前只有 UnauthorizedAuthFailure 两个状态码,在静默授权的模式下会自动调用重新登录接口,其他都是显示错误提示,如果使用登录页面的方式,请前往文件http/alova.ts中搜索Unauthorized,将清除登录、重新登录、重新发起请求的逻辑修改为跳转登录页面,记得在跳转登录页面下边添加return逻辑,否则后续的失败提示依旧会执行。

请求拦截

文件位置http/alova.ts,目前已封装常规的拦截器,功能包含:

  1. 请求头添加 token、小程序APPID
  2. 请求拦截,未登录自动调用静默登录
  3. 登录失效自动调用重新登录
  4. 响应拦截,根据状态码判断是否需要登录认证、是否需要重新登录
  5. 错误日志上报,需要在WE分析中开启实时日志功能
  6. 支持多域名配置,在meta中配置domain,非业务主域名,不处理状态码,直接返回,需要在实际使用中自行处理

请求额外可用参数说明:

ts
// 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,下方展示的源码可能以后会出现一定的差异,可以简单了解下里边的业务逻辑。

点击查看源码
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

请求失败与重试

请求失败分两种情况:

  1. 在使用期间 token 失效
  2. 因网络或者其他原因请求失败

封装的逻辑有对应的处理方案,统一在接口请求异常时处理,在判断请求失效的逻辑中添加 retryCount < RETRY_COUNT 是为了避免因过度使用静默登录导致频率超限所导致无效的请求

如果有登录页,可以注释重新登录逻辑,开启跳转登录页面逻辑,开启该方案,重试逻辑会失效

重新授权逻辑处理完毕后,处理重试逻辑判断,避免出现死循环

ts
// ****** 接口请求错误逻辑 ******
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,
  })
}

使用示例

接口定义示例

getpost传参有区别,详情可查看method详解

  • get: 链接、配置项
  • post: 链接、参数、配置项
ts
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 中,默认是不开启缓存的,可以针对某个接口开启缓存,例如:

ts
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),
  })
}

全局设置缓存模式

全局设置缓存模式,在创建实例化的时候设置,因弊端太多,不建议使用

ts
const alovaInstance = createAlova({
  // ...
  cacheFor: {
    // 统一设置POST的缓存模式
    POST: {
      mode: 'restore',
      expire: 60 * 10 * 1000,
    },
    // 统一设置HEAD请求的缓存模式
    HEAD: 60 * 10 * 1000,
  },
})

更改缓存适配器

SessionStorage 存储适配器示例

ts
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 实例级别的全局配置,并不能单独给某一个接口设置不同的缓存适配器。

普通使用示例

ts
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)
  })
}

自动管理请求状态

useRequest 官方文档

ts
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

分页请求策略 官方文档

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 设置请求中间件来自由控制请求行为,提供了强大的、几乎能控制一个请求的所有行为的能力,无论简单还是复杂的请求策略,可能你都会用上它,接下来我们看下它到底有什么神通。

中间件函数

请求中间件是一个异步函数,以下是一个简单的请求中间件,它在请求前和请求后分别打印了一些信息,没有改变任何请求行为。

ts
useRequest(todoList, {
  async middleware(_, next) {
    console.log('before request')
    await next()
    console.log('after requeste')
  },
})

忽略请求

当你不希望发出请求时,可不调用 next来忽略本次请求,就好像从来没有发起过请求一样。例如在useWatcher中某个监听字段变化时不发出请求。

ts
useWatcher(() => todoList(), [state1], {
  middleware: async (_, next) => {
    if (state1 === 'a') {
      return next()
    }
  },
})

控制响应数据

中间件函数的返回值将作为本次请求的响应数据参与后续的处理,如果中间件没有返回任何数据但调用了 next,则会将本次请求的响应数据参与后续处理。

ts
// 转换响应数据并返回
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 中间件