前顏(yan)
在前端項(xiàng)目的開發(fā)過程中泼掠,往往后端會(huì)給到一份數(shù)據(jù)接口(本文簡稱api),為了減少后期的維護(hù)以及出錯(cuò)成本垦细,我的考慮是希望能夠找到這么一種方法择镇,可以將所有的api以某種方式統(tǒng)一的管理起來,并且很方便的進(jìn)行維護(hù)括改,比如當(dāng)后端修改了api名腻豌,我可以很快的定位到該api進(jìn)行修改,或者當(dāng)后端添加了新的api嘱能,我可以很快的知道具體是一個(gè)api寫漏了吝梅。
于是,我有了構(gòu)建Api Tree的想法焰檩。
一憔涉、前后端分離(Resful api)
在前后端分離的開發(fā)模式中订框,前后端的交互點(diǎn)主要在于各個(gè)數(shù)據(jù)接口析苫,也就是說后端把每個(gè)功能封裝成了api,供前端調(diào)用。
舉個(gè)例子衩侥,假設(shè)后端提供了關(guān)于user的以下3個(gè)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 }
對應(yīng)的api描述如下(為了方便理解国旷,這里只考慮get請求):
1 獲取用戶id的用戶數(shù)據(jù)
2 獲取用戶名為name的用戶信息
3 獲取年齡為age的用戶列表
二、在Component中調(diào)用api接口獲取數(shù)據(jù)
目前各大前端框架比如angular茫死、vue以及react等跪但,都有提供相關(guān)HttpClient,用來發(fā)起http請求峦萎,比如get屡久、post、put爱榔、delete等被环,由于本人比較熟悉angular,下面代碼以angular進(jìn)行舉例(其他框架做法類似)详幽,代碼統(tǒng)一使用typescript語法筛欢。
在app.component.ts中調(diào)用api:
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();
}
}
三、封裝UserHttpService
在項(xiàng)目中唇聘,由于多個(gè)頁面可能需要調(diào)用同一個(gè)api版姑,為了減少代碼的冗余以及方便維護(hù),比較好的方式是將所有的api封裝到一個(gè)Service中迟郎,然后將這個(gè)Service實(shí)例化成單例模式剥险,為所有的頁面提供http服務(wù)。
angular提供了依賴注入的功能宪肖,可以將Service注入到Module中炒嘲,并且在Module中的各個(gè)Component共享同一個(gè)Service,因此不需要手動(dòng)去實(shí)現(xiàn)Service的單例模式匈庭。
代碼如下:
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);
}
}
這樣的好處在于:
1夫凸、團(tuán)隊(duì)合作:
可以將前端項(xiàng)目分為HttpService層和Component層,由不同的人進(jìn)行分開維護(hù)
2阱持、減少代碼的冗余:
在多個(gè)Component中調(diào)用同一個(gè)api時(shí)夭拌,不需要寫多份代碼
3、降低維護(hù)和擴(kuò)展成本:
當(dāng)后端增加或修改接口時(shí)衷咽,由于所有的user api都在UserHttpService里鸽扁,所以能夠很容易的進(jìn)行接口調(diào)整,并且不影響Component層的代碼
但以上方案還存在一個(gè)缺點(diǎn)镶骗,即url使用字符串拼接的形式:
const url = `${HOST_URL}/user/getByName/${name}`;
這樣容易出現(xiàn)以下問題:
1桶现、接口名拼接出錯(cuò),并且由于是字符串拼接鼎姊,不會(huì)有語法提示(ts)
2骡和、沒有一份完整的映射后端的api表相赁,出現(xiàn)問題時(shí),不容易排查
因此慰于,接下來進(jìn)入本文的主題:構(gòu)建Api Tree钮科。
四、手動(dòng)構(gòu)建Api Tree
什么是Api Tree呢婆赠,我把它定義為將所有的api以節(jié)點(diǎn)的形式掛在一個(gè)樹上绵脯,最后形成了一棵包含所有api的樹形結(jié)構(gòu)。
對api tree的構(gòu)建初步想法(手動(dòng)構(gòu)建)如下:
/**
* 手動(dòng)構(gòu)建 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樹上摘取各個(gè)api節(jié)點(diǎn)的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;
但是以上構(gòu)建api tree的方式存在兩個(gè)缺點(diǎn):
1、需要在各個(gè)節(jié)點(diǎn)手動(dòng)拼接全路徑
2妙黍、只能摘取子節(jié)點(diǎn)的url:getByName和getByAge
無法摘取父節(jié)點(diǎn)的url璃吧,比如我想獲取https://www.xxx.com/api/v1/user
,無法通過APITREE.domain1.api.v1.user
獲取
const APITREE = {
domain1: {
api: {
v1: {
// user為父節(jié)點(diǎn)
// 缺點(diǎn)一:無法通過APITREE.domain1.api.v1.user獲取
// https://www.xxx.com/api/v1/user
user: {
// 缺點(diǎn)二:在getByName和getByAge節(jié)點(diǎn)中手動(dòng)寫入全路徑拼接
getByName: 'https://www.xxx.com/api/v1/user/getByName',
getByAge: 'https://www.xxx.com/api/v1/user/getByAge'
}
}
}
}
};
五废境、Api Tree生成器(ApiTreeGenerator)
針對手動(dòng)構(gòu)建Api Tree的問題畜挨,我引入了兩個(gè)概念:apiTreeConfig(基本配置)和apiTreeGenerator(生成器)。
通過apiTreeGenerator對apiTreeConfig進(jìn)行處理噩凹,最終生成真正的apiTree巴元。
1、apiTreeConfig我把它稱之為基本配置驮宴,apiTreeConfig具有一定的配置規(guī)則逮刨,要求每個(gè)節(jié)點(diǎn)名(除了域名)必須與api url中的每一節(jié)點(diǎn)名一致,因?yàn)閍piTreeGenerator是根據(jù)apiTreeConfig的各個(gè)節(jié)點(diǎn)名進(jìn)行生成堵泽,
api tree config配置如下:
/**
* api tree config
* _this可以省略不寫修己,但是不寫的話,在ts就沒有語法提示
* 子節(jié)點(diǎn)getByName,getByAge以及_this可以為任意值迎罗,因?yàn)閷?huì)被apiTreeGenerator重新賦值
*/
const APITREECONFIG = {
api: {
v1: {
user: {
getByName: '',
getByAge: '',
_this: ''
}
},
_this: ''
}
};
export { APITREECONFIG };
2睬愤、apiTreeGenerator我把它稱之為生成器,具有如下功能:
1) 遍歷apiTreeConfig纹安,處理apiTreeConfig的所有子節(jié)點(diǎn)尤辱,并根據(jù)該節(jié)點(diǎn)的所有父節(jié)點(diǎn)鏈生成完整的url,并且作為該節(jié)點(diǎn)的value厢岂,比如:
APITREECONFIG.api.v1.user.getByName
-> https://www.xxx.com/api/v1/user/getByName
2) 遍歷apiTreeConfig光督,處理apiTreeConfig的所有父節(jié)點(diǎn),在每個(gè)父節(jié)點(diǎn)中添加_this子節(jié)點(diǎn)指向父節(jié)點(diǎn)的完整url塔粒。
apiTreeGenerator(生成器)的代碼如下:
(由于項(xiàng)目中只用到一個(gè)后端的數(shù)據(jù)结借,這里只實(shí)現(xiàn)了單域名的apiTreeGenerator,關(guān)于多域名的apiTreeGenerator卒茬,大家可以自行修改實(shí)現(xiàn)船老。)
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;
};
/**
* 根據(jù)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]);
}
}
// 創(chuàng)建_this節(jié)點(diǎn) (這里需要放在上面的for之后)
apiTreeConfig['_this'] = parentApiNodeChain
? addHost(`${parentApiNodeChain}`)
: addHost('');
};
apiTreeGenerator(APITREECONFIG);
export { APITREE };
結(jié)果:
優(yōu)化后的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();
}
}
六咖熟、總結(jié)
通過api tree,能帶來如下好處:
1努隙、能夠通過樹的形式來獲取api,關(guān)鍵是有語法提示
APITREE.api.v1.user.getByName
2辜昵、apiTreeConfig配置文件與后端的api接口一 一對應(yīng)荸镊,方便維護(hù)
3、當(dāng)后端修改api名時(shí)堪置,apiTreeConfig可以很方便的進(jìn)行調(diào)整
七躬存、demo
https://github.com/SimpleCodeCX/myCode/tree/master/angular/api-tree