詳解如何在Angular優(yōu)雅編寫HTTP請(qǐng)求
引言
基本上當(dāng)下的應(yīng)用都會(huì)分為前端與后端,當(dāng)然這種前端定義不在限于桌面瀏覽器、手機(jī)、APP等設(shè)備。一個(gè)良好的后端會(huì)通過一套所有前端都通用的 RESTful API 序列接口作為前后端之間的通信。
這其中對(duì)于身份認(rèn)證都不可能再依賴傳統(tǒng)的Session或Cookie;轉(zhuǎn)而使用諸如OAuth2、JWT等這種更適合API接口的認(rèn)證方式。當(dāng)然本文并不討論如何去構(gòu)建它們。
一、API 設(shè)計(jì)
首先雖然并不會(huì)討論身份認(rèn)證的技術(shù),但不管是OAuth2還是JWT本質(zhì)上身份認(rèn)證都全靠一個(gè) Token 來維持;因此,下面統(tǒng)一以 token 來表示身份認(rèn)證所需要的值。
一套合理的API規(guī)則,會(huì)讓前端編碼更優(yōu)雅。因此,希望在編寫Angular之前,能與后端相互達(dá)成一種“協(xié)議”也很有必要??梢試L試從以下幾點(diǎn)進(jìn)行考慮。
版本號(hào)
可以在URL(例:https://demo.com/v1/)或Header(例:headers: { version: 'v1' }
)中體現(xiàn),相比較我更喜歡前者的直接。
業(yè)務(wù)節(jié)點(diǎn)
以一個(gè)節(jié)點(diǎn)來表示某個(gè)業(yè)務(wù),比如:
- 商品 https://demo.com/v1/product/
- 商品SKU https://demo.com/v1/product/sku/
動(dòng)作
由HTTP動(dòng)詞來表示:
- GET 請(qǐng)求一個(gè)商品 /product/${ID}
- POST 新建一個(gè)商品 /product
- PUT 修改一個(gè)商品 /product/${ID}
- DELETE 刪除一個(gè)商品 /product/${ID}
統(tǒng)一響應(yīng)
這一點(diǎn)非常重要,特別是當(dāng)我們新建一個(gè)商品時(shí),商品的屬性非常多,但如果我們?nèi)鄙倌硞€(gè)屬性時(shí)??梢允褂眠@樣的一種統(tǒng)一的響應(yīng)格式:
{ "code": 100, // 0 表示成功 "errors": { // 錯(cuò)誤明細(xì) "title": "商品名稱必填" } }
其中 code
不管成功與否都會(huì)有該屬性。
狀態(tài)碼
后端響應(yīng)一個(gè)請(qǐng)求是包括狀態(tài)碼和響應(yīng)內(nèi)容,而每一種狀態(tài)碼又包含著不同的含義。
- 200 成功返回請(qǐng)求數(shù)據(jù)
- 401 無權(quán)限
- 404 無效資源
二、如何訪問Http?
首先,需要導(dǎo)入 HttpClientModule 模塊。
import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ HttpClientModule ] })
然后,在組件類注入 HttpClient。
export class IndexComponent { constructor(private http: HttpClient) { } }
最后,請(qǐng)求點(diǎn)擊某個(gè)按鈕發(fā)送一次GET請(qǐng)求。
user: Observable<User>; getUser() { this.user = this.http.get<User>('/assets/data/user.json'); }
打印結(jié)果:
{{ user | async | json }}
三個(gè)簡單的步驟,就是一個(gè)完整的HTTP請(qǐng)求步驟。
然后,現(xiàn)實(shí)與實(shí)際是有一些距離,比如說身份認(rèn)證、錯(cuò)誤處理、狀態(tài)碼處理等問題,在上面并無任何體現(xiàn)。
可,上面已經(jīng)足夠優(yōu)雅,要讓我破壞這種優(yōu)雅那么此文就變得無意義了!
因此……
三、攔截器
1、HttpInterceptor 接口
正如其名,我們?cè)诓桓淖兩厦鎽?yīng)用層面的代碼下,允許我們把身份認(rèn)證、錯(cuò)誤處理、狀態(tài)碼處理問題給解決了!
寫一個(gè)攔截器也是非常的優(yōu)雅,只需要實(shí)現(xiàn) HttpInterceptor
接口即可,而且只有一個(gè) intercept
方法。
@Injectable() export class JWTInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> { // doing } }
intercept
方法有兩個(gè)參數(shù),它幾乎所當(dāng)下流行的中間件概念一般,req
表示當(dāng)前請(qǐng)求數(shù)據(jù)(包括:url、參數(shù)、header等),next
表示調(diào)用下一個(gè)“中間件”。
2、身份認(rèn)證
req
有一個(gè) clone
方法,允許對(duì)當(dāng)前的請(qǐng)求參數(shù)進(jìn)行克隆并且這一過程會(huì)自行根據(jù)一些參數(shù)推導(dǎo),不管如何用它來產(chǎn)生一個(gè)新的請(qǐng)求數(shù)據(jù),并在這個(gè)新數(shù)據(jù)中加入我們期望的數(shù)據(jù),比如:token。
const jwtReq = req.clone({ headers: req.headers.set('token', 'xxxxxxxxxxxxxxxxxxxxx') });
當(dāng)然,你可以再折騰更多請(qǐng)求前的一些配置。
最后,把新請(qǐng)求參數(shù)傳遞給下一個(gè)“中間件”。
return next.handle(jwtReq);
等等,都 return
了,說好的狀態(tài)碼、異常處理呢?
3、異常處理
仔細(xì)再瞧 next.handle
返回的是一個(gè) Observable
類型。看到 Observable
我們會(huì)想到什么?mergeMap、catch
等一大堆東西。
因此,我們可以利用這些操作符來改變響應(yīng)的值。
mergeMap
請(qǐng)求過程中會(huì)會(huì)有一些過程狀態(tài),比如請(qǐng)求前、上傳進(jìn)度條、請(qǐng)求結(jié)束等,Angular在每一次這類動(dòng)作中都會(huì)觸次 next。因此,我們只需要在返回 Observable
對(duì)象加上 mergeMap
來觀察這些值的變更,這樣有非常大的自由空間想象。
return next.handle(jwtReq).mergeMap((event: any) => { if (event instanceof HttpResponse && event.body.code !== 0) { return Observable.create(observer => observer.error(event)); } return Observable.create(observer => observer.next(event)); })
只會(huì)在請(qǐng)求成功才會(huì)返回一個(gè) HttpResponse 類型,因此,我們可以大膽判斷是否來源于 HttpResponse 來表示HTTP請(qǐng)求已經(jīng)成功。
這里,統(tǒng)一對(duì)業(yè)務(wù)層級(jí)的錯(cuò)誤 code !== 0 產(chǎn)生一個(gè)錯(cuò)誤信號(hào)的 Observable。反之,產(chǎn)生一個(gè)成功的信息。
catch
catch 來捕獲非200以外的其他狀態(tài)碼的錯(cuò)誤,比如:401。同時(shí),前面的 mergeMap 所產(chǎn)生的錯(cuò)誤信號(hào),也會(huì)在這里被捕獲到。
.catch((res: HttpResponse<any>) => { switch (res.status) { case 401: // 權(quán)限處理 location.href = ''; // 重新登錄 break; case 200: // 業(yè)務(wù)層級(jí)錯(cuò)誤處理 alert('業(yè)務(wù)錯(cuò)誤:' + res.body.code); break; case 404: alert('API不存在'); break; } return Observable.throw(res); })
4、完整代碼
至此,攔截器所要包括的身份認(rèn)證token、統(tǒng)一響應(yīng)處理、異常處理都解決了。
@Injectable() export class JWTInterceptor implements HttpInterceptor { constructor(private notifySrv: NotifyService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> { console.log('interceptor') const jwtReq = req.clone({ headers: req.headers.set('token', 'asdf') }); return next .handle(jwtReq) .mergeMap((event: any) => { if (event instanceof HttpResponse && event.body.code !== 0) { return Observable.create(observer => observer.error(event)); } return Observable.create(observer => observer.next(event)); }) .catch((res: HttpResponse<any>) => { switch (res.status) { case 401: // 權(quán)限處理 location.href = ''; // 重新登錄 break; case 200: // 業(yè)務(wù)層級(jí)錯(cuò)誤處理 this.notifySrv.error('業(yè)務(wù)錯(cuò)誤', `錯(cuò)誤代碼為:${res.body.code}`); break; case 404: this.notifySrv.error('404', `API不存在`); break; } // 以錯(cuò)誤的形式結(jié)束本次請(qǐng)求 return Observable.throw(res); }) } }
發(fā)現(xiàn)沒有,我們并沒有加一大堆并不認(rèn)識(shí)的事物,單純都只是對(duì)數(shù)據(jù)流的各種操作而已。
NotifyService 是一個(gè)無須依賴HTML模板、極簡Angular通知組件。
5、注冊(cè)攔截器
攔截器構(gòu)建后,還需要將其注冊(cè)至 HTTP_INTERCEPTORS 標(biāo)識(shí)符中。
import { HttpClientModule } from '@angular/common/http'; @NgModule({ imports: [ HttpClientModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true} ] })
以上是攔截器的所有內(nèi)容,在不改變?cè)械拇a的情況下,我們只是利用短短幾行的代碼實(shí)現(xiàn)了身份認(rèn)證所需要的TOKEN、業(yè)務(wù)級(jí)統(tǒng)一響應(yīng)處理、錯(cuò)誤處理動(dòng)作。
四、async 管道
一個(gè) Observable
必須被訂閱以后才會(huì)真正的開始動(dòng)作,前面在HTML模板中我們利用了 async
管道簡化了這種訂閱過程。
{{ user | async | json }}
它相當(dāng)于:
let user: User; get() { this.http.get<User>('/assets/data/user.json').subscribe(res => { this.user = res; }); } {{ user | json }}
然而,async 這種簡化,并不代表失去某些自由度,比如說當(dāng)在獲取數(shù)據(jù)過程中顯示【加載中……】,怎么辦?
<div *ngIf="user | async as user; else loading"> {{ user | json }} </div> <ng-template #loading>加載中……</ng-template>
恩!
五、結(jié)論
Angular在HTTP請(qǐng)求過程中使用 Observable 異步數(shù)據(jù)流控制數(shù)據(jù),而利用 rxjs 提供的大量操作符,來改變最終值;從而獲得在應(yīng)用層面最優(yōu)雅的編碼風(fēng)格。
當(dāng)我們說到優(yōu)雅使用HTTP這件事時(shí),易測(cè)試是一個(gè)非常重要,因此,我建議將HTTP從組件類中剝離并將所有請(qǐng)求放到 Service 當(dāng)中。當(dāng)對(duì)某個(gè)組件編寫測(cè)試代碼時(shí),如果受到HTTP請(qǐng)求結(jié)果的限制會(huì)讓測(cè)試更困難。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Angularjs中$http以post請(qǐng)求通過消息體傳遞參數(shù)的實(shí)現(xiàn)方法
- 詳解AngularJS中$http緩存以及處理多個(gè)$http請(qǐng)求的方法
- 淺談angular2的http請(qǐng)求返回結(jié)果的subcribe注意事項(xiàng)
- AngularJS $http模塊POST請(qǐng)求實(shí)現(xiàn)
- 在 Angular6 中使用 HTTP 請(qǐng)求服務(wù)端數(shù)據(jù)的步驟詳解
- angular 用攔截器統(tǒng)一處理http請(qǐng)求和響應(yīng)的方法
- angular2中Http請(qǐng)求原理與用法詳解
- 解決angularjs中同步執(zhí)行http請(qǐng)求的方法
- Angular6封裝http請(qǐng)求的步驟詳解
- angular2實(shí)現(xiàn)統(tǒng)一的http請(qǐng)求頭方法
相關(guān)文章
微信小程序?qū)崿F(xiàn)左右聯(lián)動(dòng)的實(shí)戰(zhàn)記錄
聯(lián)動(dòng)菜單是大家在開發(fā)小程序經(jīng)常會(huì)遇到的一個(gè)功能,下面這篇文章主要給大家介紹了關(guān)于微信小程序?qū)崿F(xiàn)左右聯(lián)動(dòng)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07angularjs實(shí)現(xiàn)上拉加載和下拉刷新數(shù)據(jù)功能
本篇文章主要介紹了angularjs實(shí)現(xiàn)上拉加載和下拉刷新數(shù)據(jù)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06AngularJS實(shí)現(xiàn)自定義指令及指令配置項(xiàng)的方法
這篇文章主要介紹了AngularJS實(shí)現(xiàn)自定義指令及指令配置項(xiàng)的方法,結(jié)合實(shí)例形式簡單總結(jié)分析了AngularJS自定義指令及指令配置項(xiàng)的實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-11-11前后端如何實(shí)現(xiàn)登錄token攔截校驗(yàn)詳解
這篇文章主要給大家介紹了關(guān)于前后端如何實(shí)現(xiàn)登錄token攔截校驗(yàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09舉例詳解AngularJS中ngShow和ngHide的使用方法
這篇文章主要介紹了舉例詳解AngularJS中ngShow和ngHide的使用方法,AngularJS是一款非常熱門的JavaScript框架,需要的朋友可以參考下2015-06-06AngularJS實(shí)現(xiàn)數(shù)據(jù)列表的增加、刪除和上移下移等功能實(shí)例
這篇文章給大家分享了AngularJS循環(huán)實(shí)現(xiàn)數(shù)據(jù)列表的增加、刪除和上移下移等基礎(chǔ)功能,對(duì)大家學(xué)習(xí)AngularJS具有一定的參考借鑒價(jià)值,有需要的朋友可以看看。2016-09-09深究AngularJS如何獲取input的焦點(diǎn)(自定義指令)
本篇文章主要介紹了AngularJS如何獲取input的焦點(diǎn)(自定義指令),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06