0 偽代碼示例
0.1 demo結構
-| src
-| editCenter
-| A.js // 獲取渠道列表,獲取媒體素材列表
-| resourceCenter
-| B.js // 獲取媒體素材列表,獲取標簽列表
-| mangeConfig
-| C.js // 獲取渠道列表,獲取標簽列表
-| service
-| api
-| tagApi.js
-| mediaApi.js
-| chanelApi.js
0.2 未抽象出api
服務層的業務層代碼
A.js
……
let ChanelListParams = {
//……
};
let mediaListParams = {
//...
};
// 獲取渠道列表
function getChanelList(){
params = dealParams(ChanelListParams);
trshttpService.httpServer(trshttpService.getWCMROOT(),params,'post').
then(function(data){
dealData(data);
});
}
// 獲取素材列表
function getMediaList(){
params = dealParams(mediaListParams);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'get').
then(function(data){
dealData(data);
});
}
……
B.js
……
let TagListParams = {
//...
};
let mediaListParams = {
//...
};
// 獲取標簽列表
function getTagList(){
params = dealParams(TagListParams);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post').
then(function(data){
dealData(data);
});
}
// 獲取素材列表
function getMediaList(){
params = dealParams(mediaListParams);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'get').
then(function(data){
dealData(data);
});
}
……
C.js
……
let TagListParams = {
//...
};
let ChanelListParams = {
};
// 獲取標簽列表
function getTagList(){
params = dealParams(TagListParams);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post').
then(function(data){
dealData(data);
});
}
// 獲取渠道列表
function getChanelList(){
params = dealParams(ChanelListParams);
trshttpService.httpServer(trshttpService.getWCMROOT(),params,'post').
then(function(data){
dealData(data);
});
}
……
0.3 抽象了api服務層的代碼
A.js
let ChanelListParams = chanelApi.getListParams();
let mediaListParams = mediaApi.getListParams();
// 獲取渠道列表
function getChanelList(){
params = dealParams(ChanelListParams);
chanelApi.getChanelList(params).
then(function(data){
dealData(data);
});
}
// 獲取素材列表
function getMediaList(){
params = dealParams(mediaListParams);
mediaApi.getMediaList(params).
then(function(data){
dealData(data);
});
}
B.js
let TagListParams = tagApi.getListParams();
let mediaListParams = mediaApi.getListParams();
// 獲取標簽列表
function getTagList(){
params = dealParams(TagListParams);
tagApi.getTagList(params).
then(function(data){
dealData(data);
});
}
// 獲取素材列表
function getMediaList(){
params = dealParams(mediaListParams);
mediaApi.getMediaList(params).
then(function(data){
dealData(data);
});
}
C.js
let TagListParams = tagApi.getListParams();
let ChanelListParams = mediaApi.getListParams();
// 獲取標簽列表
function getTagList(){
params = dealParams(TagListParams);
tagApi.getTagList(params).
then(function(data){
dealData(data);
});
}
// 獲取渠道列表
function getChanelList(){
params = dealParams(ChanelListParams);
chanelApi.getChanelList(params).
then(function(data){
dealData(data);
});
}
tagApi.js
……
return {
getList: getTagList,
getListParams: getListParams,
……
};
function getTagList(params) {
return mockdata;
return new Promise(function(resolve,reject){
let params = dealParams(params);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
.then(function(data){
let result = dealData(data);
resolve(result);
},function(){reject});
});
}
function getListParams(){
return {
……
}
}
……
mediaApi.js
……
return {
getMediaList: getMediaList,
getListParams: getListParams,
……
};
function getMediaList(params) {
return new Promise(function(resolve,reject){
let params = dealParams(params);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
.then(function(data){
let result = dealData(data);
resolve(result);
},function(){reject});
});
}
function getListParams(){
return {
……
}
}
……
chanelApi.js
……
return {
getChanelList: getChanelList,
getListParams: getListParams,
……
};
function getChanelList(params) {
return new Promise(function(resolve,reject){
let params = dealParams(params);
trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
.then(function(data){
let result = dealData(data);
resolve(result);
},function(){reject});
});
}
function getListParams(){
return {
}
}
……
1. 復用和語義化
將相同的請求提取到公共服務中,可以明顯提高代碼復用率。
我們從上面的未抽象api
服務層的代碼看到,現在我們有三種請求,分別是:
- 請求標簽列表
- 請求素材列表
- 請求渠道列表
盡管我們在底層已經層狀了一層trsHttpService.httpServer
服務來實現http
請求的細節,做到了很高的復用性,但這里還是存在著重復性代碼,比如在這三個文件中,每次都要重新定義請求參數對象,如果這個參數對象只有少數幾個屬性,倒也還可以,但是如果這個參數對象包含十幾二十個參數,每次要用到這個請求前,都需要定義一遍,那重復率就太高了。另外,像trshttpService.httpServer(trshttpService.getBigdataROOT(),params,'post')
這樣的代碼也出現了多次,不僅冗長,而且從這個方法本身并無法看出它是做什么事的,無法做到語義化。
在抽象了api
服務層后,我們明顯看到兩個變化:
- 參數對象不再在各自的控制器里定義,而是統一從
api
服務層獲取。定義只在api
層發生一次,而在控制器中可以重用無數次,并且只需要一個方法調用。 - 獲取數據的方法更加語義化,消除了無語義的功能性代碼
2. 應對變化
無論后端接口的參數改動,還是地址改動,或者返回值改變等,都可以在該層做適配,不需要影響業務層。
我們來假設三個場景。
場景一:請求參數變化。
加入我們未抽象api
服務層時,控制器里定義的獲取標簽列表的請求參數如下:
let TagListParams = {
tagModel:'aaa',
tagTime: '...',
pageSize: 8,
curpage: 1
……
}
在進行請求前,我們肯定要根據用戶交互對參數進行賦值,比如:
function dealParams(params){
params.tagModel = userSelectModel;
params.tagTime = userInputTime;
...
return params;
}
好了,功能開發完成后,后端告訴你,要把tagModel
參數名稱改成 ModelName
, 現在你會怎么做呢?當然,你可能想到了用編輯器的全局查找替換功能,可是,在某個其它模塊的某個前輩開發的代碼中,有下面這樣的代碼:
function someFun(){
params.tagModel = abc;
...
}
而這個params
可能和你要改的接口毫無關系,但全局替換可能會將它也替換掉,所以,為了避免這種問題,你不得不一個一個去替換,如果整個系統中請求標簽列表的地方有四五十處,那將是非常惱人的。
如果抽象出了api
服務層,那就好辦了,我們看在api層里的代碼:
tagApi.js
//……
function dealParams(params) {
//……
params.ModelName = params.tagModel;
delete params.tagModel;
//……
return params;
}
只需要在api
服務層發送強求前的參數處理函數里統一處理一次即可,業務層的代碼完全不用做任何改動。
場景二: 接口地址變化
在未抽象api
層的代碼中我們看到,請求標簽列表和請求素材列表的接口地址是同一個,都是trshttpService.getBigdataROOT()
方法返回的,現在如果請求素材接口的地址不變還是用原來的,而請求標簽的接口使用了新的接口。那么按照老的做法,我們有如下步驟:
- 在
trshttpService
中新增一個地址獲取方法getNewRoot()
方法,并返回新的地址; - 找到項目中所有請求標簽列表的地方,一個一個將
getBigdataRoot
方法替換為getNewRoot
方法,當然,由于素材列表也使用的是getBigdataRoot
方法,我們同樣無法使用編輯器的全局搜索替換功能。
然而,這種方法不僅效率低,還有最大的問題是,程序中那么多請求標簽的地方,你在執行替換的過程中很可能會遺漏掉那么一兩處,一旦發生這種情況,那就是生產事故。
那么,在抽象了api
服務層后,我們怎么做的呢?
只有兩步:
- 在
trshttpService
中新增一個地址獲取方法getNewRoot()
方法,并返回新的地址。 - 進入
tagApi.js
,將getBigdataRoot
方法替換為 ``getNewRoot` 方法。
并且由于程序中所有請求標簽的地方都是調用的tagApi
服務,所以不用擔心有遺漏。
場景三: 響應數據變化
假設原來的素材列表返回的相應數據是這樣的:
{
DATA:[
{
relationid: 1,
resoucetitle: '測試素材'
//……
},
//……
],
PAGER: {……}
}
而我們的A.js
和C.js
中,獲取到數據后,有大概四五十處都是使用 item.resourcetitle
來使用這個值的。
現在后端響應的數據改了,不再使用resourcetitle
來顯示素材名稱,而是改為了materialName
。那我們怎么辦呢?
當然,我們一樣不能使用全局搜索替換,因為在系統的四十萬行代碼中你根本無法確定是否有個地方有一個與素材業務無關但也用了resourcetitle
這個命名。我們當然也不可能一個一個去找到進行替換,一是效率低,二是有可能遺漏。
如果未抽象api
服務層,我們最好的做法無非是:
在每一個控制器里,請求完素材的dealData()
方法了做如下處理:
function dealData(data){
data.foreach(function(item){
item.resourcetitle = item.materialName;
delete item.materialName
});
this.items = data;
}
這樣做還得有一個前提是所有請求素材列表的地方獲得數據后都得有一個dealData
方法,如果有些地方沒有,還必須再寫一個。
讓我們再次使用抽象api
服務層的方式來解決這個問題吧。
我們只需要將上面的遍歷和替換在mediaApi.js
的 dealData
里實現一次即可,并且我們不要求所有調用者都去在調用接口后執行我們規定的dealData
操作。
3. 面向接口編程
調用者不用關心請求地址如何獲取,也不關心請求是用POST方法還是GET方法,他唯一應該關心的是業務邏輯需要的數據如何獲取。
大家肯定也聽過面向接口編程,面向接口的核心思想有兩點:
調用者不關心接口內部實現細節
將定義與實現分離。
業務層需要使用數據時,只關心你給他一個獲取數據的方法,他并不需要去關心數據去哪個接口地址拿,也不需要關心這個數據是使用POST
方法還是GET
方法去獲取,他唯一需要關心的就是傳遞參數。就像我們去飯館吃飯點餐,我們并不需要關心飯店從哪里取進菜,也不需要關心廚師是用鐵鍋炒還是不銹鋼鍋炒,我們唯一需要的就是告訴服務員我們想吃什么。
在未抽象出api
服務層時,我們將getWCMRoot
,POST
這樣涉及到實現的細節暴露到了業務層,讓業務調用者參與了具體的實現方式和細節,這是不合理的。
而我們抽象出了api
服務層后,調用者只需要知道有個叫tagApi
的服務有一個getTagList()
方法可以給我們想要的標簽列表,就夠了。
上面我們解釋了面向接口編程的第一個特性:調用者不關心接口內部實現細節。下面我們看下第二個特性: 將定義與實現分離。
我們看到,在抽象出的api
服務層,我們是這樣返回的:
return {
getTagList: getTagList,
getListParams: getListParams,
……
};
function getTagList(){//...}
function getListParams(){//...}
而不是像這樣:
return {
getTagList: function(params){
//...
},
getListParams: function(params){
//...
}
}
或者這樣:
this.getTagList = function(params){//...}
this.getListParams = function(params){//...}
這種寫法上的細微差異在實際開發中帶來的效果是完全不一樣的。后兩種寫法都是將定義和實現耦合在了一起,而第一種方法實現了定義與實現的分離。這兩種思想的差異在這段代碼里看不出明顯差異,我們換個例子。
我們前端很多時候都會涉及到存儲,假設現在的存儲方案有cookie
和 localStorage
兩種,一開始我們采用了cookie
方案,api
像下面這樣:
- storeApi.js
return {
this.getCookie = function(){...}
this.setCookie = function(){...}
this.removeCookie = function(){...}
}
調用者像下面這樣使用:
-
D.js
storeApi.getCookie(...); storeApi.setCookie(...); storeApi.removeCookie(...);
后來發現cookie
方案不再滿足我們的需求了,更換為 localStorage
方案了,那有這么幾種方式:
方式一:偷懶改原來的實現
- store.js
this.getCookie = function(){//這里是localStorage實現}
this.setCookie = function(){//這里是localStorage實現}
this.removeCookie = function(){//這里是localStorage實現}
這樣做的好處是業務代碼不用更改,壞處很明顯,方法名和實現不一樣,造成后期維護成本上升.
方式二:新建一個服務
- stroeForLocal.js
this.getLocalStorage = function(){...};
this.setLocalStorage = function(){...};
this.removeLocalStorage = function(){...};
然后修改業務代碼:
- D.js
stroeForLocalApi.getLocalStorage(...)
stroeForLocalApi.setLocalStorage(...)
stroeForLocalApi.removeLocalStorage(...)
這樣做壞處顯而易見:需要大量改動業務代碼。
如果我們一開始就采用定義與實現分離的方式,看看代碼:
-
store.js
return { get: getCookie, set: setCookie, remove: removeCookie }; function getCookie(){//...} function setCookie(){//...} function removeCookie(){//...}
-
D.js
storeApi.storemethod = 'local' storeApi.get(...); storeApi.set(...); storeApi.remove(...);
變更為localStorage
方案后:
-
store.js
this.storeMethod =''; if(this.stroeMethod === 'cookie'){ return { get: getCookie, set: setCookie, remove: removeCookie }; }else { return { get: getLocalStorage, set: setLocalStorage, remove: removeLocalStorage }; } function getCookie(){//...} function setCookie(){//...} function removeCookie(){//...} function getLocalStorage(){...} function setLocalStorage(){...} function removeLocalStorage(){...}
而業務層則是無感知的。
這就是定義與實現分離所帶來的好處,業務層只需要知道有個存儲服務提供了set
,get
,remove
方法就行了,它并不關心該服務采取哪種存儲方案,存儲方案的變化只需要體現在api
服務層,不影響業務層。
4. 減少依賴
在請求服務還沒開發的情況下(也有可能是后端接口還沒開發完),只要給調用者提供一份接口清單,調用者就可以進行業務開發了,等請求服務將這些接口全部實現后,調用者不用做任何修改就可以直接使用。(可以配合mock方案)
在未抽離出api
服務層的情況下,我們一般的開發過程是
- 拿到一個需求
- 等后端給接口文檔的同時先寫靜態頁面(可能會在業務代碼中造假數據)
- 等到后端給我們接口文檔后,我們開始寫數據請求部分
- 這時候如果后端接口已經完成,我們就直接對接數據,并在業務代碼中寫請求了;
- 這時候如果后端接口還沒寫,我們就只能先按照接口文檔先寫拼參數發請求的部分,至于數據,還是自己造的假數據。
- 后面如果后端通知我們接口有變動,我們又得挨個在業務代碼里做相應的改動。
這個過程中,我們對后端其實是存在著很強的依賴性的。
比如按照未抽象api
服務層的代碼邏輯,在后端只有文檔尚未開發的時候,連后端都不知道接口要部署到哪個地址,那么前端就沒法去寫trshttpService.getBigdataRoot
這樣的代碼,因為你根本不知道地址。
而在抽象出api
服務層后,我們的過程可能變成了下面這樣:
- 拿到一個需求,根據需求在
api
服務層定義數據請求接口,可以先不實現請求,但可以根據頁面展示的元素定義并返回假數據(mock),這些假數據的字段名稱完全可以自己根據業務需求來定義。 - 等后端給接口文檔的同時做開發,同時利用假數據來進行頁面功能測試,
- 等后端給我們接口文檔后,我們在
api
層對參數和響應數據進行適配,而業務層代碼只需要因為參數的不同做少量適配就好。 - 等后端完成后,在
api
服務層去掉假數據,對接真實接口。
以上面的場景為例,即使后端還未開發,我們在業務代碼中也只是調用tagApi.getTagList()
這樣的方法,在api
層我們直接返回假數據,這樣既不影響前期開發節奏,后期即使對接了真實接口后,這一塊的代碼也不用變動。
我們看到,這個過程最大程度地減少了前端開發對后端接口的依賴,使得我們的開發更加順暢。
5. 單一職責
數據請求工作應該由專門的數據請求服務來完成,包括發送請求前的參數處理和接收到數據后的數據適配工作
以上說的都是實際開發中api
服務層帶給我們的好處,而之所以有那些好處,是因為它本身遵循了良好的設計原則,尤其是編程領域最重要的一條設計原則:單一職責原則。
單一職責的核心點就是各司其職,業務的歸業務,服務的歸服務。
就像我們去飯館吃飯,我們不會自己把點菜單送去廚房,而是由服務員送去廚房一樣,涉及請求的東西也不應該由調用者來參與(比如請求地址獲取、http方法指定、參數處理、響應數據適配)等等。
所有涉及請求的工作(包括請求的前置工作和后置工作)都應該統一交由api
服務層來進行,這才符合我們說的單一職責原則。
6.便于統一管理
在業務邏輯還未開發前就可以根據后端提供的接口文檔把所有和數據請求相關的事務集中完成
基于以上的工作,進行了api
服務的抽象后,所有和請求相關的工作都可以統一放置在api
服務層進行了,這樣便于統一管理所有接口請求,并且還帶來一個好處: 如果你在開發前已經拿到了后端給的接口文檔,那你就可以直接根據接口文檔把所有和請求相關的事務先集中完成,然后再去做業務,這樣不用在接口和業務之間來回切換,提升開發效率。