如何在Angular優雅編寫HTTP請求

引言

基本上當下的應用都會分爲前端與後端,固然這種前端定義不在限於桌面瀏覽器、手機、APP等設備。一個良好的後端會經過一套全部前端都通用的 RESTful API 序列接口做爲先後端之間的通訊。javascript

這其中對於身份認證都不可能再依賴傳統的Session或Cookie;轉而使用諸如OAuth二、JWT等這種更適合API接口的認證方式。固然本文並不討論如何去構建它們。html

1、API 設計

首先雖然並不會討論身份認證的技術,但不論是OAuth2仍是JWT本質上身份認證都全靠一個 Token 來維持;所以,下面統一以 token 來表示身份認證所須要的值。前端

一套合理的API規則,會讓前端編碼更優雅。所以,但願在編寫Angular以前,能與後端相互達成一種「協議」也頗有必要。能夠嘗試從如下幾點進行考慮。java

版本號git

能夠在URL(例:https://demo.com/v1/)或Header(例:headers: { version: 'v1' })中體現,相比較我更喜歡前者的直接。github

業務節點typescript

以一個節點來表示某個業務,好比:json

  • 商品 https://demo.com/v1/product/
  • 商品SKU https://demo.com/v1/product/sku/

動做後端

由HTTP動詞來表示:瀏覽器

  • GET 請求一個商品 /product/${ID}
  • POST 新建一個商品 /product
  • PUT 修改一個商品 /product/${ID}
  • DELETE 刪除一個商品 /product/${ID}

統一響應

這一點很是重要,特別是當咱們新建一個商品時,商品的屬性很是多,但若是咱們缺乏某個屬性時。可使用這樣的一種統一的響應格式:

{
    "code": 100, // 0 表示成功
    "errors": { // 錯誤明細
        "title": "商品名稱必填"
    }
}

其中 code 無論成功與否都會有該屬性。

狀態碼

後端響應一個請求是包括狀態碼和響應內容,而每一種狀態碼又包含着不一樣的含義。

  • 200 成功返回請求數據
  • 401 無權限
  • 404 無效資源

2、如何訪問Http?

首先,須要導入 HttpClientModule 模塊。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
    imports: [
        HttpClientModule
    ]
})

而後,在組件類注入 HttpClient

export class IndexComponent {
    constructor(private http: HttpClient) { }
}

最後,請求點擊某個按鈕發送一次GET請求。

user: Observable<User>;
getUser() {
    this.user = this.http.get<User>('/assets/data/user.json');
}

打印結果:

{{ user | async | json }}

三個簡單的步驟,就是一個完整的HTTP請求步驟。

而後,現實與實際是有一些距離,好比說身份認證、錯誤處理、狀態碼處理等問題,在上面並沒有任何體現。

可,上面已經足夠優雅,要讓我破壞這種優雅那麼此文就變得無心義了!

所以……

3、攔截器

一、HttpInterceptor 接口

正如其名,咱們在不改變上面應用層面的代碼下,容許咱們把身份認證、錯誤處理、狀態碼處理問題給解決了!

寫一個攔截器也是很是的優雅,只須要實現 HttpInterceptor 接口便可,並且只有一個 intercept 方法。

@Injectable()
export class JWTInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
        // doing
    }

}

intercept 方法有兩個參數,它幾乎所當下流行的中間件概念通常,req 表示當前請求數據(包括:url、參數、header等),next 表示調用下一個「中間件」。

二、身份認證

req 有一個 clone 方法,容許對當前的請求參數進行克隆而且這一過程會自行根據一些參數推導,無論如何用它來產生一個新的請求數據,並在這個新數據中加入咱們指望的數據,好比:token。

const jwtReq = req.clone({
    headers: req.headers.set('token', 'xxxxxxxxxxxxxxxxxxxxx')
});

固然,你能夠再折騰更多請求前的一些配置。

最後,把新請求參數傳遞給下一個「中間件」。

return next.handle(jwtReq);

等等,都 return 了,說好的狀態碼、異常處理呢?

三、異常處理

仔細再瞧 next.handle 返回的是一個 Observable 類型。看到 Observable 咱們會想到什麼?mergeMapcatch 等一大堆東西。

所以,咱們能夠利用這些操做符來改變響應的值。

mergeMap

請求過程當中會會有一些過程狀態,好比請求前、上傳進度條、請求結束等,Angular在每一次這類動做中都會觸次 next。所以,咱們只須要在返回 Observable 對象加上 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));
    })

只會在請求成功纔會返回一個 HttpResponse 類型,所以,咱們能夠大膽判斷是否來源於 HttpResponse 來表示HTTP請求已經成功。

這裏,統一對業務層級的錯誤 code !== 0 產生一個錯誤信號的 Observable。反之,產生一個成功的信息。

catch

catch 來捕獲非200之外的其餘狀態碼的錯誤,好比:401。同時,前面的 mergeMap 所產生的錯誤信號,也會在這裏被捕獲到。

.catch((res: HttpResponse<any>) => {
    switch (res.status) {
        case 401:
            // 權限處理
            location.href = ''; // 從新登陸
            break;
        case 200:
            // 業務層級錯誤處理
            alert('業務錯誤:' + res.body.code);
            break;
        case 404:
            alert('API不存在');
            break;
    }
    return Observable.throw(res);
})

四、完整代碼

至此,攔截器所要包括的身份認證token、統一響應處理、異常處理都解決了。

@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:
                        // 權限處理
                        location.href = ''; // 從新登陸
                        break;
                    case 200:
                        // 業務層級錯誤處理
                        this.notifySrv.error('業務錯誤', `錯誤代碼爲:${res.body.code}`);
                        break;
                    case 404:
                        this.notifySrv.error('404', `API不存在`);
                        break;
                }
                // 以錯誤的形式結束本次請求
                return Observable.throw(res);
            })
    }
}

發現沒有,咱們並無加一大堆並不認識的事物,單純都只是對數據流的各類操做而已。

NotifyService 是一個無須依賴HTML模板、極簡Angular通知組件。

五、註冊攔截器

攔截器構建後,還須要將其註冊至 HTTP_INTERCEPTORS 標識符中。

import { HttpClientModule } from '@angular/common/http';

@NgModule({
    imports: [
        HttpClientModule
    ],
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true}
    ]
})

以上是攔截器的全部內容,在不改變原有的代碼的狀況下,咱們只是利用短短几行的代碼實現了身份認證所須要的TOKEN、業務級統一響應處理、錯誤處理動做。

4、async 管道

一個 Observable 必須被訂閱之後纔會真正的開始動做,前面在HTML模板中咱們利用了 async 管道簡化了這種訂閱過程。

{{ user | async | json }}

它至關於:

let user: User;
get() {
    this.http.get<User>('/assets/data/user.json').subscribe(res => {
        this.user = res;
    });
}
{{ user | json }}

然而,async 這種簡化,並不表明失去某些自由度,好比說當在獲取數據過程當中顯示【加載中……】,怎麼辦?

<div *ngIf="user | async as user; else loading">
    {{ user | json }}
</div>
<ng-template #loading>加載中……</ng-template>

恩!

5、結論

Angular在HTTP請求過程當中使用 Observable 異步數據流控制數據,而利用 rxjs 提供的大量操做符,來改變最終值;從而得到在應用層面最優雅的編碼風格。

當咱們說到優雅使用HTTP這件事時,易測試是一個很是重要,所以,我建議將HTTP從組件類中剝離並將全部請求放到 Service 當中。當對某個組件編寫測試代碼時,若是受到HTTP請求結果的限制會讓測試更困難。

Happy coding!

相關文章
相關標籤/搜索