面对RESTful API开发已经有小半年了,我感觉RESTful API 是一种最恶心的API,它让前端工作量剧增,让前端同学有写不完的业务逻辑,让前端同学进入996.
现在招聘市场上的前端岗位越来越多地要求拥有RESTful API项目实践者优先。在这里我只想告诉前端同学遇到这种岗位,薪资多开个几K不为过,为什么呢?这相当把后端的一部分工作量移给前端同学,多要几K薪资不为过吧。
那为什么会有这种情况,先从什么是RESTful API说起。
什么是RESTful API
RESTful API 是一种面向“资源”开发的接口,用 URL 定位资源,用 HTTP 动词(GET、POST、DELETE、PUT、PATCH)描述操作。
可以把RESTful API理解为资源型接口,而以往我们所请求后端的接口是消费型接口。我们举一个例子来解释资源型接口和消费型接口的区别。
例如有一个日志列表,且每条日志中还有评论列表和点赞列表。假如用消费型接口来开发,只需要一个日志列表接口getLogList
就可以,其返回的数据结构如下所示,可以直接用接口返回的数据进行渲染日志列表。
[
{
id: "002e5657ad104feb986c3b640647cc08",
text: '我是一条日志',
publishTime: 1635157425,
publishName: '张三',
publishId: '7738290823676234234',
comments: [
{
id: '00dfawrw4feb986c3b640647cc08',
text: '我是一个评论',
createTime: 1635157425,
commentName: '李四',
commentId: '32894878275883957',
}
],
supports: [
{
id:'00dfawrfsgsfg86c3b640647cc08',
createTime: 1635157425,
supportName: '王五',
supportId: '32894878275883957',
}
]
}
]
假如用资源型接口来开发,会把日志列表接口拆成4个资源型接口,分别是:
- 用户资源接口
getUseList
,可根据用户ID查询对应用户信息,返回数据格式:
[
{
id:'78695979086612',
name:'张三'
}
]
- 日志资源接口
getLogList
,可根据日志ID查询对应日志信息,并提供分页能力,返回数据格式:
[
{
id: "002e5657ad104feb986c3b640647cc08",
text: '我是一条日志',
publishTime: 1635157425,
publishId: '7738290823676234234',
}
]
- 评论资源接口
getCommentList
,可根据评论ID查询对应日志信息,并提供分页能力,返回数据格式:
[
{
id: '00dfawrw4feb986c3b640647cc08',
text: '我是一个评论',
createTime: 1635157425,
commentId: '32894878275883957',
}
]
- 点赞资源接口
getSupportList
,可根据评论ID查询对应日志信息,并提供分页能力,返回数据格式:
[
{
id:'00dfawrfsgsfg86c3b640647cc08',
createTime: 1635157425,
supportId: '32894878275883957',
}
]
首先会发现返回的日志数据中publishName
这个代表日志发布人的字段不见了,原因是publishName
可以通过publishId
去用户资源中获取。
然后是comments
和supports
两个代表评论列表和点赞列表的字段也不见,得分别请求评论资源接口和点赞资源接口获取。
同样会发现评论和点赞数据中的commentName
和supportName
代表评论人名称和点赞人名称不见了,得通过commentId
和supportId
去用户资源中获取。
从上面的例子,我们可以很明显感受到了资源型接口和消费型接口的区别,作为一个前端的你会喜欢那种接口呢?答案肯定是消费型接口了,一个请求搞定,少了请求多次接口,拼接数据的工作量。
但是作为一个后端的同学,肯定会喜欢资源型接口,此类型接口比较纯粹。例如,改天产品要求开发一个用户评论和点赞记录的页面,此时后端同学不必再开发一个返回评论和点赞数据的消费型接口,直接叫前端同学使用评论资源接口、点赞资源接口、用户资源接口来开发这个界面。相当把后端的一部分工作移给前端来开发。
现在有越来越多的公司采用这种模式来开发,前端逐渐把业务逻辑的开发接走,后端重心移到对数据的获取且保证数据的可靠性上。
为了保住饭碗,前端应该学会如何面对RESTful API开发,下面来分享我认为面对RESTful API开发的四个必要技能。
学会async/await
接口是异步返回的,要用Promise
来处理。在使用资源型接口开发时,一般会这样
import * as Api from '@/api/log'
const getLogs = () => {
APi.getUseList()
.then(res => {
Api.getLogList()
.then(res1 => {
Api.getCommentList()
.then(res2 => {
//...
})
.catch(err2 => {
//...
})
Api.getSupportList()
.then(res3 => {
//...
})
.catch(err3 => {
//...
})
})
.catch(res => {
//...
})
})
.catch(err => {
//...
})
}
上面的代码出现了严重的地狱回调,得用async/await
来解决一下。
import * as Api from '@/api/log'
const getLogs = async () => {
const useList = await APi.getUseList();
const logList = await APi.getLogList();
const commentList = await APi.getCommentList();
const supportList = await APi.getSupportList();
}
async/await
的作用是用同步方式,执行异步操作,其是用try...catch
来进行处理错误。
import * as Api from '@/api/log'
const getLogs = async () => {
try {
const useList = await APi.getUseList();
} catch (err) {
console.log(err);
}
}
为了避免代码中到处都是try...catch
,在封装axios时,要创建一个含状态的对象,比如APi.getUseList()
返回的是这样一个对象。
let result = {
success:false,
data:{},
}
其中success
表示是否请求成功,true
表示成功,false
表示失败,data
表示请求回来的数据。
返回错误消息时,设置result.success
为false
,把错误消息赋值给result.data
。不管请求是否成功还是失败都用resolve(result)
返回, 这样await
执行的结果永远是正确的,就可以把代码中的try...catch
都去掉。
import * as Api from '@/api/log'
const getLogs = async () => {
const res = await APi.getUseList();
if (res.success) {
//请求成功处理
} else {
//请求失败处理
}
}
学会并发请求
使用await
把异步请求当同步请求处理,会造成阻塞。这时得考虑并发处理了。
在使用并发前,要搞明白,放在一起并发请求的接口,要满足那些业务场景,不同的场景要使用不同的Promise
的静态方法,例如:
-
要等并发请求的接口都请求成功后才能进行下一步操作或者只要有一个接口请求失败就终止操作,用
Promise.all()
。 -
只要并发请求的接口中请求成功一个就能进行下一步操作,用
Promise.any()
。 -
要等并发请求的接口都请求完成,不管成功还是失败,才能进行下一步操作,用
Promise.allSettled()
。
并发中的请求也有两种情况:
-
固定参数并发请求
Promise.all()
、Promise.any()
、Promise.allSettled()
的参数是固定写死的,这种情况比较简单。Promise.all([APi.getCommentList(),APi.getSupportList()]);
-
动态参数并发请求
Promise.all()
、Promise.any()
、Promise.allSettled()
的参数是动态变化。
例如,获取属于一个日志列表的所有评论数据,可以遍历日志列表,用每个日志ID去请求评论资源接口,并把这个并发请求处理,用map
方法构造一个异步请求的数组集合,把这个集合作为参数传入Promise.all
中。
import * as Api from '@/api/log'
const getCommentByLogId = async (logId) => {
const comment = await APi.getCommentList(logId);
if (comment.success) return comment.data.data;
return [];
}
const getAllComment = async () => {
const logs = await APi.getLogList();
if (!logs.success) return;
logs = logs.data.data;
const jobs = logs.map(item => {
if (item) {
return getCommentByLogId(item);
}
})
const result = await Promise.all(jobs);
return result.flat();
}
或许有人问,你对Promise.all()
的结果处理有错误,如果其中一个getCommentByLogId(item)
执行失败,返回一个错误信息,那么result
数组中就只有一项错误信息。
那么你要注意一下,在async
创建的异步函数中,用return
来返回结果,表示是执行成功返回结果,用throw
来返回结果,表示是执行失败返回结果。在getCommentByLogId
异步函数中只有用return
来返回结果,表示getCommentByLogId
执行结果只会是成功的。
const test = async (num) =>{
if(num > 2){
return num;
}else{
throw num
}
}
执行test(1)
时,只能用catch
捕获到返回值,执行test(2)
,只能用then
捕获到返回值。
那么要把评论添加到日志列表中对应的日志的评论区域中,该如何实现呢?在遍历日志列表中用日志ID请求评论资源接口,请求成功后直接赋值。
import * as Api from '@/api/log'
const getCommentByLogId = async (logId) => {
const comment = await APi.getCommentList(logId);
if (comment.success) return comment.data.data;
return [];
}
const getLogs = async () => {
let logs = await Api.getLogList();
if (!logs.success) return;
logs = logs.data.data;
const jobs = logs.map(async (item) => {
item.comments = await getCommentByLogId(item.id);
return item;
})
await Promise.all(jobs);
return logs
};
学会递归
现在有个需求,要获取到全部的评论数据,而评论资源接口是有分页的。此时得利用递归来实现,具体代码如下所示:
const getComment = async (page = 1, result = []) => {
const data = {
page,
prePage: 100,
}
let comment = await Api.commentList(data);
if (!comment.success) return [];
comment = comment.data.data;
if (comment.length > 0) {
result = [...result, ...comment];
page = page + 1;
return this.getComment(page, result);
} else {
return result
}
}
const init = async () =>{
const comments = await getComment();
console.log(comments);// 全部评论数据
}
学会构造树结构数据的方法
在RESTful API中,服务端一般只返回数组结构的数据,如果需要树结构的数据,只能自己构造了,
例如部门资源接口返回一个部门数组的数据,结构如下所示:
const depList = [
{
"id": "1394491627984502786",
"name": "运营部",
},
{
"id": "1394491628185829378",
"name": "运营C组",
"parentId": "1394491627984502786",
},
{
"id": "1394491628659785729",
"name": "A小组运营",
"parentId": "1394491628005474306",
},
{
"id": "1394491629515423745",
"name": "A组直辖运营",
"parentId": "1394491628005474306",
},
{
"id": "1394491629888716802",
"name": "C小组运营",
"parentId": "1394491628185829378",
},
{
"id": "1394491630131986433",
"name": "C组直辖运营1",
"parentId": "1394491628185829378",
},
{
"id": "1395669342024445954",
"name": "开发部",
},
{
"id": "1395684982546276353",
"name": "开发一组",
"parentId": "1395669342024445954",
},
{
"id": "1407968015211061249",
"name": "市场部",
},
]
当我们要展示部门树时,就要先构造一个部门树结构的数据,用createTree
函数来构造,具体代码如下所示:
很多掘友告诉我这里看不懂,为什么在生成父子关系的逻辑没有用到
tree
这个变量,这里是利用了数组、对象是引用类型的特性。
const createTree = (data) => {
let tree = [];
let dataMap = {};
for (const node of data) {
// 遍历数据生成数据map
dataMap[node.id] = node;
node.children = [];
// parentId父级ID为空则表示为树结构第一层节点
if (!node.parentId) {
tree.push(node);
}
}
// 生成父子关系
for (const node of data) {
// parentId父级ID为空则表示为树结构第一层节点,跳过循环
if (!node.parentId) continue;
const child = dataMap[node.id];
const parent = dataMap[node.parentId];
// 在dataMap中找不到子节点的数据或者父节点数据,跳过循环
if (!child || !parent) continue;
// 在dataMap中有找到子节点数据和父节点数据,
// 将子节点数据添加到父节点的children中
parent.children.push(child);
}
return tree;
};