alova(读作/əˈləʊva/) 是一个流程简化的下一代请求工具,它可以将你的 API 集成工作流从 7 个步骤极致地简化为 1 个步骤,你只需要选择 API 即可使用。^1
image.png

特性

  • 简单易用,并且学习成本更低。
  • 更先进的 openAPI 解决方案,直接扔掉中间的API文档吧。
  • 15+ 高性能的请求策略应对复杂的请求场景,帮助你快速开发性能更好的应用。

使用

安装

首先安装依赖,使用npm等方式进行安装

$ npm install alova --save
$ yarn add alova
$ pnpm add alova
$ bun add alova

创建示例

在Alova中,所有的请求都需要通过alova实例发起,在创建时需要使用请求适配器,像fetch、XMLHttpRequest等都有独立的适配器,使用某个适配器创建的alova实例会用指定的方式发起请求。

import { createAlova } from 'alova';
import adapterFetch from 'alova/fetch';

const alovaInstance = createAlova({
  requestAdapter: adapterFetch(), // 请求适配器
  baseURL: 'https://example/api/v5', // 路径前缀
  timeout: 50000, // 全局超时
  shareRequest: false, // 全局共享请求
  cacheFor: { // 全局缓存
    GET: 0, // 关闭所有GET缓存  
    POST: 60 * 60 * 1000 // 设置所有POST缓存1小时  
    }
});

发起请求

请求的参数和请求头配置可以在config对象中进行添加,如下

// get 请求
const response = await alovaInstance.Get('example.com/api/get', {  
    params: {  
        keyword: 'foo'
    },
    pathParams: {
        userId: 1
    },
    header: {
        'Content-Type': 'application/json;charset=UTF-8'
    }
});

// post 请求
const response = alovaInstance.Post('example.com/api/post', {
  title: 'foo',
  body: 'bar',
});

alova 共提供了 GET、POST、PUT、DELETE、HEAD、OPTIONS、PATCH 7 种请求类型。

实例创建函数 参数
GET alovaInstance.Get(url[, config])
POST alovaInstance.Post(url[, data[, config]])
PUT alovaInstance.Put(url[, data[, config]])
DELETE alovaInstance.Delete(url[, data[, config]])
HEAD alovaInstance.Head(url[, config])
OPTIONS alovaInstance.Options(url[, config])
PATCH alovaInstance.Patch(url[, data[, config]])

请求行为

请求行为控制都是在config中编写的配置项,常用的有如下几种
请求超时

const response = await alovaInstance.Get('example.com/api/get', {  
    timeout: 1000
});

请求共享
请求共享的作用:多个地方触发了同一个请求时,使用同一个网络请求进行返回,提高了页面流畅度,减轻了服务器的压力。

const response = await alovaInstance.Get('example.com/api/get', {  
    shareRequest: true
});

结果缓存
请求内容缓存是现在很多主流请求工具必备的能力了,页面切换时通过缓存的数据先进行渲染,数据刷新之后更新渲染,可以大大的提升用户体验。

const response = await alovaInstance.Get('example.com/api/get', {  
    cacheFor: 1000 * 60
});

转换响应数据
alova请求的配置中支持函数类型的配置,此处就是一种使用方式,通过转换函数将返回的响应数据进行转换以达到更符合视图渲染的结构

const response = await alovaInstance.Get('example.com/api/get', {  
    transform(rawData, headers) {  
        return rawData.list.map(item => {  
            return {  
                ...item,  
                isOK: item.done ? true : false
            };
        });
    }
});

元数据
元数据可以在method中添加,在拦截器中进行使用,用于控制拦截器针对不同请求的处理效果

const response = await alovaInstance.Get('example.com/api/get', {  
    meta: {  
        ignoreToken: true
    }
});

全局拦截器

拦截器也是请求工具最基础的能力了,拦截器是一种在请求发送前或响应返回后,对请求和响应进行统一处理的机制。它的核心作用是通过全局或模块化的方式,对 HTTP 请求和响应进行统一处理,减少重复代码,提升代码可维护性。
请求拦截器
在请求发送前对配置进行修改或执行操作:

  • 添加全局请求头:例如自动添加 Authorization 请求头(Token 认证)。
  • 参数处理:统一序列化请求参数(如转换 JSON、处理 FormData)。
  • 加载状态管理:显示全局 Loading 动画。
  • 日志记录:记录请求日志,用于调试或监控。
  • 权限校验:检查用户权限,拦截非法请求。
  • 重试机制:在请求失败时自动重试(如网络波动)。
const alovaInstance = createAlova({
    async beforeRequest(method) {
         // method.meta 可以拿到请求中配置的元数据
         if(!method.meta?.ignoreToken) {
             method.config.headers.token = 'token';
         }
    }  
});

响应拦截器
image.png

在响应返回后对结果进行统一处理:

  • 全局错误处理:统一处理 HTTP 错误状态码(如 401、404、500)。
  • 数据格式化:统一提取响应中的核心数据(如 response.data)。
  • 异常提示:根据错误类型弹出 Toast、Modal 等提示。
  • Token 刷新:在 Token 过期时自动刷新并重新发起请求。
  • 响应日志:记录接口耗时、响应数据。
const alovaInstance = createAlova({
  responded: {
    onSuccess: async (response, method) => {
      const json = await response.json();
      if (json.code !== 200) {
        // 抛出错误或返回reject状态的Promise实例时,此请求将抛出错误
        throw new Error(json.message);
      }
      return json.data;
    },
    onError: (err, method) => {
      alert(err.message);
    },
    onComplete: async method => {
      // 处理请求完成逻辑
    }
  }
});

代码生成

代码生成是Alova生态中重要的一环,是简化请求开发的关键一步,通过接口描述文件(目前只支持swagger的openapi协议)自动生成api文件,前端开发者不必再手写接口请求,一键生成,后面直接使用即可。
要使用代码生成能力需要安装额外的依赖

$ pnpm add @alova/wormhole -D

安装完依赖之后创建一个alova.config.js配置文件,用于定义生成代码的配置文件,例如

export default {
    // api生成设置数组,每项代表一个自动生成的规则,包含生成的输入输出目录、规范文件地址等等
    generator: [
        {
            // 接口文件和类型文件的输出路径,多个generator不能重复的地址,否则生成的代码会相互覆盖
            output: 'src/services',
            // input参数1:openapi的json文件url地址
            input: 'http://127.0.0.1:7001/swagger-ui/index.json',
            // (可选)指定生成的响应数据的mediaType,以此数据类型来生成2xx状态码的响应ts格式,默认application/json
            responseMediaType: 'application/json',
            // (可选)指定生成的请求体数据的bodyMediaType,以此数据类型来生成请求体的ts格式,默认application/json
            bodyMediaType: 'application/json',
            type: 'ts',
            global: 'Apis',
            globalHost: 'globalThis'
        },
    ],
};

然后可以执行命令来生成接口文件

$ npx alova gen -f # 强制覆盖已存在的目录

alova 还提供了VSCode插件来更加便捷的使用alova,可以一键生成请求,只需要安装alova插件之后点击右下角的image.png图标即可自动生成。
而且插件还提供了快捷引用接口的能力,只需要输入a->关键字即可唤起快捷输入面板,并且每个方法的接口描述都可以进行预览。
image.png

框架中使用

Alova 提供了针对主流前端框架的Hooks适配,只需要在创建alova实例的时候指定对应平台的HookType即可。
例如在React中,可以如下创建alova实例

import { createAlova } from 'alova';
import ReactHook from 'alova/react';

export const alovaInstance = createAlova({
  statesHook: ReactHook
});

然后就可以使用内置的策略来进行请求行为了,例如最基本的请求动作

import { useRequest } from 'alova/client';
import { alovaInstance } from './api';

const App = () => {
  const { loading, data, error } = useRequest(
    alovaInstance.Get('/user', {
      params: {
        uid: '1'
      }
    }),
    {
      initialData: {}, // data状态的初始数据
      immediate: true, // 立即发送请求,默认true
      force: true // 强制请求,不是用缓存
    }
  ).onSuccess(event => {
    event.method; // 请求的method实例
    event.data; // 请求的响应数据
  });

  if (loading) {
    return <div>Loading...</div>;
  } else if (error) {
    return <div>{error.message}</div>;
  }
  return (
    <div>
      <div>请求结果: {JSON.stringify(data)}</div>
    </div>
  );
};
export default App;

Alova提供了其他的请求策略如下👇👇👇

策略

所谓的策略,就是Alova提供的Hooks或者函数,可以在不同的场景使用不同的Hooks发起请求或者函数处理,可以帮助开发者节省一些业务处理的复杂度。

自动管理请求状态

useRequest 是Alova中最常用、最基础的一个Hooks,其他的Hooks基本上都继承了useRequest的能力,用法类似于ahooksuseRequest,可以自动管理请求过程中的一些状态,包括dataloadingerror等。
使用方式就是上面一小节的代码示例。

监听请求

当某个数据发生变化时重新请求来刷新数据是一个很常见的场景,比如搜索场景,当输入框关键字发生变化时来重新请求模糊匹配的内容。
类似场景我们可以通过useWatcher来处理,例如来实现一个简单的模糊搜索功能,通过用户名关键字来搜索用户,并进行防抖优化

import { useWatcher } from 'alova/client'
import styles from './index.module.less'
import { Input } from '@/components/ui/input'
import { useState } from 'react'

export default function Alova() {
  const [key, setKey] = useState('')
  const {data: users} = useWatcher(Apis.api.apicontroller_getuserbykeyword(
    {
      params: { keyword: key },
    }),
    [key],
    {
      force: true,
      debounce: 500, // 防抖 500ms
      initialData: []
    }
  )


  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>useWatcher</div>
        <div className={styles.name}>
          <Input onChange={({target}) => setKey(target.value)} />
          <ul>
            {users.map(({id, username}) => <li id={id}>{username}</li>)}
          </ul>
        </div>
      </div>
    </div>
  )
}

此时一个模糊搜索功能就实现了,没错,就是这么简单
image.png

表单请求

表单处理是平时最常见的需求类型之一,Alova提供了useForm来处理表单策略。比起useRequest的返回值多了formupdateForm,用于获取表单数据和更新表单数据。
例如下面一段代码,实现了一个简易的表单提交过程,通过缓存将表单数据进行持久化实现草稿功能,并且提交成功之后会自动重置表单项。

import { useForm } from 'alova/client'
import styles from './index.module.less'
import { toast } from 'sonner'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

export default function Alova() {
  const {
    form, // 表单内容
    send: submit, // 提交
    updateForm, // 更新表单内容
    loading: submitting,
    reset, // 手动重置数据
  } = useForm(
    (formData) => {
      return Apis.api.apicontroller_createuser({ data: formData })
    },
    {
      initialForm: { username: '', password: '' }, // 初始表单数据
      resetAfterSubmiting: true, // 提交完成之后自动重置表单数据
      store: true, // 持久化
    }
  ).onSuccess(() => {
    toast.success('创建成功')
  })

  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>useForm</div>
        <div className={''}>
          <label htmlFor="username">username</label>
          <Input
            name="username"
            value={form?.username || ''}
            onChange={({ target }) => updateForm({ username: target.value })}
          />
          
          <label htmlFor="password">password</label>
          <Input
            name="password"
            type="password"
            value={form?.password || ''}
            onChange={({ target }) => updateForm({ password: target.value })}
          />

          <Button onClick={() => submit()} disabled={submitting}>提交</Button>
        </div>
      </div>
    </div>
  )
}

点击提交后效果如下
image.png

注意⚠️
useForm中要使用form来获取表单数据,initialForm来设置表单初始值。
updateForm不止在创建表单时有用,在编辑表单回填数据时更加方便,异步获取数据之后通过updateForm更新表单数据即可。
草稿会在表单更新时同步更新,需要注意的是如果表单没有自动重置的话草稿内容也不会更新,需要手动重置来清空草稿。此外,非JS原生类型数据的回填需要设置序列化函数,否则不会自动回填。

除了上面的基础内容外,复杂表单拆分多步处理也比较常见,Alova也支持这种场景的处理,通过给表单设置id来实现多个组件或者多个页面中共享表单数据,返回值中的form是同一份引用,在任意表单下触发send都可以把表单数据统一提交。

// form1

const form1 = useForm(
    (formData) => {
      return Apis.api.apicontroller_createuser({ data: formData })
    },
    {
      initialForm: {
        step1Input: '',
        step2Input: '',
        step3Input: ''
      },
      id: 'step-form'
    }
  )

// form2
const form2 = useForm(
    (formData) => {
      return Apis.api.apicontroller_createuser({ data: formData })
    },
    {
      id: 'step-form'
    }
  )

分页请求

表格是中后台业务中最常见的表现形式,在移动端中的下拉刷新也是一种常见形式,这两种都用到了分页获取数据的方式,Alova也提供了分页请求策略,方便分页请求数据。

import { usePagination } from 'alova/client'
import styles from './index.module.less'
import { Button } from '@/components/ui/button'

export default function Alova() {
  const {
    update, // 更新状态
    data: list, // 数据
    isLastPage, // 根据total和size计算,如果没有total根据当前页数据是否满足size判断
    total, // 总条数
    page, // 当前页码
    pageCount, // 总页数
  } = usePagination((page, size) => Apis.api.apicontroller_getusers({
    params: { page, size },
    meta: {page: true} // 给alova实例传递参数,用于处理不同的响应处理
  }), {
    initialData: { // 初始数据
      total: 0,
      data: []
    },
    initialPage: 1, // 初始页码,默认为1
    initialPageSize: 5, // 初始每页数据条数,默认为10
    total: res => res.total, // 从响应中获取总量(默认取total字段)
    data: res => res.data, // 从数据中获取data(默认取data字段)
  })


  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>usePagination</div>
        <div className={styles.name}>
          <table>
            <thead>
              <tr>
                <th>id</th>
                <th>username</th>
                <th>createTime</th>
              </tr>
            </thead>
            <tbody>
              {list?.map((item) => (<tr key={item.id}>
                <td>{ item.id }</td>
                <td>{ item.username }</td>
                <td>{new Date(item.createTime).toLocaleString() }</td>
              </tr>))}
            </tbody>
          </table>
          <Button
            disabled={page === 1}
            onClick={() => {
              update({page: page - 1})
            }}
          >
            上一页
          </Button>
          {page}/{pageCount} 共{total}条
          <Button
            disabled={isLastPage}
            onClick={() => {
              update({page: page + 1})
            }}
          >
            下一页
          </Button>
        </div>
      </div>
    </div>
  )
}

total在不同的业务接口中可能以不同的字段返回,alova也想到了这一点,人性化的提供了函数配置来自定义获取total。

此时展示效果如下
image.png
可以看到表格数据已经渲染出来了,此时我们可以发现alova自动帮我们预加载了下一页的数据。
image.png
如果不想使用预加载,可以手动关闭

usePagination((page, size) => Apis.api.apicontroller_getusers({
    params: { page, size },
  }), {  
    // ...  
    preloadPreviousPage: false, // 关闭预加载上一页数据  
    preloadNextPage: false // 关闭预加载下一页数据  
});

如果在移动端使用,要实现下拉刷新可以通过追加模式来实现,会将请求到的数据追加到data中

usePagination((page, size) => Apis.api.apicontroller_getusers({
    params: { page, size },
  }), {  
    // ...  
    append: true
});

数据预拉取

不止分页策略可以预加载数据,其他请求也可以通过useFetcher来提前加载请求。原理是手动触发一次请求,将结果缓存起来,当请求触发时直接从缓存中获取结果。
例如我们可以在鼠标悬浮后提前加载详情的数据,当鼠标点击跳转后就可以快速获取页面内容了。

import { useFetcher } from 'alova/client'
import styles from './index.module.less'
import { Input } from '@/components/ui/input'

export default function Alova() {
  const { fetch } = useFetcher({
    updateState: false, // 关闭跨组件更新,一般使用预拉取都是跨组件的
  })
  function hoverUser(uid) {
    fetch(Apis.api.apicontroller_getuser({
      pathParams: {
        uid
      },
    }))
  }


  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>useFetcher</div>
        <div className={styles.name}>
          <Input onChange={({ target }) => setKey(target.value)} />
          <ul>
            {users.map(({ id, username }) =>
              (<li
                key={id}
                onMouseEnter={() => hoverUser(id)}
              >
                {username}
            </li>))
            }
          </ul>
        </div>
      </div>
    </div>
  )
}

useFetcher.gif

自动请求

有些场景下我们需要刷新数据,但是没有用户在页面触发事件来进行更新,此时我们可以通过浏览器的一些事件来进行更新,比如浏览器的聚焦、网络变化等。
Alova提供的useAutoRequest就可以通过这些时机来进行更新数据。

import { useAutoRequest } from 'alova/client'
import styles from './index.module.less'

export default function Alova() {
  const { data, loading } = useAutoRequest(Apis.api.apicontroller_getuser({
    pathParams: {
      uid: 1
    },
    cacheFor: 0
  }), {
    initialData: {
      username: 'null'
    },
    enableVisibility: true, // 浏览器隐藏自动请求
    enableFocus: true, // 浏览器聚焦自动请求
    enableNetwork: true, // 网络变化自动请求
    throttle: 1000, // 节流时间
  })

  return (
    <div className={styles.container}>
      <div className={styles.card} style={{ display: 'block' }}>
        <div className={styles.title}>useAutoRequest</div>
        <div className={styles.name}>{
          loading
            ? 'Loading...'
            : `UID: 1 -> userName:${data?.username}`
        }</div>
      </div>
    </div>
  )
}

autoRequest.gif
当需要停止自动刷新时,可以通过中间件来控制自动请求行为

let pause = false;
useAutoRequest({
  // ...
  middleware(_, next) {
    if (!pause) {
      next();
    }
  }
});

验证码

当敏感操作需要验证用户身份时,我们经常通过手机号短信的形式来确认用户身份,但是发送短信验证码是需要计费的,所以通常会限制发送的频率,一般是一分钟一次。Alova针对这种场景提供了验证码策略来进行处理。

import { useCaptcha } from 'alova/client'
import styles from './index.module.less'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

export default function Alova() {
  const [mobile, setMobile] = useState('');
  const { countdown, send, loading: sending } = useCaptcha(
    Apis.api.apicontroller_sendsmscode({ params: { phone: mobile } }),
    { initialCountdown: 60 } // 倒计时时长
  )

  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>useCaptcha</div>
        <div className={styles.name}>
          <Input onChange={({ target }) => setMobile(target.value)} />
          <Button
            onClick={send}
            disabled={sending || countdown > 0}>
            {loading ? '发送中...' : countdown > 0 ? `${countdown}后可重发` : '发送验证码'}
          </Button>
        </div>
      </div>
    </div>
  )
}

image.png

SSE

随着AI的崛起,对话式AI应用也越来越多,由于生成内容需要时间,所以常见的方式是通过SSE的形式将生成的内容分段发送到前端展示,Alvoa也跟上了潮流,提供了useSSE这个Hooks来方便这种场景的使用。
通过useSSE我们可以很方便的绑定各种事件处理,例如

import { useSSE } from 'alova/client'
import styles from './index.module.less'
import { Button } from '@/components/ui/button'

export default function Alova() {
  const [text, setText] = useState('');
  const { send: sendMessage, readyState, onMessage } = useSSE(
    Apis.api.apicontroller_sse({
      
    }),
    {
      immediate: true, // 立即发起, 如果不自动发起可以通过send来手动触发
      interceptByGlobalResponded: false // 现在数据不会被响应拦截
    }
  )
  onMessage(message => {
    setText(text + `${message.data}, `)
  })
  
  return (
    <div className={styles.container}>
      <div className={styles.card}>
        <div className={styles.title}>useSSE</div>
        <div className={styles.name}>
          <Button onClick={sendMessage} disabled={readyState <= 1}>开始</Button>
          <br />
          <textarea value={text} readOnly />
        </div>
      </div>
    </div>
  )
}

image.png

提示🔔:如果你自己编写后端接口来测试SSE,在流式返回数据的时候要将内容写为stream.write(data: ${i}\n\n)格式,这是SSE message事件的标准格式,如果不一致则无法触发message事件。
SSE连接只能存在一个,send()函数触发会中断上一条连接。

除了onMessage事件,还可以绑定onOpenonError,甚至可以通过on`函数来绑定任意事件。

type SSEReturnType<S, Data> = {
  readyState: ExportedType<SSEHookReadyState, S>;
  data: ExportedType<Data | undefined, S>;
  eventSource: ExportedType<EventSource | undefined, S>;
  /**
   * 手动发起请求。在使用 `immediate: true` 时该方法会自动触发
   * @param args 请求参数,会传递给 method
   */
  send: (...args: any[]) => Promise<void>;
  /**
   * 关闭连接
   */
  close: () => void;
  /**
   * 注册 EventSource open 的回调函数
   * @param callback 回调函数
   * @returns 取消注册函数
   */
  onOpen(callback: SSEOnOpenTrigger): () => void;

  /**
   * 注册 EventSource message 的回调函数
   * @param callback 回调函数
   * @returns 取消注册函数
   */
  onMessage<T = Data>(callback: SSEOnMessageTrigger<T>): () => void;

  /**
   * 注册 EventSource error 的回调函数
   * @param callback 回调函数
   * @returns 取消注册函数
   */
  onError(callback: SSEOnErrorTrigger): () => void;

  /**
   * @param eventName 事件名称,默认存在 `open` | `error` | `message`
   * @param handler 事件处理器
   */
  on(
    eventName: string,
    handler: (event: AlovaSSEMessageEvent<S, E, R, T, RC, RE, RH>) => void
  ) => () => void;
};

请求重试

有些场景下我们需要轮训接口来获取结果,比如支付时我们需要轮训流水号的订单状态,如果状态没有发生改变就持续轮训下去,这种场景下我们手写代码是不太好控制的。Alova的请求重试策略就可以非常简单的进行处理,通过动态重试就可以很好的控制。

const { 
  data,
  stop // 当重试到达阈值之后停止重试
} = useRetriableRequest(method, {
  // 第一个参数为上一次的错误实例,从第二个参数开始为send传入的参数
  retry(error, ...args) {
    // 请求超时则继续重试
    return error.code === '130987';
  },
  // retry: 5 // 静态次数,固定5次
  backoff: {  
    delay: 2000, // 设置延迟时间为2秒
    multiplier: 2, // 延迟倍数,下一次请求延迟是上一次请求的x倍
  }
});

串行请求

当某些请求存在前后依赖关系时,可以通过useSerialRequest来组合请求链

import { useSerialRequest } from 'alova/client';

const {
  loading, // 串行加载状态,全部请求完成才会改为false
  data, // 最后一个请求的响应数据
  send
} = useSerialRequest(
  [
    // args为send函数传入的参数
    (...args) => request1(args),
    // 从第二个handler开始,第一个参数为上一个请求的响应数据,args从第二个开始接收
    (response1, ...args) => request2(response1, args),
    (response2, ...args) => request3(response2, args)
  ],
  {
    immediate: false
  }
);

// 手动触发请求并传参
send(1);

前端小白