前顏(yan)
在前端專案的開發過程中,往往後端會給到一份資料介面(本文簡稱api),為了減少後期的維護以及出錯成本,我的考慮是希望能夠找到這麼一種方法,可以將所有的api以某種方式統一的管理起來,並且很方便的進行維護,比如當後端修改了api名,我可以很快的定位到該api進行修改,或者當後端新增了新的api,我可以很快的知道具體是一個api寫漏了。
於是,我有了構建Api Tree的想法。
一、前後端分離(Resful api)
在前後端分離的開發模式中,前後端的互動點主要在於各個資料介面,也就是說後端把每個功能封裝成了api,供前端呼叫。
舉個例子,假設後端提供了關於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請求):
1 獲取使用者id的使用者資料
2 獲取使用者名稱為name的使用者資訊
3 獲取年齡為age的使用者列表
複製程式碼
二、在Component中呼叫api介面獲取資料
目前各大前端框架比如angular、vue以及react等,都有提供相關HttpClient,用來發起http請求,比如get、post、put、delete等,由於本人比較熟悉angular,下面程式碼以angular進行舉例(其他框架做法類似),程式碼統一使用typescript語法。
在app.component.ts中呼叫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
在專案中,由於多個頁面可能需要呼叫同一個api,為了減少程式碼的冗餘以及方便維護,比較好的方式是將所有的api封裝到一個Service中,然後將這個Service例項化成單例模式,為所有的頁面提供http服務。
angular提供了依賴注入的功能,可以將Service注入到Module中,並且在Module中的各個Component共享同一個Service,因此不需要手動去實現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、團隊合作:
可以將前端專案分為HttpService層和Component層,由不同的人進行分開維護
2、減少程式碼的冗餘:
在多個Component中呼叫同一個api時,不需要寫多份程式碼
3、降低維護和擴充套件成本:
當後端增加或修改介面時,由於所有的user api都在UserHttpService裡,所以能夠很容易的進行介面調整,並且不影響Component層的程式碼
但以上方案還存在一個缺點,即url使用字串拼接的形式:
const url = `${HOST_URL}/user/getByName/${name}`;
複製程式碼
這樣容易出現以下問題:
1、介面名拼接出錯,並且由於是字串拼接,不會有語法提示(ts)
2、沒有一份完整的對映後端的api表,出現問題時,不容易排查 因此,接下來進入本文的主題:構建Api Tree。
四、手動構建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的方式存在兩個缺點:
1、需要在各個節點手動拼接全路徑
2、只能摘取子節點的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'
}
}
}
}
};
複製程式碼
五、Api Tree生成器(ApiTreeGenerator)
針對手動構建Api Tree的問題,我引入了兩個概念:apiTreeConfig(基本配置)和apiTreeGenerator(生成器)。
通過apiTreeGenerator對apiTreeConfig進行處理,最終生成真正的apiTree。
1、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 };
複製程式碼
2、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.tsimport { 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();
}
}
複製程式碼
六、總結
通過api tree,能帶來如下好處:
1、能夠通過樹的形式來獲取api,關鍵是有語法提示
APITREE.api.v1.user.getByName
2、apiTreeConfig配置檔案與後端的api介面一 一對應,方便維護
3、當後端修改api名時,apiTreeConfig可以很方便的進行調整
七、demo
github程式碼: github.com/SimpleCodeC…