# 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`启动,将自动执行本方法:
![image-20210304093344967](https://img.kancloud.cn/48/cd/48cdf278ef99592125cf62b77bf449eb_912x332.png)
如果想仅仅执行当前方法,则可以在`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();
```
此时,单元测试则将仅仅执行当前方法:
![image-20210304093500558](https://img.kancloud.cn/9d/40/9d401b0b0e3e14effcb936606298d47c_880x178.png)
控制台日志如下:
![image-20210304093549368](https://img.kancloud.cn/ca/8d/ca8dba700abc966092f0b29cd6c13491_882x214.png)
### 接拼认证信息
```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);
}👈
}
```
控制台信息如下:
![image-20210304093954438](https://img.kancloud.cn/e8/c7/e8c7670826eba3114c522b1798cddfba_778x148.png)
由于初始化的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();
});
});
```
![image-20210304094227330](https://img.kancloud.cn/21/ea/21ea9c2100c1019f630ff7b0453d4162_980x252.png)
### 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`函数来快捷的完成加密操作 👈🏻。
![image-20210304095629649](https://img.kancloud.cn/76/dc/76dc93d9d1ce1f83ac5220978551eea1_782x184.png)
### 请求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));
}
}
```
此时单元测试中将触发一个错误,相信你现在有足够的能力把它解决掉,解决以后控制台将打印以下信息:
![image-20210305075729153](https://img.kancloud.cn/4a/96/4a96457295a82884f60a750834bac8b7_1428x216.png)
除使用`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();
});
});
```
![image-20210305080512651](https://img.kancloud.cn/c7/22/c7224d680a59a6682607d4bba48b22b1_1490x178.png)
再验证一下密码错误的情况:
```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();
});
});
```
![image-20210305080512651](https://img.kancloud.cn/c7/22/c7224d680a59a6682607d4bba48b22b1_1490x178.png)
### 中文用户名密码
虽然我个人没有将中文做为用户名密码的习惯,但是部分用户的确有这个需求,那么我们当前代码是否能够很好的处理这种情况呢?
```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();
});
});
```
![image-20210305080955464](https://img.kancloud.cn/cf/10/cf10057c35ba2a758f6c26b9dfaf2bfd_1082x116.png)
我们得到了一个错误,该错误表明当前代码在处理中文用户名时会发生异常。那么处理中文密码是否会发生异常呢,请先给出自己的答案后验证。
当前控制台信息如下:
![image-20210305081630515](https://img.kancloud.cn/85/23/8523b0bffc7759444e03db58377f1e57_580x138.png)
由以上信息我们能够得出,上述异常发生在`btoa()`方法上,用以下关键字来搜索问题,我们可以快速的找到问题的原因及解决方案:
![image-20210305081814242](https://img.kancloud.cn/13/c1/13c1883d70e22a62632c7185f577f66c_1068x150.png)
搜索结果为我们指引到了[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
```
我们当然也可以用中文来做下实验:
![image-20210305083642444](https://img.kancloud.cn/1f/df/1fdf6993d0a663c38498fbe0f9d68518_1156x284.png)
如上所示`zhangsan`中的首字母`z`,转换为10进制后值为`122`,该值位于`0-255`之间,占用一个字节。当然了,实际上我们完全可以在ASSCI编码表中找到字母`z`的编码:
![image-20210305083829485](https://img.kancloud.cn/d6/ee/d6eebdf7320baa5b521d530a6c8574b6_544x132.png)
继续测试中文的`张`:
![image-20210305084048576](https://img.kancloud.cn/f0/87/f0871b575190c26f949215728b945a7e_1264x536.png)
上述代码分别将`张`转换为10 16 2进制,我们能够由16进制的`5f20`快速的得出`张`占用了两个字节,实际上我们还可以在字符[编码相关的站点](https://www.fileformat.info/info/unicode/char/5f20/index.htm)上来快速的找到`张`的utf编码。
![image-20210305085125647](https://img.kancloud.cn/07/04/0704afa089201556f1130700825760f5_1002x158.png)
错误的原因找到了,解决问题的重点便在于如何将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编码的方案。以我们用的百度翻译(类似的例子有很多,基本上涉及到查询都会有)为例:
![image-20210305085523472](https://img.kancloud.cn/23/6d/236dec393f39033c7419e189cac8ad66_1618x516.png)
请跟随教程打开翻译,然后查询一个`你好`,请注意当前的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);
```
![image-20210305095434083](https://img.kancloud.cn/b7/c8/b7c8b84a8d9f04553fccaa9e5a87dd05_800x120.png)
## 本节作业
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 发布部署
- 第九章 总结