拥抱ts之后更优雅的异步请求处理
前言
笔者个人认为,业务侧的前端其实蛮大一部分的工作的在处理异步请求和组织各种各样的请求把数据进行组装,比如从登录开始就需要处理登录拿到用户信息展示avator之类的组件,获取新闻列表接口拿到数据做table组件展示之类的业务是十分常见的。业务代码中的异步请求越多,如果处理的请求的代码不够优雅,那么后期维护的成本和可读性将会大打折扣。
告别try catch
告别try catch之前我想先说说为什么需要try catch吧。如果说读者连为什么需要try catch都不知道的话后续的内容将会听到云里雾里。先来看以下代码:
const callApi = () => {
const a = 12;
a.say(); // 此处有bug
return axios.get('www.baidu.com/info', {
id: 1
});
}
async function a() {
await callApi()
}
a();
对于异步处理的第一类常见异常就是上述的语法异常,如果在async/await函数中没有try catch这个异常又或者最外层的代码没有catch到这个异常,那么浏览器是会报错的引起程序崩溃,这可以说是每个程序员都不想发生的大事件了。于是乎我们在async/await中引入try catch来捕获这类错误。 还有一类异常是请求异常。先来看以下代码:
const callApi = () => {
return axios.get('www.baidu.com/info', {
id: 1
});
}
async function a() {
await callApi()
}
a();
这里笔者通过访问一个404接口引起一个请求异常,结果浏览器正合我意的崩溃了。
于是乎,不管是哪两种异常都好,出于对代码崩溃的敬畏,我们开始对async/await敬而远之,无论当中的什么await都打上一层try catch以图一份内心的安定。于是乎开始出现了以下代码:
async componentDidMount() {
try {
const loginInfo = await axios.get('www.baidu.com/login', {
id: 1
});
if (loginInfo.success) {
alert('登录成功');
} else {
alert(loginInfo.description);
}
} catch(err) {
console.log(err);
}
}
getNewsList = async () => {
try {
const newsData = await axios.get('www.baidu.com/news', {
id: 1
});
if (newsData.success) {
alert('登录成功');
} else {
alert(newsData.description);
}
} catch(err) {
console.log(err);
}
}
现实就是,基于一个又一个的开发者不假思索的try catch,代码将会被try catch充斥。好了,让我们开始准备告别try catch吧。下来书写以下代码:
// 包装一个promise请求,写上catch,返回一个结果确定的promise
function hRequest(p) {
return p.then(res => [res, undefined]).catch(err => [undefined, err]);
}
// 封装axios请求
export async function Get(p: {url: string, params: any}) {
const {url, params} = p;
return await hRequest(axios.get(url, params));
}
// 组件中的业务代码
async login(params) {
const [res, err] = await Get({
url: LOGINAPI,
params
});
if (err) {
console.log('登录失败')
} else {
// do something
}
}
非常好,业务中很多try catch代码将会被删去,一下子代码变得清真很多。以上的方法对于请求异常捕获十分凑效,但是对于语法逻辑异常无能为力。所以我们需要在全局对代码入口进行try catch或者注册全局error监听即可。也十分简单。至此,我们已经可以对try catch这等代码说再见了。
拥抱ts
为什么我会想要拥抱ts,因为对于前端而言,不仅仅简单的就是开发交互以及界面,跟后台打交道,处理接口,获取信息都是日常的开发任务。用上Ts之后,可以对后台的接口进行接口描述,数据类型声明,相比于之前没有Ts的接口逻辑会非常严谨,大大降低了因为数据类型引起的语法异常报错。可能我这么说会非常抽象,来看代码吧:这是一个用户信息接口,当我们用这个接口去约束我们的登录请求的返回主体,我们在任何一个地方需要用到用户信息,那么编辑器会自动接口提示所有的属性和类型,当我们想用不属于该数据类型的方法时,tslint将会报错,在开发阶段便帮我们解决了许多潜在bug。说了这么多拥抱Ts的好处,来看看我们怎么组织我们的请求,让他可以在请求时进行约定返回类型,为我们在使用的时候带来便利。
// 用泛型约束hRequest
function hRequest<T = any>(p: Promise<any>): [T, any] {
return p.then(res => [res, undefined]).catch(err => [undefined, err]) as unknown as [T, any];
}
export async function Get<T = any>(p: { url: string, params: any }): Promise<[T, any]> {
const { url, params } = p;
return await hRequest<T>(axios.get(url, params));
}
业务代码怎么使用呢?
有过ts开发经验的开发者很容易认出来,主要就是使用泛型,对入参和输出进行约束,在请求之前进行泛型约束,对请求结果便可以根据接口进行各类方便的操作。如果需要了解ts-泛型,可点击笔者另外一篇文章走进TS-泛型
更优雅的约束
在前端跟后台的对接中,后台返回的字段结构必然是存在一定的设计结构的,比如:
interface UserInfo {
id: string
name: string
age: number
}
interface LoginRes {
success: boolean
data: UserInfo
description: string
code: number
}
interface NewsDetail {
id: string
content: string
time: number
}
interface NewsRes {
success: boolean
data: NewsDetail[]
description: string
code: number
}
可以看见这些接口都存在大量重复的结构,于是乎我们可以抽象data的部分出来,然后去业务中请求试试
interface FetchRes<T> {
success: boolean
data: T
description: string
code: number
}
但是这样还不够简单,我们只想在请求的时候更关注data本身的接口类型,于是乎我们把FetchRes接口封装进请求方法内部作为泛型的默认值。
function hRequest<T = any>(p: Promise<any>): [T, any] {
return p.then(res => [res, undefined]).catch(err => [undefined, err]) as unknown as [T, any];
}
export async function Get<T = any>(p: {url: string, params: any}): Promise<[FetchRes<T>, any]> {
writeHeader();
const {url, params} = p;
const [res, err] = await hRequest<FetchRes<T>>(axios.get(url, params));
if (err) {
return [res, err];
} else {
fetchLog(url, params, res);
return [getDataFromMock(res) as FetchRes<T>, err]
}
}
这样我们的业务代码只需要关注data本身就好。
总结
经过上述的组合拳,项目中的异步请求代码更为简化、清真,不再对async/await敬而远之了,需要自定义处理请求状态可以判断解构值,对于拥抱了ts之后的请求主体可以再约定了主体接口之后放心大胆的使用对应的数据类型和方法。