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
服务层进行了,这样便于统一管理所有接口请求,并且还带来一个好处: 如果你在开发前已经拿到了后端给的接口文档,那你就可以直接根据接口文档把所有和请求相关的事务先集中完成,然后再去做业务,这样不用在接口和业务之间来回切换,提升开发效率。