@angular前端項目代碼優化:構建Api Tree

前顏(yan)

在前端項目的開發過程當中,每每後端會給到一份數據接口(本文簡稱api),爲了減小後期的維護以及出錯成本,個人考慮是但願可以找到這麼一種方法,能夠將全部的api以某種方式統一的管理起來,而且很方便的進行維護,好比當後端修改了api名,我能夠很快的定位到該api進行修改,或者當後端添加了新的api,我能夠很快的知道具體是一個api寫漏了。javascript

因而,我有了構建Api Tree的想法。css

1、先後端分離(Resful api)

在先後端分離的開發模式中,先後端的交互點主要在於各個數據接口,也就是說後端把每一個功能封裝成了api,供前端調用。html

舉個例子,假設後端提供了關於user的如下3個api:前端

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 }
複製代碼

對應的api描述以下(爲了方便理解,這裏只考慮get請求):vue

1 獲取用戶id的用戶數據
 2 獲取用戶名爲name的用戶信息    
 3 獲取年齡爲age的用戶列表
複製代碼

2、在Component中調用api接口獲取數據

目前各大前端框架好比angular、vue以及react等,都有提供相關HttpClient,用來發起http請求,好比get、post、put、delete等,因爲本人比較熟悉angular,下面代碼以angular進行舉例(其餘框架作法相似),代碼統一使用typescript語法。java

在app.component.ts中調用api:node

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();
  }

}

複製代碼

3、封裝UserHttpService

在項目中,因爲多個頁面可能須要調用同一個api,爲了減小代碼的冗餘以及方便維護,比較好的方式是將全部的api封裝到一個Service中,而後將這個Service實例化成單例模式,爲全部的頁面提供http服務。react

angular提供了依賴注入的功能,能夠將Service注入到Module中,而且在Module中的各個Component共享同一個Service,所以不須要手動去實現Service的單例模式。git

代碼以下:github

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();
  }

}
複製代碼

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);
  }

}
複製代碼

這樣的好處在於:

一、團隊合做:

能夠將前端項目分爲HttpService層和Component層,由不一樣的人進行分開維護

二、減小代碼的冗餘:

在多個Component中調用同一個api時,不須要寫多份代碼

三、下降維護和擴展成本:

當後端增長或修改接口時,因爲全部的user api都在UserHttpService裏,因此可以很容易的進行接口調整,而且不影響Component層的代碼

但以上方案還存在一個缺點,即url使用字符串拼接的形式:

const url = `${HOST_URL}/user/getByName/${name}`;
複製代碼

這樣容易出現如下問題:

一、接口名拼接出錯,而且因爲是字符串拼接,不會有語法提示(ts)

二、沒有一份完整的映射後端的api表,出現問題時,不容易排查 所以,接下來進入本文的主題:構建Api Tree。

4、手動構建Api Tree

什麼是Api Tree呢,我把它定義爲將全部的api以節點的形式掛在一個樹上,最後造成了一棵包含全部api的樹形結構。

對api tree的構建初步想法(手動構建)以下:

/** * 手動構建 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 };
複製代碼

有了api tree,咱們就能夠採用以下方式來從api樹上摘取各個api節點的url,代碼以下:

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

// 獲取url:https://xxx.xxx.cn/api/car/api1
const carApi1Url = APITREE.domain2.api.car.api1;
複製代碼

可是以上構建api tree的方式存在兩個缺點:

一、須要在各個節點手動拼接全路徑

二、只能摘取子節點的url:getByName和getByAge,沒法摘取父節點的url,好比我想獲取https://www.xxx.com/api/v1/user,沒法經過APITREE.domain1.api.v1.user獲取

const APITREE = {
  domain1: {
    api: {
      v1: {
        // user爲父節點
        // 缺點一:沒法經過APITREE.domain1.api.v1.user獲取
        // https://www.xxx.com/api/v1/user
        user: {
          // 缺點二:在getByName和getByAge節點中手動寫入全路徑拼接
          getByName: 'https://www.xxx.com/api/v1/user/getByName',
          getByAge: 'https://www.xxx.com/api/v1/user/getByAge'
        }
      }
    }
  }
};

複製代碼

5、Api Tree生成器(ApiTreeGenerator)

針對手動構建Api Tree的問題,我引入了兩個概念:apiTreeConfig(基本配置)和apiTreeGenerator(生成器)。

經過apiTreeGenerator對apiTreeConfig進行處理,最終生成真正的apiTree。

一、apiTreeConfig我把它稱之爲基本配置,apiTreeConfig具備必定的配置規則,要求每一個節點名(除了域名)必須與api url中的每一節點名一致,由於apiTreeGenerator是根據apiTreeConfig的各個節點名進行生成, api tree config配置以下:

/** * api tree config * _this能夠省略不寫,可是不寫的話,在ts就沒有語法提示 * 子節點getByName,getByAge以及_this能夠爲任意值,由於將會被apiTreeGenerator從新賦值 */
const APITREECONFIG = {
  api: {
    v1: {
      user: {
        getByName: '',
        getByAge: '',
        _this: ''
      }
    },
    _this: ''
  }
 };

export { APITREECONFIG };

複製代碼

二、apiTreeGenerator我把它稱之爲生成器,具備以下功能:

1) 遍歷apiTreeConfig,處理apiTreeConfig的全部子節點,並根據該節點的全部父節點鏈生成完整的url,而且做爲該節點的value,好比: APITREECONFIG.api.v1.user.getByName -> https://www.xxx.com/api/v1/user/getByName

2) 遍歷apiTreeConfig,處理apiTreeConfig的全部父節點,在每一個父節點中添加_this子節點指向父節點的完整url。

apiTreeGenerator(生成器)的代碼以下:

(因爲項目中只用到一個後端的數據,這裏只實現了單域名的apiTreeGenerator,關於多域名的apiTreeGenerator,你們能夠自行修改實現。)

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

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

/** * 爲api node chain添加HOST_URL前綴 */

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

/** * 根據api tree config 生成 api tree: * @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]);
    }
  }
  // 建立_this節點 (這裏須要放在上面的for以後)
  apiTreeConfig['_this'] = parentApiNodeChain
    ? addHost(`${parentApiNodeChain}`)
    : addHost('');
};

apiTreeGenerator(APITREECONFIG);

export { APITREE };

複製代碼

結果:

優化後的UserHttpService代碼以下: user.http.service.ts

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();
  }

}

複製代碼

6、總結

經過api tree,能帶來以下好處:

一、可以經過樹的形式來獲取api,關鍵是有語法提示 APITREE.api.v1.user.getByName

二、apiTreeConfig配置文件與後端的api接口一 一對應,方便維護

三、當後端修改api名時,apiTreeConfig能夠很方便的進行調整

7、demo

github代碼: github.com/SimpleCodeC…

相關文章
相關標籤/搜索