ice/website/docs/guide/advanced/request.md

522 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 网络请求
:::tip
小程序端不支持该能力。
:::
大部分前端应用都会选择通过 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 } = useRequest('/api/repo');
// 用法 2传入配置对象
const { data, error, loading } = useRequest({
url: '/api/repo',
method: 'get',
});
// 用法 3传入 service 函数
const { data, error, loading, request } = useRequest((id) => ({
url: '/api/repo',
method: 'get',
data: { id },
});
```
更多使用方式详见 [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.
BASEURL=http://localhost:9999/api
```
```shell title=".env.prod"
BASEURL=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.BASEURL,
});
```