# Basic access authentication
用戶登錄在術語中更多的稱為用戶認證,英文單詞以authentication為關鍵字,也常常被簡寫為auth。認證的方式有很多種,比如我們常看到的用戶名密碼認證、手機號驗證碼認證、使用微信支付寶等第三方快捷認證等。在此,我們僅講述用戶名密碼的認證方式。
認證的過程也可以有很多種,比如我們歷史上曾經學習過將用戶名、密碼做為表單數據,以post方式發送給過去,繼而完成用戶認證。今天我們學習的是另一種更加通用的認證方式:`Basic access authentication`,有時也被簡稱為`Basic Auth`。
## Basic Auth
Basic Auth,顧名思義其為一種基本的認證模式,它也是最常用的HTTP認證方案。它的基本認證邏輯是:將認證信息放到Http請求的Header部分。
以用戶名為`zhangsan`密碼為`yunzhi.club`為例,使用Basic Auth認證流程如下:
1. 將用戶名密碼與`:`相連,接拼為字符串`zhangsan:yunzhi.club`。
2. 使用base64進行加密 `base64(zhangsan:yunzhi.club)`,加密結果為`emhhbmdzYW46eXVuemhpLmNsdWI=`。
3. 在http請求中的headers中增加以下項:`Authorization: Basic emhhbmdzYW46eXVuemhpLmNsdWI=`
4. 向后臺發起請求
此時,用戶名密碼便成功的通過headers以Basic Auth的模式發送給了后臺。
> 除最常用的Basic認證外,還有**Bearer**、**Digest**、**HOBA**等認證模式。
## 后臺接口
后臺為我們提供了專用的認證地址(實際上并不拘泥于此),接口信息如下:
```bash
GET /teacher/login
```
認證模式:Basic。認證失敗將返回狀態碼401,認證成功將返回用戶名密碼對應的教師數據。
## 發起認證
我們來到login組件的`onSubmit`方法,按Basic Auth的步驟逐步完成代碼。
### 自動化
按前面學習的方法,我們可以利用`ng t`來啟動組件測試,接著點擊登錄中的登錄按扭,以達到調用`onSubmit`的方法。其我們還可以借助單元測試的思想,寫一些自動化的代碼,這樣當我們每次改動代碼并按`ctrl + s`保存文件后,這些代碼便會自動執行。在這些自動執行的代碼中實現**調用onSubmit**的方法。
是的,我們完全可以參考第一節的內容,使用模塊點擊V層按鈕的方法。除此以外,我們還可以在單元測試代碼直接調用組件的方法。為此,我們增加如下代碼以協助開發用戶登錄。
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -39,4 +39,11 @@ fdescribe('LoginComponent', () => {
// 點擊按鈕以后,onSubmit方法應該被調用了1次。
expect(component.onSubmit).toHaveBeenCalledTimes(1);
});
+
+ it('onSubmit 用戶登錄', () => {
+ // 啟動自動變更檢測
+ fixture.autoDetectChanges();
+
+ component.onSubmit();
+ });
});
```
使用`ng t`啟動,將自動執行本方法:

如果想僅僅執行當前方法,則可以在`it`前面加入`f`:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -40,7 +40,7 @@ fdescribe('LoginComponent', () => {
expect(component.onSubmit).toHaveBeenCalledTimes(1);
});
- it('onSubmit 用戶登錄', () => {
+ fit('onSubmit 用戶登錄', () => {
// 啟動自動變更檢測
fixture.autoDetectChanges();
```
此時,單元測試則將僅僅執行當前方法:

控制臺日志如下:

### 接拼認證信息
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -19,5 +19,7 @@ export class LoginComponent implements OnInit {
onSubmit(): void {
console.log('點擊了登錄按鈕');
+ const authString = this.teacher.username + ':' + this.teacher.password;
+ console.log(authString);
}??
}
```
控制臺信息如下:

由于初始化的teacher并不存在用戶名密碼信息,所以最終在控制臺打印了`undefined:undefined`。為此,在單元測試代碼中,我們為`teacher`設置一個用戶名、密碼:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -43,7 +43,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動自動變更檢測
fixture.autoDetectChanges();
-
+ component.teacher = {username: 'zhangsan', password: 'codedemo.club'};
component.onSubmit();
});
});
```

### Base64加密
Base64是眾多加密算法中的一種,最近也被廣泛地用于在瀏覽器中顯示圖片。比如你可以將以下代碼粘貼到html文件中,在對應的位置上將顯示一張圖片:
````
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE4NHB4IiBoZWlnaHQ9IjIwMHB4IiB2aWV3Qm94PSIwIDAgMTg0IDIwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDMuMi4yICg5OTgzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5zaGllbGQtbGFyZ2U8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz48L2RlZnM+CiAgICA8ZyBpZD0iUGFnZS0xIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj4KICAgICAgICA8ZyBpZD0ic2hpZWxkLWxhcmdlIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIj4KICAgICAgICAgICAgPGcgaWQ9IkltcG9ydGVkLUxheWVycyIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC4wMDAwMDAsIDExLjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTgzLjIsMS4wMzI0OTI3NSBMODMuMiwxLjAyODYwMTQ1IEwwLjMwNjgsMzAuNDQ4MTU5NCBMMTIuODk4NiwxMzkuNjczMTgxIEw4Mi45OTk4LDE3OC40NTAwMjkgTDE1NC4wOTgxLDEzOS4xNTA0NDkgTDE2Ny45NTg3LDI5LjkyNjcyNDYgTDgzLjIsMS4wMzI0OTI3NSIgaWQ9IkZpbGwtMSIgZmlsbD0iI0UyMzIzNyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2Ny44Mzc4LDI5LjkyNjcyNDYgTDgyLjk3MTIsMS4wMzI0OTI3NSBMODIuOTcxMiwxNzguNDUwMDI5IEwxNTQuMDgzOCwxMzkuMTUwNDQ5IEwxNjcuODM3OCwyOS45MjY3MjQ2IEwxNjcuODM3OCwyOS45MjY3MjQ2IFoiIGlkPSJGaWxsLTIiIGZpbGw9IiNCNTJFMzEiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0wLjE5MjQsMzAuNDQ4MTU5NCBMMTIuODQxNCwxMzkuNjczMTgxIEw4Mi45NzEyLDE3OC40NTAwMjkgTDgyLjk3MTIsMS4wMjg2MDE0NSBMMC4xOTI0LDMwLjQ0ODE1OTQgTDAuMTkyNCwzMC40NDgxNTk0IFoiIGlkPSJGaWxsLTMiIGZpbGw9IiNFMjMyMzciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMDAuNjgzNyw5NC4yMjY2Mzc3IEw4Mi45NzEyLDU3LjcwNTQ0OTMgTDY3LjcyNjEsOTQuMjI2NjM3NyBMMTAwLjY4MzcsOTQuMjI2NjM3NyBMMTAwLjY4MzcsOTQuMjI2NjM3NyBaIE0xMDcuMzY3LDEwOS41ODMwMjIgTDYwLjkwNSwxMDkuNTgzMDIyIEw1MC41MTE1LDEzNS41MjM3NTQgTDMxLjE3NzksMTM1Ljg3OTE1OSBMODIuOTcxMiwyMC44MTA2OTU3IEwxMzYuNjY2NCwxMzUuODc5MTU5IEwxMTguNzQ1OSwxMzUuODc5MTU5IEwxMDcuMzY3LDEwOS41ODMwMjIgTDEwNy4zNjcsMTA5LjU4MzAyMiBaIiBpZD0iRmlsbC00IiBmaWxsPSIjRkZGRkZGIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==">
````
上面的圖片`src`的部分以`data:image/svg+xml;base64`打頭,即表示使用了base64算法。簡單來講,base64算法一種在加密時將二進制串轉換為ASCII字符串(實際上只選取了部分ASCII),在解密時再將ASCII字符串轉換為二制進的加密解密算法。由于http中的header部分只能夠攜帶ASCII編碼的字符串,所以在沒有base64算法轉換之前。將用戶名、密碼信息放到header中傳遞,則僅支持英文字符;在base64的幫助下,可以將用戶名、密碼轉換為ASCII字符串,近而可以做為header數據項中傳遞。
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -21,5 +21,7 @@ export class LoginComponent implements OnInit {
console.log('點擊了登錄按鈕');
const authString = this.teacher.username + ':' + this.teacher.password;
console.log(authString);
+ const authToken = btoa(authString); ????
+ console.log(authToken);
}
}
```
TypeScript提供了`btoa`函數來快捷的完成加密操作 ????。

### 請求Header
Angular提供了傳用的HttpHeaders用于構建請求的header信息:
```typescript
+++ b/first-app/src/app/login/login.component.ts
-import {HttpHeaders} from '@angular/common/http';
+import {HttpClient, HttpHeaders} from '@angular/common/http';
@@ -24,5 +24,6 @@ export class LoginComponent implements OnInit {
console.log(authString);
const authToken = btoa(authString);
console.log(authToken);
+ let httpHeaders = new HttpHeaders();
+ httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken); ??
}
}
注意是`Basic `不是`Basic`, 前一個存在空格 ??
```
### 發起請求
然后便可以在httpClient的任意方法中加入此header請求信息:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -1,5 +1,5 @@
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-login',
@@ -12,7 +12,7 @@ export class LoginComponent implements OnInit {
password: string
};
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
ngOnInit(): void {
@@ -26,5 +26,12 @@ export class LoginComponent implements OnInit {
console.log(authToken);
let httpHeaders = new HttpHeaders();
httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);
+
+ this.httpClient
+ .get(
+ 'http://angular.api.codedemo.club:81/teacher/login',
+ {headers: httpHeaders})
+ .subscribe(teacher => console.log(teacher),
+ error => console.log('發生錯誤, 登錄失敗', error));
}
}
```
此時單元測試中將觸發一個錯誤,相信你現在有足夠的能力把它解決掉,解決以后控制臺將打印以下信息:

除使用`get`方法外,還可以使用`put`、`post`、`delete`等請求方式,比如:
```typescript
this.httpClient.post(url, {}, {headers: httpHeaders})
```
**注意:**我們的后臺每日將清空一次數據,對所有的成員開放后臺API,這意味著當前正在有其它的學員進行教師編輯功能的練習。這會使得用戶名`zhangsan`處于失效狀態(比如有學員將zhangsan改成了zhangsanfeng)。你可以在瀏覽器中直接訪問[http://angular.api.codedemo.club:81/teacher](http://angular.api.codedemo.club:81/teacher)來獲取當前有效的用戶名信息,系統默認用戶的密碼均為`codedemo.club` 。
## 充分的測試
一個優秀的項目離不開充分的測試,測試是保障軟件質量最重要的一環,沒有之一。在測試中,我們需要充分的站在用戶的角度,根據自己的經驗努力思索用戶在實際使用過程中可能會出現的情景,然后一一把它們模擬出來。
### 用戶名密碼錯誤
前面我們僅驗證了用戶名、密碼正確的情況。在實際的使用過程中顯然這是不夠的。而用戶名、密碼錯誤時是否是按我們的預期發起的呢?與其猜、想、看、盯,不如實際用代碼測試一下:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動自動變更檢測
fixture.autoDetectChanges();
- component.teacher = {username: 'zhangsan', password: 'codedemo.club'};
+ component.teacher = {username: 'notzhangsan', password: 'codedemo.club'};
component.onSubmit();
});
});
```

再驗證一下密碼錯誤的情況:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟用自動變更檢測
fixture.autoDetectChanges();
- component.teacher = {username: 'notzhangsan', password: 'codedemo.club'};
+ component.teacher = {username: 'zhangsan', password: 'password'};
component.onSubmit();
});
});
```

### 中文用戶名密碼
雖然我個人沒有將中文做為用戶名密碼的習慣,但是部分用戶的確有這個需求,那么我們當前代碼是否能夠很好的處理這種情況呢?
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -45,7 +45,7 @@ fdescribe('LoginComponent', () => {
fit('onSubmit 用戶登錄', () => {
// 啟動自動變更檢測
fixture.autoDetectChanges();
- component.teacher = {username: 'zhangsan', password: 'password'};
+ component.teacher = {username: '中文用戶名', password: 'codedemo.club'};
component.onSubmit();
});
});
```

我們得到了一個錯誤,該錯誤表明當前代碼在處理中文用戶名時會發生異常。那么處理中文密碼是否會發生異常呢,請先給出自己的答案后驗證。
當前控制臺信息如下:

由以上信息我們能夠得出,上述異常發生在`btoa()`方法上,用以下關鍵字來搜索問題,我們可以快速的找到問題的原因及解決方案:

搜索結果為我們指引到了[https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings](https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings)一文,該文的回答又為我們指引到了權威的[https://developer.mozilla.org/en-US/docs/Glossary/Base64](https://developer.mozilla.org/en-US/docs/Glossary/Base64)在此文章中,有一行note是這么說的:
```
Note that btoa() expects to be passed binary data, and will throw an exception if the given string contains any characters whose UTF-16 representation occupies more than one byte. For more details, see the documentation for btoa().
```
上面大概是說:
```
注意btoa()方法只能傳入二進制數據,如果傳入的參數中包含任何UTF-16的大于1個字節的字符串,將會觸發異常。
```
我們接著點擊函數名,查看詳情[https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa),該文中又有如下的描述:
```
The btoa() function takes a JavaScript string as a parameter. In JavaScript strings are represented using the UTF-16 character encoding: in this encoding, strings are represented as a sequence of 16-bit (2 byte) units. Every ASCII character fits into the first byte of one of these units, but many other characters don't.
```
簡單翻譯下我們大概明白了,原來btoa只能接收以1個字節的字符組成的字符串。而JavaScript的string是用UTF-16來編碼的,該編碼占用了2個字節。每個ASCII編碼的字符都可以用首單元的字節來代碼,但是其它的字符就不是了(言外之意,其它字符就是2個字節了)。
這就需要我們在C語言、數據結構、計算機組成原理等基礎課程中學習過的ASCII編碼了。ASCII編碼中,0 - 127分別代表一個字符,共128個。占用了一個字節的后7位,為:`0000 0000` 至 `0111 1111`。所以每個ASCII編碼的字符,必然可以用一個字節來表示。
在[WindowOrWorkerGlobalScope.btoa()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa)一文中,我們還可以找到相應測試的示例代碼:
```typescript
const ok = "a";
console.log(ok.codePointAt(0).toString(16)); // 61: occupies < 1 byte
const notOK = "?"
console.log(notOK.codePointAt(0).toString(16)); // 2713: occupies > 1 byte
console.log(btoa(ok)); // YQ==
console.log(btoa(notOK)); // error
```
我們當然也可以用中文來做下實驗:

如上所示`zhangsan`中的首字母`z`,轉換為10進制后值為`122`,該值位于`0-255`之間,占用一個字節。當然了,實際上我們完全可以在ASSCI編碼表中找到字母`z`的編碼:

繼續測試中文的`張`:

上述代碼分別將`張`轉換為10 16 2進制,我們能夠由16進制的`5f20`快速的得出`張`占用了兩個字節,實際上我們還可以在字符[編碼相關的站點](https://www.fileformat.info/info/unicode/char/5f20/index.htm)上來快速的找到`張`的utf編碼。

錯誤的原因找到了,解決問題的重點便在于如何將UTF-16中占2個字節的編碼變換為變1個字節的ASSCI。
### 單元大小為1字節的字符串
mozilla給出了如何將多字節字符組成的字符串變為1個字符組成的字符串的方案:
```javascript
// convert a Unicode string to a string in which
// each 16-bit unit occupies only one byte
function toBinary(string) {
const codeUnits = new Uint16Array(string.length);
for (let i = 0; i < codeUnits.length; i++) {
codeUnits[i] = string.charCodeAt(i);
}
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
}
```
參數上述代碼,建立轉換方法如下:
### encodeURI
既然已經擴展到此程度了,我們不防再多擴展一下。其實我們早早的就接觸到了這種將非ASSCI編碼轉換為ASSCI編碼的方案。以我們用的百度翻譯(類似的例子有很多,基本上涉及到查詢都會有)為例:

請跟隨教程打開翻譯,然后查詢一個`你好`,請注意當前的URL。接下來,我們復制這個URL,然后再粘貼到任意的位置,你將得到如下鏈接:
```
https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD
```
如果你在瀏覽器中打開[https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD](https://fanyi.baidu.com/#zh/en/%E4%BD%A0%E5%A5%BD),同樣可以正常訪問顯示為你好。
將這個`你好`變更為`%E4%BD%A0%E5%A5%BD`的過程稱為`encodeURI`,表示對URI進行編碼。目的是適用于http協議中非主體部分只支持ASSCI編碼的規則。`encodeURIComponent`函數則可以實現此功能。
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -20,7 +20,7 @@ export class LoginComponent implements OnInit {
onSubmit(): void {
console.log('點擊了登錄按鈕');
- const authString = this.teacher.username + ':' + this.teacher.password;
+ const authString = encodeURIComponent(this.teacher.username) + ':' + this.teacher.password;
console.log(authString);
const authToken = btoa(authString);
console.log(authToken);
```

## 本節作業
1. 一個項目前后臺是統一的整體,我們剛剛在傳送用戶名密碼時增加了encodeURI轉碼,那么后臺是否也支持這種方式呢?請新創建一個新教師并使用`codedemo.club`做為用戶名嘗試登錄。
2. 如果我們想使密碼也支持中文的話該怎么辦呢?
3. 我們往往怕的是修改好了一個新功能,同時卻改壞了兩個老功能。中文用戶名的問題解決了,那么是否還支持英文登錄呢?請測試。
4. 請思索:在團隊開發中,如何保證你已有的功能不被其它團隊成員誤殺。
| 名稱 | 地址 | 備注 |
| ---------------------- | ------------------------------------------------------------ | ---- |
| Http身份認證 | [https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication) | |
| RFC7617 Basic認證 | [https://tools.ietf.org/html/rfc7617](https://tools.ietf.org/html/rfc7617) | |
| Base64的編碼與解碼 | [https://developer.mozilla.org/zh-CN/docs/Glossary/Base64](https://developer.mozilla.org/zh-CN/docs/Glossary/Base64) | |
| 一個查詢字符編碼的網站 | [https://www.fileformat.info/info/unicode/char/68a6/index.htm](https://www.fileformat.info/info/unicode/char/68a6/index.htm) | |
| ASCII | [https://zh.wikipedia.org/wiki/ASCII](https://zh.wikipedia.org/wiki/ASCII) | |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step3.3.zip](https://github.com/mengyunzhi/angular11-guild/archive/step3.3.zip) | |
- 序言
- 第一章 Hello World
- 1.1 環境安裝
- 1.2 Hello Angular
- 1.3 Hello World!
- 第二章 教師管理
- 2.1 教師列表
- 2.1.1 初始化原型
- 2.1.2 組件生命周期之初始化
- 2.1.3 ngFor
- 2.1.4 ngIf、ngTemplate
- 2.1.5 引用 Bootstrap
- 2.2 請求后臺數據
- 2.2.1 HttpClient
- 2.2.2 請求數據
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調函數
- 2.2.5 集成測試
- 2.2.6 本章小節
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對接后臺
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數
- 2.4.3 插值與模板表達式
- 2.4.4 初識泛型
- 2.4.5 更新教師
- 2.4.6 測試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標庫
- 2.6.3 firefox
- 2.7 總結
- 第三章 用戶登錄
- 3.1 初識單元測試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結
- 第四章 個人中心
- 4.1 原型
- 4.2 管道
- 4.3 對接后臺
- 4.4 x-auth-token認證
- 4.5 攔截器
- 4.6 小結
- 第五章 系統菜單
- 5.1 延遲及測試
- 5.2 手動創建組件
- 5.3 隱藏測試信息
- 5.4 規劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結
- 第六章 班級管理
- 6.1 新增班級
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級
- 6.1.3 ApiInterceptor
- 6.1.4 數據驗證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構
- 6.1.8 小結
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應用組件
- 6.2.5 小結
- 6.3 班級列表
- 6.3.1 原型設計
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態分頁
- 6.3.5 動態分頁
- 6.3.6 @Input()
- 6.4 編輯班級
- 6.4.1 測試模塊
- 6.4.2 響應式表單驗證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構
- 6.4.7 小結
- 6.5 刪除班級
- 6.6 集成測試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉
- 6.6.4 ngStyle
- 6.7 初識Service
- 6.7.1 catchError
- 6.7.2 單例服務
- 6.7.3 單元測試
- 6.8 小結
- 第七章 學生管理
- 7.1 班級列表組件
- 7.2 新增學生
- 7.2.1 exports
- 7.2.2 自定義驗證器
- 7.2.3 異步驗證器
- 7.2.4 再識DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結
- 7.3 單元測試進階
- 7.4 學生列表
- 7.4.1 JSON對象與對象
- 7.4.2 單元測試
- 7.4.3 分頁模塊
- 7.4.4 子組件測試
- 7.4.5 重構分頁
- 7.5 刪除學生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向對象
- 7.6 集成測試
- 7.7 編輯學生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學生
- 7.7.4 集成測試
- 7.7.5 可訂閱的路由參數
- 7.7.6 小結
- 7.8 總結
- 第八章 其它
- 8.1 打包構建
- 8.2 發布部署
- 第九章 總結