数据请求

大多数前端应用都需要通过 HTTP 协议与后端服务器通讯。在 icejs 框架中内置约定和规范了一套从 UI 交互到请求服务端数据的完整方案,更进一步简化了应用的数据请求流程,基于此提供了 request 和 useRequest Hooks 方法。

目录约定

目录组织如下:

  1. src
  2. ├── models
  3. +├── services // 定义全局数据请求
  4. +│ └── user.ts
  5. └── pages
  6. | ├── Home
  7. | ├── models
  8. +| ├── services // 定义页面级数据请求
  9. +| | └── repo.ts
  10. | └── components
  11. | ├── About
  12. | ├── services
  13. | ├── components
  14. | └── index.tsx
  15. └── app.ts

定义 service

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

  1. import { request } from 'ice';
  2. export default {
  3. // 简单场景
  4. async getUser() {
  5. return await request('/api/user');
  6. },
  7. // 参数场景
  8. async getRepo(id) {
  9. return await request(`/api/repo/${id}`);
  10. },
  11. // 格式化返回值
  12. async getDetail(params) {
  13. const data = await request({
  14. url: `/api/detail`,
  15. params
  16. });
  17. return data.map(item => {
  18. return {
  19. ...item,
  20. price: item.oldPrice,
  21. text: item.status === '1' ? '确定' : '取消'
  22. };
  23. });
  24. }
  25. }

消费 service

消费 service 主要有两种方式:

  • 在模型中调用 service:service -> model -> view
  • 在视图中调用 service:service -> view

在模型中调用 service

结合 状态管理文档 使用

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

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

  1. import userService from '@/services/user';
  2. // src/models/user.ts
  3. export default {
  4. state: {
  5. name: 'taoxiaobao',
  6. age: 20,
  7. },
  8. reducers: {
  9. update(prevState, payload) {
  10. return { ...prevState, ...payload };
  11. },
  12. },
  13. effects: (dispatch) => ({
  14. async fetchUserInfo() {
  15. const data = await userService.getUser();
  16. dispatch.user.update(data);
  17. },
  18. }),
  19. };
  • 在视图中调用模型方法:
  1. import React, { useEffect } from 'react';
  2. import store from '@/store';
  3. const HomePage = () => {
  4. // 调用定义的 user 模型
  5. const [userState, userDispatchers] = store.useModel('user');
  6. useEffect(() => {
  7. // 调用 user 模型中的 fetchUserInfo 方法
  8. userDispatchers.fetchUserInfo();
  9. }, []);
  10. return <>Home</>;
  11. };

在视图中调用 service

  • service:约定数据请求统一管理在 services 目录下;
  • view:最终在视图里通过 useRequest 直接调用 service 触发数据请求。
  1. import React, { useEffect } from 'react';
  2. import { useRequest } from 'ice';
  3. import userService from '@/services/user';
  4. export default function HomePage() {
  5. // 调用 service
  6. const { data, error, loading, request } = useRequest(userService.getUser);
  7. useEffect(() => {
  8. // 触发数据请求
  9. request();
  10. }, []);
  11. return <>Home</>;
  12. }

API

request

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

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

使用方式如下:

  1. import { request } from 'ice';
  2. async function getList() {
  3. const resData = await request({
  4. url: '/api/user',
  5. });
  6. console.log(resData.list);
  7. const { status, statusText, data } = await request({
  8. url: '/api/user',
  9. withFullResponse: true
  10. });
  11. console.log(data.list);
  12. }

常用使用方式:

  1. request(RequestConfig);
  2. request.get('/user', RequestConfig);
  3. request.post('/user', data, RequestConfig);

RequestConfig:

  1. {
  2. // `url` is the server URL that will be used for the request
  3. url: '/user',
  4. // `method` is the request method to be used when making the request
  5. method: 'get', // default
  6. // `headers` are custom headers to be sent
  7. headers: {'X-Requested-With': 'XMLHttpRequest'},
  8. // `params` are the URL parameters to be sent with the request
  9. // Must be a plain object or a URLSearchParams object
  10. params: {
  11. ID: 12345
  12. },
  13. // `data` is the data to be sent as the request body
  14. // Only applicable for request methods 'PUT', 'POST', and 'PATCH'
  15. data: {
  16. firstName: 'Fred'
  17. },
  18. // `timeout` specifies the number of milliseconds before the request times out.
  19. // If the request takes longer than `timeout`, the request will be aborted.
  20. timeout: 1000, // default is `0` (no timeout)
  21. // `withCredentials` indicates whether or not cross-site Access-Control requests
  22. // should be made using credentials
  23. withCredentials: false, // default
  24. // `responseType` indicates the type of data that the server will respond with
  25. // options are: 'arraybuffer', 'document', 'json', 'text', 'stream'
  26. responseType: 'json', // default
  27. // should be made return full response
  28. withFullResponse: false,
  29. // request instance name
  30. instanceName: 'request2'
  31. }

更完整的配置请 参考

返回完整 Response Schema 如下:

  1. {
  2. // `data` is the response that was provided by the server
  3. data: {},
  4. // `status` is the HTTP status code from the server response
  5. status: 200,
  6. // `statusText` is the HTTP status message from the server response
  7. statusText: 'OK',
  8. // `headers` the HTTP headers that the server responded with
  9. // All header names are lower cased and can be accessed using the bracket notation.
  10. // Example: `response.headers['content-type']`
  11. headers: {},
  12. // `config` is the config that was provided to `axios` for the request
  13. config: {},
  14. // `request` is the request that generated this response
  15. // It is the last ClientRequest instance in node.js (in redirects)
  16. // and an XMLHttpRequest instance in the browser
  17. request: {}
  18. }

useRequest

使用 useRequest 可以极大的简化对请求状态的管理,useRequest 基于 ahooks/useRequest 封装,差异点:

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

API

  1. const {
  2. // 请求返回的数据,默认为 undefined
  3. data,
  4. // 请求抛出的异常,默认为 undefined
  5. error,
  6. // 请求状态
  7. loading,
  8. // 手动触发请求,参数会传递给 service
  9. request,
  10. // 当次执行请求的参数数组
  11. params,
  12. // 取消当前请求,如果有轮询,停止
  13. cancel,
  14. // 使用上一次的 params,重新执行请求
  15. refresh,
  16. // 直接修改 data
  17. mutate,
  18. // 默认情况下,新请求会覆盖旧请求。如果设置了 fetchKey,则可以实现多个请求并行,fetches 存储了多个请求的状态
  19. fetches
  20. } = useRequest(service, {
  21. // 默认为 true 即需要手动执行请求
  22. manual,
  23. // 初始化的 data
  24. initialData,
  25. // 请求成功时触发,参数为 data 和 params
  26. onSuccess,
  27. // 请求报错时触发,参数为 error 和 params
  28. onError,
  29. // 格式化请求结果
  30. formatResult,
  31. // 请求唯一标识
  32. cacheKey,
  33. // 设置显示 loading 的延迟时间,避免闪烁
  34. loadingDelay,
  35. // 默认参数
  36. defaultParams,
  37. // 轮询间隔,单位为毫秒
  38. pollingInterval
  39. // 在页面隐藏时,是否继续轮询,默认为 true,即不会停止轮询
  40. pollingWhenHidden,
  41. // 根据 params,获取当前请求的 key
  42. fetchKey,
  43. // 在屏幕重新获取焦点或重新显示时,是否重新发起请求。默认为 false,即不会重新发起请求
  44. refreshOnWindowFocus,
  45. // 屏幕重新聚焦,如果每次都重新发起请求,不是很好,我们需要有一个时间间隔,在当前时间间隔内,不会重新发起请求,需要配置 refreshOnWindowFocus 使用
  46. focusTimespan,
  47. // 防抖间隔, 单位为毫秒,设置后,请求进入防抖模式
  48. debounceInterval,
  49. // 节流间隔, 单位为毫秒,设置后,请求进入节流模式。
  50. throttleInterval,
  51. // 只有当 ready 为 true 时,才会发起请求
  52. ready,
  53. // 在 manual = false 时,refreshDeps 变化,会触发请求重新执行
  54. refreshDeps
  55. });

常用使用方式

  1. // 用法 1:传入字符串
  2. const { data, error, loading } = useRequest('/api/repo');
  3. // 用法 2:传入配置对象
  4. const { data, error, loading } = useRequest({
  5. url: '/api/repo',
  6. method: 'get',
  7. });
  8. // 用法 3:传入 service 函数
  9. const { data, error, loading, request } = useRequest((id) => ({
  10. url: '/api/repo',
  11. method: 'get',
  12. data: { id },
  13. });

更多使用方式详见 ahooks/useRequest

请求配置

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

  1. import { runApp } from 'ice';
  2. const appConfig = {
  3. request: {
  4. // 可选的,全局设置 request 是否返回 response 对象,默认为 false
  5. withFullResponse: false,
  6. baseURL: '/api',
  7. headers: {},
  8. // ...RequestConfig 其他参数
  9. // 拦截器
  10. interceptors: {
  11. request: {
  12. onConfig: (config) => {
  13. // 发送请求前:可以对 RequestConfig 做一些统一处理
  14. config.headers = { a: 1 };
  15. return config;
  16. },
  17. onError: (error) => {
  18. return Promise.reject(error);
  19. },
  20. },
  21. response: {
  22. onConfig: (response) => {
  23. // 请求成功:可以做全局的 toast 展示,或者对 response 做一些格式化
  24. if (!response.data.status !== 1) {
  25. alert('请求失败');
  26. }
  27. return response;
  28. },
  29. onError: (error) => {
  30. // 请求出错:服务端返回错误状态码
  31. console.log(error.response.data);
  32. console.log(error.response.status);
  33. console.log(error.response.headers);
  34. return Promise.reject(error);
  35. },
  36. },
  37. },
  38. },
  39. };
  40. runApp(appConfig);

多个请求配置

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

  1. import { runApp } from 'ice';
  2. const appConfig = {
  3. request: [
  4. {
  5. baseURL: '/api',
  6. // ...RequestConfig 其他参数
  7. },
  8. {
  9. // 配置 request 实例名称,如果不配默认使用内置的 request 实例
  10. instanceName: 'request2'
  11. baseURL: '/api2',
  12. // ...RequestConfig 其他参数
  13. }
  14. ]
  15. };
  16. runApp(appConfig);

使用示例:

  1. import { request } from 'ice';
  2. export default {
  3. // 使用默认的请求方法,即调用 /api/user 接口
  4. async getUser() {
  5. return await request({
  6. url: '/user',
  7. });
  8. },
  9. // 使用自定义的 request 请求方法,即调用接口 /api2/user
  10. async getRepo(id) {
  11. return await request({
  12. instanceName: 'request2',
  13. url: `/repo/${id}`,
  14. });
  15. },
  16. };

异常处理

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

  1. {
  2. // 服务端返回错误状态码时则存在该字段
  3. response: {
  4. data: {},
  5. status: {},
  6. headers: {}
  7. },
  8. // 服务端未返回结构时则存在该字段
  9. request: XMLHttpRequest,
  10. // 一定存在,即 RequestConfig
  11. config: {
  12. },
  13. // 一定存在
  14. message: ''
  15. }

高阶用法

Mock 接口

项目开发初期,后端接口可能还没开发好或不够稳定,此时前端可以通过 Mock 的方式来模拟接口,参考文档 本地 Mock 能力

使用真实的后端接口调试前端代码

当项目开发到一定时间段时,我们需要联调后端接口,此时可能会遇到各种跨域问题,参考文档 本地 Proxy 能力

如何解决接口跨域问题

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

根据环境配置不同的 baseURL

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

  1. const appConfig = {
  2. request: {
  3. baseURL: '//service.example.com/api',
  4. },
  5. };

结合环境配置即可实现不同环境使用不同的 baseURL:

  1. // src/config.ts
  2. export default {
  3. local: {
  4. baseURL: `http://localhost:${process.env.SERVER_PORT}/api`,
  5. },
  6. daily: {
  7. baseURL: 'https://daily.example.com/api',
  8. },
  9. prod: {
  10. baseURL: 'https://example.com/api',
  11. },
  12. };

src/app.ts 中配置 request.baseURL:

  1. import { runApp, config } from 'ice';
  2. const appConfig = {
  3. request: {
  4. baseURL: config.baseURL,
  5. },
  6. };
  7. runApp(appConfig);

版本升级

内置的 axios 从 0.19.x 升级到 0.21.x

icejs 2.0 中升级