---
order: 0302
---

# HTTP 请求

大部分前端应用都会选择通过 HTTP(s) 协议与后端服务通讯。  
ice.js 提供了一套从 UI 交互到请求服务端数据的完整方案，通过切面编程的方式统一了数据请求管理，简化了设置参数、错误处理等逻辑的实现。

## 安装 [request 插件](https://www.npmjs.com/@ice/plugin-request)

网络请求是可选能力，在使用前需要单独安装 `@ice/plugin-request` 插件。

```bash
npm i @ice/plugin-request -D
```

在配置文件中添加插件：

```tsx title="ice.config.mts"
import { defineConfig } from '@ice/app';
import request from '@ice/plugin-request';

export default defineConfig(() => ({
  plugins: [
    request(),
  ],
}));
```

## 目录约定

框架约定 `service` 目录用于收敛请求逻辑，目录组织如下：

```diff
 src
 ├── models
+├── services                       // 定义全局数据请求，非必须
+│   └── user.ts
 └── pages
 |   ├── home
 |   │   ├── models
+|   │   ├── services               // 定义页面级数据请求
+|   │   |    └── repo.ts
 |   │   └── components
 |   ├── about
 |   │   ├── services
 |   │   ├── components
 |   │   └── index.tsx
 └── app.ts
```

通过调用 `request` 定义数据请求如下：

```ts title="pages/home/service/repo.ts"
import { request } from 'ice';

export default {
  // 简单场景
  async getUser() {
    return await request('/api/user');
  },

  // 参数场景
  async getRepo(id) {
    return await request(`/api/repo/${id}`);
  },

  // 格式化返回值
  async getDetail(params) {
    const data = await request({
      url: `/api/detail`,
      params
    });

    return data.map(item => {
      return {
        ...item,
        price: item.oldPrice,
        text: item.status === '1' ? '确定' : '取消'
      };
    });
  }
}
```

## 消费 service

消费 service 主要有两种方式：

- 在模型中调用 service：`service` -> `model` -> `view`
- 在视图中调用 service：`service` -> `view`

### 在模型中调用 service

> 结合 [状态管理](./store.md) 使用

- `service`：约定数据请求统一管理在 services 目录下；
- `model`：约定数据请求统一在 models 里进行调用；
- `view`：最终在视图里通过调用 models 的 effects 的方法触发数据请求。

在模型中调用定义好的 service：

```ts
import userService from '@/services/user';

// src/models/user.ts
export default {
  state: {
    name: 'taoxiaobao',
    age: 20,
  },
  reducers: {
    update(prevState, payload) {
      return { ...prevState, ...payload };
    },
  },
  effects: (dispatch) => ({
    async fetchUserInfo() {
      const data = await userService.getUser();
      dispatch.user.update(data);
    },
  }),
};
```

- 在视图中调用模型方法：

```tsx
import React, { useEffect } from 'react';
import store from '@/store';

const HomePage = () => {
  // 调用定义的 user 模型
  const [userState, userDispatchers] = store.useModel('user');

  useEffect(() => {
    // 调用 user 模型中的 fetchUserInfo 方法
    userDispatchers.fetchUserInfo();
  }, []);

  return <>Home</>;
};
```

### 在视图中调用 service

- `service`：约定数据请求统一管理在 services 目录下；
- `view`：最终在视图里通过 useRequest 直接调用 service 触发数据请求。

```tsx
import React, { useEffect } from 'react';
import { useRequest } from 'ice';
import userService from '@/services/user';

export default function HomePage() {
  // 调用 service
  const { data, error, loading, request } = useRequest(userService.getUser);

  useEffect(() => {
    // 触发数据请求
    request();
  }, []);

  return <>Home</>;
}
```

## API

### request

request 基于 axios 进行封装，在使用上整体与 axios 保持一致，差异点：

1. 默认只返回服务端响应的数据 `Response.data`，而不是整个 Response，如需返回整个 Response 请通过 `withFullResponse` 参数开启
2. 在 axios 基础上默认支持了多请求实例的能力

使用方式如下：

```ts
import { request } from 'ice';

async function getList() {
  const resData = await request({
    url: '/api/user',
  });
  console.log(resData.list);

  const { status, statusText, data } = await request({
    url: '/api/user',
    withFullResponse: true
  });
  console.log(data.list);
}
```

常用使用方式：

```js
request(RequestConfig);

request.get('/user', RequestConfig);
request.post('/user', data, RequestConfig);
```

RequestConfig:

```js
{
  // `url` is the server URL that will be used for the request
  url: '/user',
  // `method` is the request method to be used when making the request
  method: 'get', // default
  // `headers` are custom headers to be sent
  headers: {'X-Requested-With': 'XMLHttpRequest'},
  // `params` are the URL parameters to be sent with the request
  // Must be a plain object or a URLSearchParams object
  params: {
    ID: 12345
  },
  // `data` is the data to be sent as the request body
  // Only applicable for request methods 'PUT', 'POST', and 'PATCH'
  data: {
    firstName: 'Fred'
  },
  // `timeout` specifies the number of milliseconds before the request times out.
  // If the request takes longer than `timeout`, the request will be aborted.
  timeout: 1000, // default is `0` (no timeout)
  // `withCredentials` indicates whether or not cross-site Access-Control requests
  // should be made using credentials
  withCredentials: false, // default
  // `responseType` indicates the type of data that the server will respond with
  // options are: 'arraybuffer', 'document', 'json', 'text', 'stream'
  responseType: 'json', // default
  // should be made return full response
  withFullResponse: false,
  // request instance name
  instanceName: 'request2'
}
```

更完整的配置请 [参考](https://github.com/axios/axios#request-config)。

返回完整 Response Scheme 如下：

```ts
{
  // `data` is the response that was provided by the server
  data: {},

  // `status` is the HTTP status code from the server response
  status: 200,

  // `statusText` is the HTTP status message from the server response
  statusText: 'OK',

  // `headers` the HTTP headers that the server responded with
  // All header names are lower cased and can be accessed using the bracket notation.
  // Example: `response.headers['content-type']`
  headers: {},

  // `config` is the config that was provided to `axios` for the request
  config: {},

  // `request` is the request that generated this response
  // It is the last ClientRequest instance in node.js (in redirects)
  // and an XMLHttpRequest instance in the browser
  request: {}
}
```

### useRequest

使用 useRequest 可以极大的简化对请求状态的管理，useRequest 基于 [ahooks/useRequest](https://ahooks.js.org/zh-CN/hooks/use-request/index) 封装，差异点：

- 将 `requestMethod` 参数默认设置为上述的 `request`（即 axios），保证框架使用的一致性
- manual 参数默认值从 `false` 改为 `true`，因为实际业务更多都是要手动触发的
- 返回值 `run` 改为 `request`，因为更符合语义

#### API

```ts
const {
  // 请求返回的数据，默认为 undefined
  data,
  // 请求抛出的异常，默认为 undefined
  error,
  // 请求状态
  loading,
  // 手动触发请求，参数会传递给 service
  request,
  // 当次执行请求的参数数组
  params,
  // 取消当前请求，如果有轮询，停止
  cancel,
  // 使用上一次的 params，重新执行请求
  refresh,
  // 直接修改 data
  mutate,
  // 默认情况下，新请求会覆盖旧请求。如果设置了 fetchKey，则可以实现多个请求并行，fetches 存储了多个请求的状态
  fetches
} = useRequest(service, {
  // 默认为 true 即需要手动执行请求
  manual,
   // 初始化的 data
  initialData,
  // 请求成功时触发，参数为 data 和 params
  onSuccess,
  // 请求报错时触发，参数为 error 和 params
  onError,
  // 格式化请求结果
  formatResult,
  // 请求唯一标识
  cacheKey,
  // 设置显示 loading 的延迟时间，避免闪烁
  loadingDelay,
  // 默认参数
  defaultParams,
  // 轮询间隔，单位为毫秒
  pollingInterval,
  // 在页面隐藏时，是否继续轮询，默认为 true，即不会停止轮询
  pollingWhenHidden,
  // 根据 params，获取当前请求的 key
  fetchKey,
  // 在屏幕重新获取焦点或重新显示时，是否重新发起请求。默认为 false，即不会重新发起请求
  refreshOnWindowFocus,
  // 屏幕重新聚焦，如果每次都重新发起请求，不是很好，我们需要有一个时间间隔，在当前时间间隔内，不会重新发起请求，需要配置 refreshOnWindowFocus 使用
  focusTimespan,
  // 防抖间隔, 单位为毫秒，设置后，请求进入防抖模式
  debounceInterval,
  // 节流间隔, 单位为毫秒，设置后，请求进入节流模式。
  throttleInterval,
  // 只有当 ready 为 true 时，才会发起请求
  ready,
  // 在 manual = false 时，refreshDeps 变化，会触发请求重新执行
  refreshDeps,
});
```

#### 常用使用方式

```ts
import { useRequest } from 'ice';
// 用法 1：传入请求地址
const { data, error, loading, request } = useRequest('/api/repo');

request();

// 用法 2：传入 Axios 配置对象
const { data, error, loading, request } = useRequest({
  url: '/api/repo',
  method: 'get',
});

request();

// 用法 3：传入 service 函数
const { data, error, loading, request } = useRequest((id) => Promise.resolve({
  url: '/api/repo',
  method: 'get',
  data: { id },
}));

request();
```

更多使用方式详见 [ahooks/useRequest](https://ahooks.js.org/zh-CN/hooks/use-request/index)

### 请求配置

在实际项目中通常需要对请求进行全局统一的封装，例如配置请求的 baseURL、统一 header、拦截请求和响应等等，这时只需要在应用的的 appConfig 中进行配置即可。

```ts title="src/app.tsx"
import { defineRequestConfig } from '@ice/plugin-request/types';

export const requestConfig = defineRequestConfig({
  // 可选的，全局设置 request 是否返回 response 对象，默认为 false
  withFullResponse: false,

  baseURL: '/api',
  headers: {},
  // ...RequestConfig 其他参数

  // 拦截器
  interceptors: {
    request: {
      onConfig: (config) => {
        // 发送请求前：可以对 RequestConfig 做一些统一处理
        config.headers = { a: 1 };
        return config;
      },
      onError: (error) => {
        return Promise.reject(error);
      },
    },
    response: {
      onConfig: (response) => {
        // 请求成功：可以做全局的 toast 展示，或者对 response 做一些格式化
        if (!response.data.status !== 1) {
          alert('请求失败');
        }
        return response;
      },
      onError: (error) => {
        // 请求出错：服务端返回错误状态码
        console.log(error.response.data);
        console.log(error.response.status);
        console.log(error.response.headers);
        return Promise.reject(error);
      },
    },
  },
});
```

### 多个请求配置

在某些复杂场景的应用中，我们也可以配置多个请求，每个配置请求都是单一的实例对象。

```ts title="src/app.tsx"
import { defineRequestConfig } from '@ice/plugin-request/types';

export const requestConfig = defineRequestConfig([
  {
    baseURL: '/api',
    // ...RequestConfig 其他参数
  },
  {
    // 配置 request 实例名称，如果不配默认使用内置的 request 实例
    instanceName: 'request2',
    baseURL: '/api2',
    // ...RequestConfig 其他参数
  }
]);
```

使用示例：

```ts
import { request } from 'ice';

export default {
  // 使用默认的请求方法，即调用 /api/user 接口
  async getUser() {
    return await request({
      url: '/user',
    });
  },

  // 使用自定义的 request 请求方法，即调用接口 /api2/user
  async getRepo(id) {
    return await request({
      instanceName: 'request2',
      url: `/repo/${id}`,
    });
  },
};
```

## 异常处理

无论是拦截器里的错误参数，还是 `request` / `useRequest` 返回的错误对象，都符合以下类型：

```js
const error = {
  // 服务端返回错误状态码时则存在该字段
  response: {
    data: {},
    status: {},
    headers: {}
  },
  // 服务端未返回结构时则存在该字段
  request: XMLHttpRequest,
  // 一定存在，即 RequestConfig
  config: {
  },
  // 一定存在
  message: ''
}
```

## 高阶用法

### Mock 接口

项目开发初期，后端接口可能还没开发好或不够稳定，此时前端可以通过 Mock 的方式来模拟接口，参考文档 [本地 Mock 能力](../basic/mock.md)。

### 如何解决接口跨域问题

当访问页面地址和请求接口地址的域名或端口不一致时，就会因为浏览器的同源策略导致跨域问题，此时推荐后端接口通过 CORS 支持信任域名的跨域访问，具体请参考：

- [HTTP 访问控制（CORS）](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS)
- [跨域资源共享 CORS 详解](https://www.ruanyifeng.com/blog/2016/04/cors.html)
- [Using CORS](https://www.html5rocks.com/en/tutorials/cors/)

### 根据环境配置不同的 baseURL

大部分情况下，前端代码里用到的后端接口写的都是相对路径如 `/api/getFoo.json`，然后访问不同环境时浏览器会根据当前域名发起对应的请求。如果域名跟实际请求的接口地址不一致，则需要通过 `request.baseURL` 来配置：

```ts title="src/app.tsx"
import { defineRequestConfig } from '@ice/plugin-request/types';

export const requestConfig = defineRequestConfig({
  baseURL: '//service.example.com/api',
});
```

结合[构建配置](../basic/env.md)即可实现不同环境使用不同的 baseURL：

```shell title=".env.local"
# The should not be committed.
ICE_BASE_URL=http://localhost:9999/api
```

```shell title=".env.production"
ICE_BASE_URL=https://example.com/api
```

在 `src/app.tsx` 中配置 `request.baseURL`:

```ts title="src/app.tsx"
import { defineRequestConfig } from '@ice/plugin-request/types';

export const requestConfig = defineRequestConfig({
  baseURL: process.env.ICE_BASE_URL,
});
```
