Data request is a very important part of our development. How to abstract it elegantly is not an easy thing, and it is often ignored. If it is not handled properly, duplicate code will be scattered everywhere and maintenance cost is very high.

Therefore, we need to sort out what aspects are involved in the data request and have an overall control over it, so as to design a scheme with high scalability.

Case analysis

Let’s start with axios, the request library.

Suppose we make a POST request on the page, something like this:

axios.post('/user/create', { name: 'beyondxgb' }).then((result) = > {
  // do something
});
Copy the code

Headers = x-xSRF-token = x-xSRF-token = x-xSRF-token = headers = x-xSRF-token

axios.post('/user/create', { name: 'beyondxgb' }, {
  headers: {
    'X-XSRF-TOKEN': 'xxxxxxxx',
  },
}).then((result) = > {
  // do something
});
Copy the code

Do you need this configuration every time you make a POST request? So it’s tempting to pull this configuration out and abstract out a method like this:

function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',},... config, }); }Copy the code

So we need to abstract the parameter configuration.

What if, when it comes time to test the process, the server does not always return a successful request? Then catch it:

post('/user/create', { name: 'beyondxgb' }).then((result) = > {
  // do something
}).catch((error) = > {
  // deal with error
  / / 200
  / / 503
  // SESSION EXPIRED
  // ...
});
Copy the code

Write down always feel where is wrong ah, the original request error has so many cases, my whole project has a lot of request data place, this part of the code must be universal, abstract out!

function dealWithRequestError(error) {
  // deal with error
  / / 200
  / / 503
  // SESSION EXPIRED
  // ...
}
function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',},... config, }).catch(dealWithRequestError); }Copy the code

So we need to abstract exception handling.

Before the project goes online, the business side may put forward the requirement of stability. At this time, we need to monitor the request and record the success and failure of the interface request. Again, we want to write this part of the code in a public place, like this:

function post(url, data, config) {
  return axios.post(url, data, {
    headers: {
      'X-XSRF-TOKEN': 'xxxxxxxx',},... config, }).then((result) = > {
    // Record success. return result; }) .catch((error) = > {
    // Record the failure. return dealWithRequestError(error); ) ; }Copy the code

So we need to abstract request monitoring.

The project design

From the case analysis of a simple POST request above, we can see that data requests mainly involve three aspects: parameter configuration, exception handling, and request monitoring. The above example is still a rough one, and the code as a whole needs to be organized and layered.

Parameter configuration

First, let’s deal with the configuration of parameters. The above example only analyzes THE POST request. In fact, for other requests such as GET and PUT, we can handle these requests uniformly.

request.js

import axios from 'axios';

// The http header that carries the xsrf token value { X-XSRF-TOKEN: '' }
const csrfConfig = {
  'X-XSRF-TOKEN': ' '};// Build uniform request
async function buildRequest(method, url, params, options) {
  let param = {};
  let config = {};
  if (method === 'get') { param = { params, ... options }; }else {
    param = JSON.stringify(params);
    config = {
      headers: {
        ...csrfConfig,
      },
    };
    config = Object.assign({}, config, options);
  }
  return axios[method](url, param, config);
}

export const get = (url, params = {}, options) = > buildRequest('get', url, params, options);
export const post = (url, params = {}, options) = > buildRequest('post', url, params, options);
Copy the code

In this case, we expose the get and POST methods, similar to other requests, using only GET and POST as examples, with API addresses, data, and extension configurations, respectively.

Exception handling

In fact, the exception processing scenario will be complicated, not simply catch, often accompanied by the interaction of business logic and UI, exceptions are mainly divided into two aspects, global exceptions and business exceptions.

Global exceptions can also be said to be common exceptions, such as 503 returned by the server, network exception, login failure, no permission, etc. These exceptions can be expected and controllable, as long as the format is agreed with the server, and the exception can be captured and displayed.

Business exceptions refer to those closely related to business logic, such as submission failure and data verification failure, etc. These exceptions often have different situations in each interface and need to be personalized to display errors. Therefore, this part may not be handled uniformly, and sometimes the display errors need to be handed over to the View layer to achieve.

In implementation, we don’t catch directly in the above request method, but use the Axios provided interceptors function, which can isolate exception handling from the core request method, after all, this part of the interaction with the UI. Let’s see how:

error.js

import axios from 'axios';

// Add a response interceptor
axios.interceptors.response.use((response) = > {
  const { config, data } = response;
  // The Code specified with the server
  const { code } = data;
  switch (code) {
    case 200:
      return data;
    case 401:
      // The login is invalid
      break;
    case 403:
      / / without permission
      break;
    default:
      break;
  }
  if (config.showError) {
    // The interface configuration specifies the need to personalize the display of errors
    return Promise.reject(data);
  }
  // Display error by default
  // ... Toast error
}, (error) => {
  // Common error
  if (axios.isCancel(error)) {
    // Request cancel
  } else if(navigator && ! navigator.onLine) {// Network is disconnect
  } else {
    // Other error
  }
  return Promise.reject(error);
});
Copy the code

The Axios interceptors function, in fact, is a chain call, which can do things before and after the request. Here, we intercept after the request, check the returned data and catch exceptions. For common errors, we directly display them through UI interaction. For business errors, we check whether the interface is configured to display errors in a personalized manner. If so, we will hand over error processing to the page. If not, we will carry out error handling.

Request to monitor

The request monitoring section is similar to exception handling, except that it only records the situation and does not involve interaction on the UI or with business code, so you can either write this logic directly in the exception handling area, or add an interceptor after the request and handle it separately.

monitor.js

axios.interceptors.response.use((response) = > {
  const { status, data, config } = response;
  // The request is buried based on the returned data and interface parameter configuration
}, (error) => {
  // The request is buried based on the returned data and interface parameter configuration
});
Copy the code

The comparison suggests doing this to keep each module separate and in line with the single function principle (SRP).

Ok, so far, parameter configuration, exception handling, and request monitoring have been designed, and there are three files:

  • request.js: Request library configuration, external exposureget.postMethods.
  • error.js: Some exception handling of the request, which relates to whether the interface needs to be personalized to display errors.
  • monitor.js: A separate piece of record of the request.

When called on a page, it could look something like this:

import { get, post } from 'request.js';

get('/user/info').then((data) = > {});
post('/user/update', { name: 'beyondxgb' }, { showError: true }).then((data) = > {
  if(data.code ! = =200) {
    // Display error
  } else {
    // do something}});Copy the code

If the API name is changed later, and the API is called in multiple pages, the maintenance cost will be high. We have two methods. The first method is to configure all the APIS in a separate file for the page to read. The second method is to add another layer before the request library and the page, called Service, which is the so-called service layer, to expose interface methods to the page. The page does not need to care about what the interface is or how it fetches data, and any subsequent changes to the interface can be made at the service layer without affecting the page at all.

Of course I took the second approach, something like this:

services.js

import { get, post } from 'request.js';

// fetch random data
export async function fetchRandomData(params) {
  return get('https://randomuser.me/api', params);
}

// update user info
export async function updateUserInfo(params, options) {
  return post('/user/info', params, { showError: true. options }); }Copy the code

In this way, the page does not interact directly with the request library, but with the service layer to retrieve the corresponding methods.

import { fetchRandomData, updateUserInfo } from 'services.js';

fetchRandomData().then((data) = > {});
updateUserInfo({ name: 'beyondxgb' }).then((data) = > {
  if(data.code ! = =200) {
    // Display error
  } else {
    // do something}});Copy the code

Let’s see what the final solution looks like:

extending

All the above mentioned are based on the axios request library as an example, in fact, the idea is interchangeable, change a request library is the same processing method. One of the things I like about Axios is that it’s very well designed. It’s like middleware. Before the request data reaches the page, We can filter, process, verify and monitor the data through write interceptor.

I think any request library can do this, even if the request library has a history of baggage, it can also cover itself. For example, there is the request library ABC, which has a request method, which can be overridden like this:

import abc from 'abc';

function dispatchRequest(options) {
  const reqConfig = Object.assign({}, options);
  return abc.request(reqConfig).then(response= > ({
    response,
    options,
  })).catch(error= > (
    Promise.reject({
      error,
      options,
    })
  ));
}

class Request {
  constructor(config) {
    this.default = config;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager(),
    };
  }
}

Request.prototype.request = function request(config = {}) {
  // Add interceptors
  const chain = [dispatchRequest, undefined];
  let promise = Promise.resolve(options);

  // Add request interceptors
  this.interceptors.request.forEach((interceptor) = > {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // Add response interceptors
  this.interceptors.response.forEach((interceptor) = > {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
Copy the code

More and more

Front we have solved the problem of data request well, and on the other hand, is also closely related and data request, is the data simulation (Mock), in the early stage of the project development server not ready before, we have only oneself to Mock data locally, or many companies already have a better platform to realize the function, I’ll show you how you can implement Mock data by launching a small tool locally, without using a platform.

Here I wrote a little tool of my own, @ris/ Mock, that I could just inject into webpack-dev-server as middleware.

webpack.config.js

const mock = require('@ris/mock');

module.exports = {
  / /...
  devServer: {
    compress: true.port: 9000.after: (app) = > {
      // Start mock datamock(app); }}};Copy the code

Create a mock folder in the root directory of your project, and create a rules.js file in the folder.

module.exports = {
  'GET /api/user': { name: 'beyondxgb' },
  'POST /api/form/create': { success: true },
  'GET /api/cases/list': (req, res) = > { res.end(JSON.stringify([{ id: 1.name: 'demo' }])); },
  'GET /api/user/list': 'user/list.json'.'GET /api/user/create': 'user/create.js'};Copy the code

After the rules are configured, interface requests will be forwarded. The forwarding can be an object, function, or file. For details, refer to the documentation.

conclusion

In the design of data request scheme, it is also confirmed that our “writing code” is “programming”, rather than “programming”, we should be responsible for our own code, how to make their code high maintainability, easy to expand, is the basic quality of excellent engineers.

The above solution has been precipitate in RIS, including the code organization structure and technical implementation, can initialize a Standard application, the previous article “RIS, new Options for Creating React application” briefly mentioned, welcome to experience.