前后端通信

前后端通信通常使用 AJAX 方案,对于 AJAX 社区有非常多的封装,目前主流推荐 axios

使用 axios 进行通信

安装依赖:

  1. $ npm install axios --save

通常情况下,AJAX 请求都是异步的,因此 axios 默认返回一个 Promise,因此你可以通过 Promise 或者 async/await 的方式调用:

  1. import axios from 'axios';
  2. // async/await 方式使用
  3. async function getUser() {
  4. try {
  5. const response = await axios.get('/user', {
  6. // request query
  7. params: {
  8. id: 1
  9. }
  10. });
  11. console.log(response);
  12. } catch (error) {
  13. console.error(error);
  14. }
  15. }
  16. // Promise 方式调用
  17. axios.get('/user')
  18. .then(function (response) {
  19. console.log(response);
  20. })
  21. .catch(function (error) {
  22. console.log(error);
  23. });
  24. // 发送 POST 请求
  25. axios({
  26. method: 'post',
  27. url: '/user',
  28. // request query
  29. params: { foo: 'bar' },
  30. // request body
  31. data: {
  32. firstName: 'Fred',
  33. lastName: 'Flintstone'
  34. }
  35. });

在这些基础功能上,axios 支持对请求进行自定义配置,如请求参数、异常状态码判断、全局处理异常、全局配置请求参数等,具体参见 axios 文档

业务里通常会有请求成功或失败的通用逻辑,建议参考下文为业务封装统一的请求方法。

在 React 组件中请求并渲染数据

请求异步数据并渲染,往往需要在视图上区分不同的视图,比如加载中、接口出错、渲染数据,此处以 Function Component + Hooks 为例:

  1. import React, { useState } from 'react';
  2. function CustomComponent {
  3. const [data, setData] = useState([]);
  4. const [loading, setLoading] = useState(false);
  5. const [error, setError] = useState(null);
  6. useEffect(() => {
  7. const fetchData = async () => {
  8. setLoading(true);
  9. try {
  10. const result = await axios('/list');
  11. setData(result.data);
  12. } catch (err) {
  13. setError(err);
  14. }
  15. setLoading(false);
  16. };
  17. fetchData();
  18. }, []);
  19. return (
  20. <>
  21. {error && <div>{error.message}</div>}
  22. {
  23. loading ? <div>loading...</div> : (
  24. (data || []).map((item, idx) => {
  25. return <div key={idx}>{item.name}</div>;
  26. })
  27. )
  28. }
  29. </>
  30. );
  31. }

简化请求状态

通过上面的例子,会发现每个请求都包含请求成功、加载中、请求异常三个状态,如果每个请求都这样处理就会非常繁琐,因此接下来介绍如何通过封装让业务层无需关心请求过程中的这么多状态。对于 React 16.8.0 以下不支持 Hooks 的项目建议使用组件 DataBinder

在业务代码中封装 request 以及 useRequest 的通用方法:

  1. // src/utils/request.js
  2. import { useReducer } from 'react';
  3. import axios from 'axios';
  4. import { Message } from '@alifd/next';
  5. // Set baseURL when debugging production url in dev mode
  6. // axios.defaults.baseURL = '//xxxx.taobao.com';
  7. /**
  8. * Method to make ajax request
  9. *
  10. * @param {object} options - axios config (https://github.com/axios/axios#request-config)
  11. */
  12. export async function request(options) {
  13. try {
  14. const response = await axios(options);
  15. const { data, error } = handleResponse(response);
  16. if (error) {
  17. throw error;
  18. } else {
  19. return { response, data };
  20. }
  21. } catch (error) {
  22. console.error(error);
  23. throw error;
  24. }
  25. }
  26. /**
  27. * Hooks to make ajax request
  28. *
  29. * @param {object} options - axios config (https://github.com/axios/axios#request-config)
  30. * @return {object}
  31. * @param {object} response - response of axios (https://github.com/axios/axios#response-schema)
  32. * @param {object} error - HTTP or use defined error
  33. * @param {boolean} loading - loading status of the request
  34. * @param {function} request - function to make the request manually
  35. */
  36. export function useRequest(options) {
  37. const initialState = {
  38. response: null,
  39. loading: false,
  40. error: null,
  41. };
  42. const [state, dispatch] = useReducer(requestReducer, initialState);
  43. /**
  44. * Method to make request manually
  45. * @param {object} config - axios config to shallow merged with options before making request
  46. */
  47. async function request(config) {
  48. try {
  49. dispatch({
  50. type: 'init',
  51. });
  52. const response = await axios({
  53. ...options,
  54. ...config,
  55. });
  56. const { data, error } = handleResponse(response);
  57. if (error) {
  58. throw error;
  59. } else {
  60. dispatch({
  61. type: 'success',
  62. response,
  63. });
  64. return { response, data };
  65. }
  66. } catch (error) {
  67. dispatch({
  68. type: 'error',
  69. error,
  70. });
  71. throw error;
  72. }
  73. }
  74. return {
  75. ...state,
  76. request,
  77. };
  78. }
  79. /**
  80. * Reducer to handle the status of the request
  81. * @param {object} state - original status
  82. * @param {object} action - action of dispatch
  83. * @return {object} new status
  84. */
  85. function requestReducer(state, action) {
  86. switch (action.type) {
  87. case 'init':
  88. return {
  89. response: null,
  90. error: null,
  91. loading: true,
  92. };
  93. case 'success':
  94. return {
  95. response: action.response,
  96. error: null,
  97. loading: false,
  98. };
  99. case 'error':
  100. return {
  101. response: null,
  102. error: action.error,
  103. loading: false,
  104. };
  105. default:
  106. return {
  107. response: null,
  108. error: null,
  109. loading: false,
  110. };
  111. }
  112. }
  113. /**
  114. * Custom response data handler logic
  115. *
  116. * @param {object} response - response data returned by request
  117. * @return {object} data or error according to status code
  118. */
  119. function handleResponse(response) {
  120. const { data } = response;
  121. // Please modify the status key according to your business logic
  122. // normally the key is `status` or `code`
  123. if (data.status === 'SUCCESS') {
  124. return { data };
  125. } else if (data.status === 'NOT_LOGIN') {
  126. location.href = '';
  127. } else {
  128. const error = new Error(data.message || '后端接口异常');
  129. return { error };
  130. }
  131. }

单独使用 request 方法:

  1. import { request } from '@/utils/request';
  2. async function test() {
  3. try {
  4. const { response, data } = await request({
  5. url: '/api/list',
  6. });
  7. console.log('success', data);
  8. } catch(err) {
  9. // request 方法已处理异常,通常这里不需要做特殊处理
  10. console.error(err);
  11. }
  12. }

在组件中使用 useRequest 请求数据并渲染:

  1. import { useRequest } from '@/utils/request';
  2. function ListView(props) {
  3. const { loading, error, response, request } = useRequest({
  4. url: '/api/list',
  5. method: 'GET',
  6. });
  7. const dataSource = response ? response.data.dataSource : [];
  8. useEffect(() => {
  9. request();
  10. }, []);
  11. return (
  12. <>
  13. {error && <div>{error.message}</div>}
  14. {loading ? (
  15. <div>loading....</div>
  16. ) : (
  17. data.map(item => {
  18. return <div>{item.name}</div>;
  19. })
  20. )}
  21. </>
  22. );
  23. }

跨域问题

因为浏览器的同源策略,前端经常要面临跨域问题,同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指协议、域名、端口三者相同,因此如果当前页面与发起 AJAX 请求的地址中协议、域名、端口有一个不一致,则会出现跨域问题,跨域问题最明显的现象是 AJAX 接口无法请求成功

应对跨域问题有非常多的方案,当下主流以及推荐的方案是 CORS(Cross-origin resource sharing),CORS 是一个 W3C 标准,全称是跨域资源共享。它允许浏览器向跨源服务器发起 MLHttpRequest 请求,从而克服了同源策略的限制。CORS 需要服务端配置一些头信息,这方面谷歌上有非常多的内容可以参考,这里不再详细描述,具体可参考 跨域资源共享 CORS 详解