close

開發中無論怎樣都會產生網絡請求,這樣一來自然也就避免不了大量使用then、catch或try catch來捕獲錯誤,而捕獲錯誤的代碼量是隨着網絡請求的增多而增多,那應該如何優雅的系統性捕獲某個網絡請求中所產生的所有錯誤呢?

首先最常用的兩種處理網絡請求的形式即Promise與async(事實上很多請求庫都是基於這兩者的封裝),使用Promise那必然要與then、catch掛鈎,也就是說每個請求都對應一個Promise實例,然後通過該實例上對應的方法來完成對應的操作,這應該算是比較常用的一種形式了

但如果涉及嵌套請求,那可能還要不斷的增加then、catch來完成需求,好了,現在可以使用看起來真的像同步編程的async來着手優化了,即await promise,那這種情況下就根本不需要手動then了,但如果await promise拋出了錯誤呢?那恐怕不得不讓try catch來幫忙了,而如果也是嵌套請求,那與Promise寫法類似的問題又來了,有多少次請求難道我就要多少次try catch嗎?那這樣看來的話,Promise與async在面對這種屎山請求的時候確實有點心有餘而力不足了

前言

之所以寫作本篇文章是因為前幾天在優化數據庫操作時,發現要不停try catch,且操作數據庫的代碼越多,則try catch就越多,於是突發奇想,能不能封裝一個工具類來實現智能化捕獲錯誤呢?在這種思維的推動下,我覺得這個工具類不僅僅是以一種創意的形式出現,更多的是實用性!(先不考慮這個創意能否實現)

一個令人頭疼的需求

家在吉林的小明想去海南看望他的老奶奶,但小明覺得旅途如此之長,不如先去山東學習學習馬保國老師的「接化發」,然後再去雲南拍一個「**我是雲南的 雲南怒江的...**」的視頻發一下朋友圈,最後再去海南看望老奶奶

請你運用所學知識幫幫小明,查詢吉林--山東--雲南--海南的車票還有嗎?

如果有的話,老奶奶希望小明不要在車票上花費太多的錢,所以當小明出發時,需要告訴老奶奶本次所有車票的開銷是多少
如果沒有的話,請你務必告訴小明是哪裡的車票沒有了,因為小明可能會換個路線去找老奶奶

注意,當確定吉林-山東的車票未售空時才去查詢山東-雲南的車票是否已售空,並以此類推;因為這樣的話,小明可以知道是哪個地方的車票沒有了,並及時換乘

雖然吉林--山東--雲南--海南的車票可以一次性查詢完畢,但為了體現嵌套請求的複雜度,我們此處不討論並發請求的情況,關於並發,你可以使用Promise.all

flow_chart.png

先來細化題目,可以看到路線依次為:吉林-山東、山東-雲南、雲南-海南,也就分別對應三個請求,且這三個請求又是嵌套發出的。而每次發出的請求,最終都會有兩種情況:請求成功/失敗,請求成功則代表本輪次車票未售空,請求失敗則代表本輪次車票已售空

之所以請求失敗對應車票已售空,是為了模擬請求失敗的情況,而不是通過返回一個標識來代表本輪次車票是否已售空

這個令人頭疼的需求,我建議你再認真讀一遍

準備工作

為了簡單起見,這裡就不額外開啟一台服務器了,轉而使用定時器模擬異步任務

以下是用於查詢車票的接口,我們稱之為請求函數

在下文中所指的請求函數就是requestJS、requestSY、requestYH

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,true]//查詢吉林-山東的車票是否已售空的接口constrequestJS=()=>newPromise((res,rej)=>{setTimeout(()=>{//請求成功(resolve)則代表車票未售空if(interface[0])returnres({ticket:true,price:530,destination:'吉林-山東'})//請求成功(rejected)則代表車票已售空rej({ticket:false,destination:'吉林-山東'})},1000)})//查詢山東-雲南的車票是否已售空的接口constrequestSY=()=>newPromise((res,rej)=>{setTimeout(()=>{if(interface[1])returnres({ticket:true,price:820,destination:'山東-雲南'})rej({ticket:false,destination:'山東-雲南'})},1500)})//查詢雲南-海南的車票是否已售空的接口constrequestYH=()=>newPromise((res,rej)=>{setTimeout(()=>{if(interface[2])returnres({ticket:true,price:1500,destination:'雲南-海南'})rej({ticket:false,destination:'雲南-海南'})},2000)})複製代碼Promise

一定要避免重複造輪子,所以先用Promise實現一下,看看效果如何,然後再決定應該怎麼操作

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,true]//先查詢吉林到山東requestJS().then(({price:p1})=>{console.log(`吉林-山東的車票未售空,價格是${p1}RMB`)//如果吉林-山東的車票未售空,則繼續查詢山東-雲南的車票requestSY().then(({price:p2})=>{console.log(`山東-雲南的車票未售空,價格是${p2}RMB`)//如果山東-雲南的車票未售空,則繼續查詢雲南-海南的車票requestYH().then(({price:p3})=>{console.log(`雲南-海南的車票未售空,價格是${p3}RMB`)console.log(`本次旅途共計車費${p1+p2+p3}RMB`)}).catch(({destination})=>{console.log(`來晚了,${destination}的車票已售空`)})}).catch(({destination})=>{console.log(`來晚了,${destination}的車票已售空`)})}).catch(({destination})=>{console.log(`來晚了,${destination}的車票已售空`)})複製代碼

測試結果如下

promise1.gif

不錯,符合預期效果,現在來將第二次請求變為失敗(即山東-雲南請求失敗)

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,false,true]複製代碼

現在再來看結果

promise2.gif

依然符合預期效果,但這種方式嵌套的層級太多,一不小心就會成為屎山的必備條件,必須優化一下

由於then會在請求成功時觸發,catch會在請求失敗時觸發,而無論是then或catch都會返回一個Promise實例(return this),我們也正是藉助這個特性來實現then的鏈式調用

如果then方法沒有返回值,則默認返回一個成功的Promise實例,而下面代碼則手動為then指定了其需要返回的Promise實例。無論其中哪個Promise的狀態更改為失敗,都會被最後一個catch所捕獲

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,false]letacc=0//先查詢吉林到山東requestJS().then(({price:p1})=>{acc+=p1console.log(`吉林-山東的車票未售空,價格是${p1}RMB`)//如果吉林-山東的車票未售空,則繼續查詢山東-雲南的車票returnrequestSY()}).then(({price:p2})=>{acc+=p2console.log(`山東-雲南的車票未售空,價格是${p2}RMB`)//如果山東-雲南的車票未售空,則繼續查詢雲南-海南的車票returnrequestYH()}).then(({price:p3})=>{//能執行到這裡,就說明前面所有請求都成功了acc+=p3console.log(`雲南-海南的車票未售空,價格是${p3}RMB`)console.log(`本次旅途共計車費${acc}RMB`)}).catch(({destination})=>console.log(`來晚了,${destination}的車票已售空`))複製代碼promise3.gif

可以看到經過優化後的Promise已經把屎山磨平了一點,美中不足的就是如果想要計算總共花費的車費,那麼需要在外部額外聲明一個acc用來統計數據,其實這種情況可以對請求車票數據的函數requestJS等來和每次then的返回值進行簡單包裝,但在此處,我不想改動請求車票數據的函數體,至於為什麼,我們繼續往下看

async

既然Promise都說了,也是時候把async這位老大哥請出來幫幫場子了,不多贅述,我們來看async會怎麼處理這種嵌套請求

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,true]constf=async()=>{try{constjs=awaitrequestJS()console.log(`吉林-山東的車票未售空,價格是${js.price}RMB`)constsy=awaitrequestSY()console.log(`山東-雲南的車票未售空,價格是${sy.price}RMB`)constyh=awaitrequestYH()console.log(`雲南-海南的車票未售空,價格是${yh.price}RMB`)console.log(`本次旅途共計車費${js.price+sy.price+yh.price}RMB`)}catch({destination}){console.log(`來晚了,${destination}的車票已售空`)}}f()複製代碼async1.gif

要麼怎麼稱它為老大哥呢,不得不說,果然老練啊,基本不用怎麼優化就已經磨平了一點屎山

其實async與上面Promise的第二種寫法有異曲同工之妙,可以看做都是將所有成功的邏輯放在了一起,僅僅使用了一個catch便可以捕獲所有錯誤,不得不說,真是妙蛙種子吃着妙脆角進了米奇妙妙屋,妙到家了

但,你以為今天的文章就到這了嗎?大錯特錯,正是因為這種重複性catch,所以才會萌生出自己封裝一個智能化捕獲函數來處理這種情況。上面所講到的Promise與async其實已經是很常見的一種寫法了,但如果項目中存在第二種嵌套請求(比如先請求所在省份的天氣,再請求所在縣的天氣)。如果放在async面前,我想它一定會使用兩個f函數,一個為查詢小明車票,一個為查詢天氣,那這就避免不了要寫兩個try catch了,文章開頭我所說到的對數據庫的操作大概就是這種困惑

現在來解開謎底,分享一下我是如何在有想法--確定目標--開始實現--遇到問題--解決問題--達到目標這種模式的推動下來一步一步完成的函數封裝

如果你對上述Promise和async有更好的優化方式,請分享在評論區 期待你的最優解

combine-async-error心路歷程

要解決一個問題,首先要明白解決它的意義何在。在小明看望老奶奶這個問題中,我們正是被這種不停地catch所困惑,所以才要想出更好的辦法去優化它。於是我就想着能不能封裝一個函數來替我完成所有的catch操作呢?既然這種念頭已經有了,那就開始動手實現

撿撿之前的知識

在封裝之前,你必須要知道以下知識點

try catch 不能捕獲異步錯誤//可以捕獲try{throwReferenceError('對象isnotdefined')}catch(e){console.log(e)}複製代碼//不可以捕獲try{setTimeout(()=>{throwReferenceError('對象isdefined')})}catch(e){console.log(e)}複製代碼Generator

你可以把Generator函數稱作生成器,調用生成器函數會返回一個迭代器來控制這個生成器執行其代碼,在生成器中你可以使用yield關鍵字,理論上yield可以出現在任何能求值的地方,我們通過迭代器的next方法來確保生成器始終是可控的

constf=function*(){console.log(1)//注意yield只能出現在Gerenator函數中//如果你將yield寫在了回調里,請一定要確認這個回調是一個生成器函數yieldconsole.log(2)}f().next()//1複製代碼async

async函數在執行時,遇到await會交出「線程」,轉而去執行其它任務,且await總是會異步求值

constf=async()=>{console.log(1)await'鯊魚辣椒'console.log(3)}f()console.log(2)//123複製代碼

如果你對上面幾個題目還存在疑問,請在《JavaScript每日一題》[1]專欄中找到對應的題目進行練習

好了,現在開始由淺入深逐步分析

讓await永遠不要拋出錯誤

讓await永遠不要拋出錯誤,這也是最重要的前提

//getInfo為獲取車票信息的功能函數constgetInfo=async()=>{try{constresult=awaitrequestJS()returnresult}catch(e){returne}}複製代碼

await右邊是獲取吉林-山東車票信息的函數requestJS,該函數會返回一個promise對象,當這個promise對象的狀態為成功時,await會把成功的值賦給result,而當失敗時,會直接拋出錯誤,一般我們會在await外包裹一層try catch來捕獲可能出現的錯誤,那能不能不讓await拋出錯誤呢?

很明確的告訴你,可以,只需要封裝一下await關鍵字即可

保證不拋出錯誤// noErrorAwait負責拿到成功或失敗的值,並保證永遠不會拋出錯誤!constnoErrorAwait=asyncf=>{try{constr=awaitf()return{flag:true,data:r}}catch(e){return{flag:false,data:e}}}constgetInfo=()=>{constresult=noErrorAwait(requestJS)returnresult}複製代碼

在noErrorAwait的catch里請不要進行一些副作用操作,除非你真的需要那些東西

有了noErrorAwait的加持,getInfo可以不再是一個async函數了,但此時的getInfo仍會返回一個promise對象,這是因為noErrorAwait是async函數的緣故。封裝到這裡,noErrorAwait已經實現了它的第一個特點——保證不拋出錯誤,現在來把getInfo補全

constnoErrorAwait=asyncf=>{try{constr=awaitf()//(A)return{flag:true,data:r}}catch(e){return{flag:false,data:e}}}constgetInfo=()=>{constjs=noErrorAwait(requestJS)//(B)console.log(`吉林-山東的車票未售空,價格是${js.data.price}RMB`)constsy=noErrorAwait(requestSY)//(C)console.log(`山東-雲南的車票未售空,價格是${sy.data.price}RMB`)constyh=noErrorAwait(requestYH)//(D)console.log(`雲南-海南的車票未售空,價格是${yh.data.price}RMB`)console.log(`本次旅途共計車費${js.price+sy.price+yh.price}`)}複製代碼

我們分別為(B)、(C)、(D)所對應的請求函數都套上了一層noErrorAwait,正是由於這種緣故,我們可以在getInfo中始終確保(B)、(C)、(D)下的請求函數不會報錯,但致命的問題也隨之到來,getInfo會確保請求函數是順序執行的嗎?

仔細看一遍就會發現getInfo是不負責順序執行的,甚至可能會報錯。這是因為noErrorAwait中await關鍵字的緣故,現在手動執行一下分析原因

調用getInfo
調用noErrorAwait並傳遞參數requestJS
來到noErrorAwait中,由於noErrorAwait是async函數,所以會返回一個promise對象
執行await f(),這個f就是requestJS,由於requestJS是一個異步任務,所以交出本次「線程」,也就是從(A)跳到(B)的下方,打印js.data.price,結果發現拋出了TypeError
拋出TypeError的原因是因為(B)的變量js是一個初始化狀態的promise對象,所以說訪問初始化中的數據怎麼可能不報錯!

那問題來了,noErrorAwait只負責讓所有的請求函數都不拋出錯誤,但它並不能確保所有請求函數是按順序執行的,如何才能讓它們按照順序執行呢?

難不成又要把getInfo變回async函數,然後再通過await noErrorAwait(...)的形式來確保所有請求函數是按照順序執行的,果然魚與熊掌不可得兼,如果真的使用這種方式,那await noErrorAwait(...)如果拋出了錯誤,誰來捕獲呢?總不能在它外面再套一層noErrorAwait吧

保證順序執行

這個想法實現到這裡,其實已經出現了很大的問題了——「保證不拋出錯誤」和「順序執行」不能同時成立,但也不能遇到bug就關機睡覺呀。這個問題當時我認真思考過,期間不泛break、Proxy等其它騷操作,在束手無策的時候,我突然想到了它的表哥——Generator,由於生成器是可控的,我只需要在上一次請求完成時,調用next發起下一次請求,這不就可以解決了嗎,確實是不錯的想法,現在來試試

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,true]constnoErrorAwait=asyncf=>{try{constr=awaitf()generator.next({flag:true,data:r})}catch(e){return{flag:false,data:e}}}constgetInfo=function*(){constjs=yieldnoErrorAwait(requestJS)console.log(`吉林-山東的車票未售空,價格是${js.data.price}RMB`)constsy=yieldnoErrorAwait(requestSY)console.log(`山東-雲南的車票未售空,價格是${sy.data.price}RMB`)constyh=yieldnoErrorAwait(requestYH)console.log(`雲南-海南的車票未售空,價格是${yh.data.price}RMB`)console.log(`本次旅途共計車費${js.data.price+sy.data.price+yh.data.price}`)}constgenerator=getInfo()generator.next()複製代碼

先來看測試結果

generator1.gif

當請求全部成功時,所有數據都拿到了,不得不說,這一切都要歸功於yield關鍵字

當noErrorAwait感知到請求函數成功時,會調用next,從而推動嵌套請求的發起,而且也不用擔心生成器在什麼時候執行完,因為一個noErrorAwait總會對應着一次next,這樣一來getInfo就差不多已經在掌控之中了,但有個致命的問題就是:noErrorAwait感知到錯誤時,應該如何處理?如果繼續調用next,那就與不用生成器沒有區別了,因為始終都會順序執行,解決辦法就是傳遞一個函數,在noErrorAwait感知到錯誤時調用該函數,並且把出錯的請求函數之前的所有請求結果全部傳遞進去,這樣當這個回調執行時,便代表某一個請求函數拋出了錯誤

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,false,true]//存儲每次的請求結果constresult=[]//失敗的回調(不要關心callback定義在哪裡,以及如何傳遞)constcallback=(...args)=>console.log('某個請求出錯了,前面收到的結果是',...args)//(A)constnoErrorAwait=asyncf=>{try{constr=awaitf()constargs={flag:true,data:r}result.push(args)generator.next(args)}catch(e){constargs={flag:false,data:e}result.push(args)callback(result)returnargs}}constgetInfo=function*(){//(B)constjs=yieldnoErrorAwait(requestJS)console.log(`吉林-山東的車票未售空,價格是${js.data.price}RMB`)constsy=yieldnoErrorAwait(requestSY)console.log(`山東-雲南的車票未售空,價格是${sy.data.price}RMB`)constyh=yieldnoErrorAwait(requestYH)console.log(`雲南-海南的車票未售空,價格是${yh.data.price}RMB`)console.log(`本次旅途共計車費${js.data.price+sy.data.price+yh.data.price}`)}constgenerator=getInfo()//(C)generator.next()//(D)複製代碼

通過測試可以發現當第二個請求函數拋出了錯誤時,noErrorAwait可以完全捕獲,並及時通過callback向用戶返回了數據

generator2.gif

這樣就實現了一個功能較為齊全的處理嵌套請求的函數了,但仔細看看就會發現,代碼中的(A)、(B)、(C)、(D)(包括(B)中的所有yield)都是由用戶自定義的,也就是說,每次用戶在使用這段處理嵌套請求的邏輯之前,都必須要自定義上面四處代碼,那這樣一來這個功能就變的極其雞肋了,不僅對用戶來說很頭疼,就連開發者也落不到一個好的口碑

既然沒有達到理想層面,那就說明還需要努力優化

是時候解決掉所有問題了

開始封裝

通過上面的種種問題,就能得出自己的經驗和教訓,要麼優化好了,但不能顧及其它情況;要麼完成了功能,但使用起來的體驗極其差勁。現在就來封裝一個combineAsyncError函數,這個函數會完成所有的邏輯處理及調度,而用戶則只需要傳遞請求函數即可

combineAsyncError即字面意思,捕獲異步錯誤,當然它也可以捕獲同步錯誤

使用形式constcombineAsyncError=tasks=>{}constgetInfo=[requestJS,requestSY,requestYH]combineAsyncError(getInfo).then(data=>{console.log('請求結果為:',data)})複製代碼

combineAsyncError接收一個由請求函數所構成的數組,該函數會返回一個Promise對象,其then方法被執行時,就代表嵌套請求結束了(有可能因為成功而結束,亦有可能因為失敗而結束),不過不要擔心,因為data的值始終為{ result, error },如果error存在則代表請求失敗,反之成功

完成combineAsyncError的返回值constcombineAsyncError=tasks=>{returnnewPromise(res=>handler(res))}複製代碼

當調用res時,會通知當前的Promise實例去執行它的then方法,而res也正是殺手鐧,只需在請求失敗或全部請求成功時調用res,這樣then就會知道嵌套請求的邏輯執行完畢

combineAsyncError的初始化工作

在handler中完成處理請求函數的邏輯。也就是操作Generator函數,既然這裡要使用生成器,那就很有必要做一下初始化工作

constcombineAsyncError=tasks=>{constdoGlide={node:null,//生成器節點out:null,//結束請求函數的執行times:0,//表示執行的次數data:{//data為返回的最終數據result:[],error:null,}}consthandler=res=>{}returnnewPromise(res=>handler(res))}複製代碼

doGlide相當於一個公共區域(你也可以理解為原型對象),把一些值和數據存放在這個公共區域中,其它人可以通過這個公共區域來訪問這裡面的值和數據

在handler中使用Generator

初始化完畢,現在所有的值和數據都找到」家「(存放的地方)了,接下來在handler中使用生成器

constcombineAsyncError=tasks=>{constdoGlide={}consthandler=res=>{doGlide.out=res//預先定義好生成器doGlide.node=(function*(){const{out,data}=doGlideconstlen=tasks.length//yield把循環帶回了JavaScript編程的世界while(doGlide.times<len)yieldnoErrorAwait(tasks[doGlide.times++])//全部請求成功(生成器執行完畢)時,返回數據out(data)})()doGlide.node.next()}returnnewPromise(res=>handler(res))}複製代碼

把res賦值給doGlide.out,調用out就是調用res,而調用res就代表本次處理完成(可以理解成out對應了一個then方法)。把Generator生成的迭代器交給doGlide.node,並先在本地啟動一下生成器doGlide.node.next(),這個時候會進入while,然後執行noErrorAwait(tasks[doGlide.times++]),發出執行noErrorAwait(...)的命令後,noErrorAwait會被調用,且while會在此時變為可控的循環,因為noErrorAwait是一個異步函數,只有當yield得到具體的值時才會執行下一次循環(換句話說,yield得到了具體的值,那就代表本輪循環完成),而yield有沒有值其實無所謂,我們只是利用它的特性來把循環變為可控的而已

擴展noErrorAwait

至此,所有的準備工作其實都已完備,就差noErrorAwait來完成整體的調度了,話不多說,接下來開始實現

constcombineAsyncError=tasks=>{constdoGlide={}constnoErrorAwait=asyncf=>{try{//執行請求函數constr=awaitf()//追加數據doGlide.data.result.push({flag:true,data:r})//請求成功時繼續執行生成器doGlide.node.next()}catch(e){doGlide.data.error=e//當某個請求函數失敗時,立即終止函數執行並返回數據doGlide.out(doGlide.data)}}consthandler=res=>{}returnnewPromise(res=>handler(res))}

在noErrorAwait這個async函數中,使用try catch來保證每一次請求函數執行時都不會拋出錯誤,當請求成功時,追加請求成功的數據,並且繼續執行生成器,而生成器執行完畢,也就代表while執行完畢,所以out(data)實則是結束了整個combineAsyncError函數;而當請求失敗時,則賦予error實際的值,並且執行doGlide.out來向用戶返回所有值

至此,一個簡單的combine-async-error函數便封裝完畢了,現在通過兩種情況進行測試

請求函數全部成功
//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,true,true]constgetInfo=[requestJS,requestSY,requestYH]combineAsyncError(getInfo).then(data=>{console.log('請求結果為:',data)})複製代碼c_a_e1.gif
某一個請求函數拋出錯誤
//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,false,true]constgetInfo=[requestJS,requestSY,requestYH]combineAsyncError(getInfo).then(data=>{console.log('請求結果為:',data)})c_a_e2.gif碼上掘金

上面所編寫的示例及封裝的combine-async-error已存放至碼上掘金

https://code.juejin.cn/pen/7121685764311613447

比較三種形式(Promise、async、combine-async-error)

現在來比較一下三種形式,三種形式統一使用下面的請求結果

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,false,true]複製代碼Promiseletacc=0//先查詢吉林到山東requestJS().then(({price:p1})=>{acc+=p1console.log(`吉林-山東的車票未售空,價格是${p1}RMB`)//如果吉林-山東的車票未售空,則繼續查詢山東-雲南的車票returnrequestSY()}).then(({price:p2})=>{acc+=p2console.log(`山東-雲南的車票未售空,價格是${p2}RMB`)//如果山東-雲南的車票未售空,則繼續查詢雲南-海南的車票returnrequestYH()}).then(({price:p3})=>{//能執行到這裡,就說明前面所有請求都成功了acc+=p3console.log(`雲南-海南的車票未售空,價格是${p3}RMB`)console.log(`本次旅途共計車費${acc}RMB`)}).catch(({destination})=>console.log(`來晚了,${destination}的車票已售空`))複製代碼asyncconstf=async()=>{try{constjs=awaitrequestJS()console.log(`吉林-山東的車票未售空,價格是${js.price}RMB`)constsy=awaitrequestSY()console.log(`山東-雲南的車票未售空,價格是${sy.price}RMB`)constyh=awaitrequestYH()console.log(`雲南-海南的車票未售空,價格是${yh.price}RMB`)console.log(`本次旅途共計車費${js.price+sy.price+yh.price}RMB`)}catch({destination}){console.log(`來晚了,${destination}的車票已售空`)}}f()combine-async-errorconstgetInfo=[requestJS,requestSY,requestYH]combineAsyncError(getInfo).then(({result,error})=>{result.forEach(({data})=>console.log(`${data.destination}的車票未售空,價格是${data.price}RMB`))if(error)console.log(`來晚了,${error.destination}的車票已售空`)})

可以看到combine-async-error這種智能捕獲錯誤的方式確實優雅,無論多少次嵌套請求,始終只需要一個then便可以輕鬆勝任所有工作,並且使用combine-async-error的形式也很簡潔,根本不需要編寫複雜的嵌套層級,在使用之前也不需要進行其它令人頭疼的操作

擴展功能

雖然combineAsyncError函數實現到這裡已經取得了不小的成就,但經過多次測試,我發現combineAsyncError始終還差點東西

現在來對combineAsyncError增加可選的配置項,提高其擴展性、靈活性

由於 combineAsyncError 配置項眾多,所以僅以 forever 舉例,如果你想了解更加強大的 combineAsyncError ,我在文末有詳細介紹

forever取它的字面意思,即永遠;不斷地,在combineAsyncError里我們使用配置項forever來決定當請求函數遇到錯誤時,是否繼續執行,默認為false

//標識每次請求的成功與否(吉林-山東、山東-雲南、雲南-海南)constinterface=[true,false,true]constcombineAsyncError=(tasks,config)=>{constdoGlide={}constnoErrorAwait=asyncf=>{try{constr=awaitf()doGlide.data.result.push({flag:true,data:r})doGlide.node.next()}catch(e){doGlide.data.result.push({flag:false,data:e})//當forever為true時,不必理會錯誤,而是繼續執行生成器if(config.forever)returndoGlide.node.next()doGlide.out(doGlide.data)}}consthandler=res=>{}returnnewPromise(res=>handler(res))}constgetInfo=[requestJS,requestSY,requestYH]combineAsyncError(getInfo,{forever:true}).then(data=>{console.log('請求結果為:',data)})複製代碼c_a_e3.gif

通過測試結果可以看到即使第二次請求失敗了,但第三次請求依舊會正常發出,且combineAsyncError不會拋出任何錯誤。而無論是請求成功的結果,還是請求失敗的結果,都可以在result中拿到

其它配置項

重新定義了combineAsyncError的入參數組,使其擴展性變的更高,另外增加了以下配置項

isCheckTypes

在設計combine-async-error時,關於傳入的參數是否進行校驗,其實是存在一些負面影響的,為此,combine-async-error主動添加了isCheckTypes配置項,如果該配置項的值為false,則不對入參進行檢查,反之進行嚴格的類型檢查。如果可以確保傳入的類型始終是正確的,那麼強烈建議你將該配置項更改為false;默認為true

由於JavaScript中存在隱式類型轉換,所以即使你指定了isCheckTypes為true,combine-async-error也不會對傳入的第二個參數(config)進行檢查

isCheckTypes:true複製代碼accacc:()=>{}複製代碼

如果指定了該值為函數,則所有請求完成後會執行該函數,此回調函數會收到最終的請求結果

如果未指定該值,則combine-async-error返回一個Promise對象,你可以在它的then方法中得到最終的請求結果

forever

遇到錯誤時,是否繼續執行(發出請求)。無論是嵌套還是並發請求模式,該配置項始終生效

forever:false複製代碼pipessingle

後一個請求函數是否接收前一個請求函數的結果

whole

後一個請求函數是否接收前面所有請求函數的結果

當whole為true時,single無效,反之有效

pipes:{single:false,whole:true}複製代碼all

combine-async-error應該得到原有的擴展,為此它支持新的配置項all,如果為all指定了order值,則傳入combine-async-error的請求數組會並發執行,而不是繼續以嵌套的形式執行

下面的寫法相當於使用了all的默認配置,因為all的值默認為false

all:false//嵌套請求複製代碼

下面的寫法則是開啟了並發之旅,all為一個對象,其order屬性決定並發的請求結果是否按照順序來存放到最終數組中

all:{order:true}複製代碼

關於order的使用,舉例如下

//假設requestAuthor始終會在3-6秒鐘之內返回請求結果constrequestAuthor=()=>{}//假設requestPrice始終會在1秒鐘之內返回請求結果constrequestPrice=()=>{}constgetInfo=[requestAuthor,requestPrice]combine-async-error(getInfo,{all:{order:true}})複製代碼

由於你指定了order為true,那麼在最終的請求結果數組result中,requestAuthor的請求結果會作為result的第一個成員出現,而requestPrice的請求結果則會作為該數組的第二個成員出現,這是因為order始終會保證result與getInfo的順序一一對應,即使requestPrice是最先執行完的請求函數

如果指定了order為false,則最先執行完的請求函數所對應的結果就會在result中越靠前;在上例中requestPrice的請求結果會出現在result的第一個位置

requestCallbackrequestCallback:{always:false,success:false,failed:false}複製代碼

always表示無論請求函數是成功還是失敗,都會在拿到請求結果後執行為該請求函數提前指定好的callback,此callback會收到當前請求函數的結果

success表示只有當請求函數成功時,才會去執行提前執行好的callback,並且callback會收到當前請求函數執行成功的結果;failed則表示失敗,與success同理

例如,當傳入請求函數的形式為

combineAsyncError([{func:requestAuthor,callback:()=>{}//提前為requestAuthor指定好的回調函數}],{requestCallback:{failed:true//指定了failed}})複製代碼

上述示例中,只有當requestAuthor請求函數出錯時,才會執行該請求函數所指定好的callback回調,並且此回調函數會收到requestAuthor失敗的原因

設計初衷

combine-async-error設計的初衷是為了解決複雜的嵌套請求,現在通過豐富的配置項它也可以支持並發請求模式(不僅如此,還可以把請求函數玩出新的高度),但面對單個請求函數的情況,其效果並不理想,舉例

創建一個新的請求函數requestTest,請求結果為失敗

constrequestTest=name=>newPromise((res,rej)=>{setTimeout(()=>{rej({name,destination:'今日所有車票已售空'})},1000)})複製代碼

現在分別使用Promise和combine-async-error來完成requestTest的調用

//PromiserequestTest('小明').catch(({name,destination})=>console.log(`${name}你好,${destination}`))複製代碼//combine-async-errorconstgetInfo=[{func:requestTest,args:['小明']}]combineAsyncError(getInfo).then(({error:{msg:{name,destination}}})=>console.log(`${name}你好,${destination}`))複製代碼vs.gif

在面對單個請求函數的情況下,使用Promsie可以便捷的發出請求,並且使用形式也較為簡單;而combine-async-error則顯得有些冗餘了(指定請求函數、指定其收到的參數...);但隨着請求函數的增多,我想combine-async-error的優勢一定會體現出來

立即體驗

此倉庫中包含了該工具類詳細的使用教程及各配置項的講解;如果你對它有更好的建議,歡迎反饋

如果你覺得本篇文章不錯,可以留個贊

現在你可以通過

npm install combine-async-error複製代碼

來感受一下如何梭哈嵌套請求

或者查看它的

Git地址 github.com/FuncJin/combine-async-error

combine-async-error的心路歷程到此為止...

作者:FuncJin

https://juejin.cn/post/7121853787794325512

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()