Front face

In the front in the development process of the project, the back-end tend to give to a data interface (in this paper, referred to as “API”), in order to reduce the cost of maintenance and error late, my thinking is hoping to find such a method, all of the API in a certain way can be unified management, and convenient maintenance, such as when the backend to modify the API name, I can quickly locate the API to make changes, or when a new API is added to the back end, I can quickly know that an API is missing.

So I came up with the idea of building an Api Tree.

Separation of front and back ends (Resful API)

In the development mode of the front and back end separation, the interaction point of the front and back end is mainly the data interface, that is, the back end encapsulates each function into an API for the front end to call.

As an example, suppose the backend provides the following three apis for user:

1 http(s)://www.xxx.com/api/v1/user/{ id }
2 http(s)://www.xxx.com/api/v1/user/getByName/{ name }
3 http(s)://www.xxx.com/api/v1/user/getByAge/{ age }
Copy the code

The corresponding API is described as follows (only GET requests are considered here for ease of understanding) :

 1Get user data for the user ID2Get user information of user name3Gets a list of users whose age is ageCopy the code

Call the API in Component to get the data

Angular, Vue, react, etc., all provide HttpClient, which is used to initiate HTTP requests, such as GET, POST, PUT, delete, etc. The following code uses the Angular example (similar to other frameworks) and uses typescript syntax.

Call API in app.component.ts:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root'.templateUrl: './app.component.html'.styleUrls: ['./app.component.scss']})export class AppComponent {

  userInfo;

  constructor(private http: HttpClient) {
    this.getUserById(1);
  }

  async getUserById(userId) {
    const url = `https://www.xxx.com/api/v1/user/${userId}`;
    this.userInfo = await this.http.get(url).toPromise(); }}Copy the code

Encapsulate UserHttpService

In a project, because multiple pages may need to invoke the same API, to reduce code redundancy and facilitate maintenance, it is better to encapsulate all apis into a Service, and then instantiate this Service into a singleton pattern to provide HTTP services for all pages.

Angular provides dependency injection to inject services into the Module, and the components in the Module share the same Service, so you don’t need to implement the singleton pattern of services manually.

The code is as follows:

user.http.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const HOST_URL = `https://www.xxx.com/api/v1`;

@Injectable()
export class UserHttpService {

  constructor(private http: HttpClient) { }

  async getUserById(userId) {
    const url = `${HOST_URL}/user/${userId}`;
    return this.http.get(url).toPromise();
  }

  async getUserByName(name) {
    const url = `${HOST_URL}/user/getByName/${name}`;
    return this.http.get(url).toPromise();
  }

  async getUserByAge(age) {
    const url = `${HOST_URL}/user/getByAge/${age}`;
    return this.http.get(url).toPromise(); }}Copy the code

app.component.ts

import { Component } from '@angular/core';
import { UserHttpService } from './user.http.service';
@Component({
  selector: 'app-root'.templateUrl: './app.component.html'.styleUrls: ['./app.component.scss']})export class AppComponent {

  constructor(private userHttpService: UserHttpService) {
    this.getUserById(1);
  }

  async getUserById(userId) {
    const userInfo = await this.userHttpService.getUserById(userId);
    console.log(userInfo);
  }

  async getUserByName(name) {
    const userInfo = await this.userHttpService.getUserByName(name);
    console.log(userInfo);
  }

  async getUserByAge(age) {
    const userInfoList = await this.userHttpService.getUserByAge(age);
    console.log(userInfoList); }}Copy the code

The benefits of this are:

1. Teamwork:

Front-end projects can be divided into an HttpService layer and a Component layer, maintained separately by different people

2. Reduce code redundancy:

There is no need to write multiple copies of code when calling the same API in multiple Components

3, reduce maintenance and expansion costs:

When interfaces are added or modified on the back end, since all user apis are in UserHttpService, interface adjustments can be made easily without affecting the Component layer code

However, the above scheme still has a disadvantage, that is, the URL uses the form of string concatenation:

const url = `${HOST_URL}/user/getByName/${name}`;
Copy the code

This is prone to the following problems:

Error in interface name concatenation, and no syntax prompt because of string concatenation (TS)

2, there is no complete mapping back-end API table, when there is a problem, it is not easy to troubleshoot, therefore, the next topic of this article: building API Tree.

4. Build Api Tree manually

What is an Api Tree? I define it as hanging all the apis in the form of nodes on a Tree, resulting in a Tree structure containing all the apis.

Initial ideas for building API Tree (manual build) are as follows:

/** * Build API tree */
const APITREE = {
  domain1: {
    api: {
      v1: {
        user: {
          getByName: 'https://www.xxx.com/api/v1/user/getByName'.getByAge: 'https://www.xxx.com/api/v1/user/getByAge'
        },
        animal: {
          getByType: 'https://www.xxx.com/api/v1/animal/getByType'.getByAge: 'https://www.xxx.com/api/v1/animal/getByAge'}}}},domain2: {
    api: {
      car: {
        api1: 'https://xxx.xxx.cn/api/car/api1'.api2: 'https://xxx.xxx.cn/api/car/api2'}}},domain3: {}};export { APITREE };
Copy the code

With API tree, we can extract the URL of each API node from the API tree as follows:

/ / get url:https://www.xxx.com/api/v1/user/getByName
const getByNameUrl = APITREE.domain1.api.v1.user.getByName;

/ / get the url: https://xxx.xxx.cn/api/car/api1
const carApi1Url = APITREE.domain2.api.car.api1;
Copy the code

However, there are two drawbacks to the above approach to building an API tree:

1. You need to manually join a full path on each node

2, can gather the child nodes of url: getByName and getByAge, unable to gather the parent node of the url, for example, I want to get https://www.xxx.com/api/v1/user, through APITREE. Domain1. API. V1. The user access

const APITREE = {
  domain1: {
    api: {
      v1: {
        // User is the parent node
        . / / a disadvantage: through APITREE domain1. API. V1. User access
        // https://www.xxx.com/api/v1/user
        user: {
          // Disadvantage 2: Write full path concatenation manually in getByName and getByAge
          getByName: 'https://www.xxx.com/api/v1/user/getByName'.getByAge: 'https://www.xxx.com/api/v1/user/getByAge'}}}}};Copy the code

5. ApiTreeGenerator

For the problem of manually building an Api Tree, I introduced two concepts: apiTreeConfig (basic configuration) and apiTreeGenerator (generator).

The apiTreeConfig is processed by the apiTreeGenerator to generate the real apiTree.

ApiTreeConfig is what I call the basic configuration. ApiTreeConfig has certain configuration rules that require every node name (except domain name) to be the same as every node name in the API URL. Since apiTreeGenerator is generated from the apiTreeConfig node names, the API Tree config configuration is as follows:

/** * API tree config * _this can be omitted, but if not, there is no syntax in TS. The child nodes getByName,getByAge, and _this can be arbitrary as they will be reassigned by apiTreeGenerator */
const APITREECONFIG = {
  api: {
    v1: {
      user: {
        getByName: ' '.getByAge: ' '._this: ' '}},_this: ' '}};export { APITREECONFIG };

Copy the code

2. ApiTreeGenerator, as I call it, has the following functions:

1) Iterate through apiTreeConfig, process all child nodes of apiTreeConfig, and generate complete urls according to all parent node chains of the node, and serve as the value of the node, for example: APITREECONFIG.api.v1.user.getByName -> https://www.xxx.com/api/v1/user/getByName

2) Iterate through apiTreeConfig, process all parent nodes of apiTreeConfig, and add _this child node to each parent node to point to the full URL of the parent node.

The code for apiTreeGenerator is as follows:

(Since only one back-end data is used in the project, only single-domain apiTreeGenerator is implemented here. You can modify the implementation of multi-domain apiTreeGenerator by yourself.)

import { APITREECONFIG } from './api-tree.config';

const APITREE = APITREECONFIG;
const HOST_URL = `https://www.xxx.com`;

/** * Add the HOST_URL prefix */ to the API node chain

const addHost = (apiNodeChain: string) = > {
  return apiNodeChain ? `${HOST_URL}/${apiNodeChain.replace(/ ^ / / /.' ')}` : HOST_URL;
};

/** * Generate API tree config:  * @param apiTreeConfig api tree config * @param parentApiNodeChain parentApiNode1/parentApiNode2/parentApiNode3 */
const apiTreeGenerator = (apiTreeConfig: string | object, parentApiNodeChain? : string) = > {
  for (const key of Object.keys(apiTreeConfig)) {
    const apiNode = key;
    const prefixChain = parentApiNodeChain ? `${parentApiNodeChain}/ ` : ' ';
    if (Object.prototype.toString.call(apiTreeConfig[key]) === '[object Object]') {
      apiTreeGenerator(apiTreeConfig[key], prefixChain + apiNode);
    } else{ apiTreeConfig[key] = parentApiNodeChain ? addHost(prefixChain + apiTreeConfig[key]) : addHost(apiTreeConfig[key]); }}// Create the _this node (this needs to be placed after the for above)
  apiTreeConfig['_this'] = parentApiNodeChain
    ? addHost(`${parentApiNodeChain}`)
    : addHost(' ');
};

apiTreeGenerator(APITREECONFIG);

export { APITREE };

Copy the code

Results:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { APITREE } from './api-tree';

@Injectable()
export class UserHttpService {

  constructor(private http: HttpClient) { }

  async getUserById(userId) {
    const url = APITREE.api.v1.user._this + '/' + userId;
    return this.http.get(url).toPromise();
  }

  async getUserByName(name) {
    const url = APITREE.api.v1.user.getByName + '/' + name;
    return this.http.get(url).toPromise();
  }

  async getUserByAge(age) {
    const url = APITREE.api.v1.user.getByAge + '/' + age;
    return this.http.get(url).toPromise(); }}Copy the code

Six, summarized

The API Tree provides the following benefits:

1, can be in the form of a tree to get the API, the key is to have a grammar tip APITREE. API. V1. User. GetByName

2. The apiTreeConfig configuration file corresponds to the BACKEND API interface 1-1 for easy maintenance

3. ApiTreeConfig can be easily adjusted when the backend API name is changed

Seven, the demo

Github code: github.com/SimpleCodeC…