引言
基本上當下的應用都會分為前端與后端,當然這種前端定義不在限于桌面瀏覽器仅政、手機、APP等設備。一個良好的后端會通過一套所有前端都通用的 RESTful API 序列接口作為前后端之間的通信蒂誉。
這其中對于身份認證都不可能再依賴傳統(tǒng)的Session或Cookie;轉而使用諸如OAuth2距帅、JWT等這種更適合API接口的認證方式右锨。當然本文并不討論如何去構建它們。
一碌秸、API 設計
首先雖然并不會討論身份認證的技術绍移,但不管是OAuth2還是JWT本質上身份認證都全靠一個 Token 來維持悄窃;因此,下面統(tǒng)一以 token 來表示身份認證所需要的值蹂窖。
一套合理的API規(guī)則轧抗,會讓前端編碼更優(yōu)雅。因此恼策,希望在編寫Angular之前鸦致,能與后端相互達成一種“協(xié)議”也很有必要』量可以嘗試從以下幾點進行考慮分唾。
版本號
可以在URL(例:https://demo.com/v1/
)或Header(例:headers: { version: 'v1' }
)中體現,相比較我更喜歡前者的直接绽乔。
業(yè)務節(jié)點
以一個節(jié)點來表示某個業(yè)務,比如:
- 商品
https://demo.com/v1/product/
- 商品SKU
https://demo.com/v1/product/sku/
動作
由HTTP動詞來表示:
-
GET
請求一個商品/product/${ID}
-
POST
新建一個商品/product
-
PUT
修改一個商品/product/${ID}
-
DELETE
刪除一個商品/product/${ID}
統(tǒng)一響應
這一點非常重要碳褒,特別是當我們新建一個商品時,商品的屬性非常多睦授,但如果我們缺少某個屬性時∩径ィ可以使用這樣的一種統(tǒng)一的響應格式:
{
"code": 100, // 0 表示成功
"errors": { // 錯誤明細
"title": "商品名稱必填"
}
}
其中 code
不管成功與否都會有該屬性。
狀態(tài)碼
后端響應一個請求是包括狀態(tài)碼和響應內容淑廊,而每一種狀態(tài)碼又包含著不同的含義逗余。
-
200
成功返回請求數據 -
401
無權限 -
404
無效資源
二、如何訪問Http季惩?
首先录粱,需要導入 HttpClientModule
模塊。
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule
]
})
然后画拾,在組件類注入 HttpClient
关摇。
export class IndexComponent {
constructor(private http: HttpClient) { }
}
最后,請求點擊某個按鈕發(fā)送一次GET請求碾阁。
user: Observable<User>;
getUser() {
this.user = this.http.get<User>('/assets/data/user.json');
}
打印結果:
{{ user | async | json }}
三個簡單的步驟输虱,就是一個完整的HTTP請求步驟。
然后脂凶,現實與實際是有一些距離宪睹,比如說身份認證愁茁、錯誤處理、狀態(tài)碼處理等問題亭病,在上面并無任何體現鹅很。
可,上面已經足夠優(yōu)雅罪帖,要讓我破壞這種優(yōu)雅那么此文就變得無意義了促煮!
因此……
三、攔截器
1整袁、HttpInterceptor
接口
正如其名菠齿,我們在不改變上面應用層面的代碼下,允許我們把身份認證坐昙、錯誤處理绳匀、狀態(tài)碼處理問題給解決了!
寫一個攔截器也是非常的優(yōu)雅炸客,只需要實現 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
表示調用下一個“中間件”。
2抖所、身份認證
req
有一個 clone
方法,允許對當前的請求參數進行克隆并且這一過程會自行根據一些參數推導痕囱,不管如何用它來產生一個新的請求數據田轧,并在這個新數據中加入我們期望的數據,比如:token鞍恢。
const jwtReq = req.clone({
headers: req.headers.set('token', 'xxxxxxxxxxxxxxxxxxxxx')
});
當然傻粘,你可以再折騰更多請求前的一些配置。
最后帮掉,把新請求參數傳遞給下一個“中間件”弦悉。
return next.handle(jwtReq);
等等,都 return
了蟆炊,說好的狀態(tài)碼稽莉、異常處理呢?
3涩搓、異常處理
仔細再瞧 next.handle
返回的是一個 Observable
類型污秆∨恚看到 Observable
我們會想到什么?flatMap
良拼、catch
等一大堆東西战得。
因此,我們可以利用這些操作符來改變響應的值庸推。
flatMap
請求過程中會會有一些過程狀態(tài)常侦,比如請求前、上傳進度條贬媒、請求結束等聋亡,Angular在每一次這類動作中都會觸次 next
。因此掖蛤,我們只需要在返回 Observable
對象加上 flatMap
來觀察這些值的變更杀捻,這樣有非常大的自由空間想象。
return next.handle(jwtReq).flatMap((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請求已經成功。
這里器赞,統(tǒng)一對業(yè)務層級的錯誤 code !== 0
產生一個錯誤信號的 Observable
垢袱。反之,產生一個成功的信息港柜。
catch
catch
來捕獲非200以外的其他狀態(tài)碼的錯誤请契,比如:401。同時夏醉,前面的 flatMap
所產生的錯誤信號爽锥,也會在這里被捕獲到。
.catch((res: HttpResponse<any>) => {
switch (res.status) {
case 401:
// 權限處理
location.href = ''; // 重新登錄
break;
case 200:
// 業(yè)務層級錯誤處理
alert('業(yè)務錯誤:' + res.body.code);
break;
case 404:
alert('API不存在');
break;
}
return Observable.throw(res);
})
4畔柔、完整代碼
至此氯夷,攔截器所要包括的身份認證token、統(tǒ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)
.flatMap((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:
// 業(yè)務層級錯誤處理
this.notifySrv.error('業(yè)務錯誤', `錯誤代碼為:${res.body.code}`);
break;
case 404:
this.notifySrv.error('404', `API不存在`);
break;
}
// 以錯誤的形式結束本次請求
return Observable.throw(res);
})
}
}
發(fā)現沒有,我們并沒有加一大堆并不認識的事物玄捕,單純都只是對數據流的各種操作而已踩蔚。
NotifyService 是一個無須依賴HTML模板、極簡Angular通知組件枚粘。
5馅闽、注冊攔截器
攔截器構建后,還需要將其注冊至 HTTP_INTERCEPTORS
標識符中。
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
HttpClientModule
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true}
]
})
以上是攔截器的所有內容捞蛋,在不改變原有的代碼的情況下孝冒,我們只是利用短短幾行的代碼實現了身份認證所需要的TOKEN、業(yè)務級統(tǒng)一響應處理拟杉、錯誤處理動作庄涡。
四、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>
恩球凰!
五、結論
Angular在HTTP請求過程中使用 Observable
異步數據流控制數據腿宰,而利用 rxjs 提供的大量操作符呕诉,來改變最終值;從而獲得在應用層面最優(yōu)雅的編碼風格吃度。
當我們說到優(yōu)雅使用HTTP這件事時甩挫,易測試是一個非常重要,因此椿每,我建議將HTTP從組件類中剝離并將所有請求放到 Service 當中伊者。當對某個組件編寫測試代碼時,如果受到HTTP請求結果的限制會讓測試更困難间护。
Happy coding!