# 延遲
當(dāng)前用戶登錄失敗只是顯示在控制臺(tái),對(duì)用戶而言這是極不友好的。 本我們實(shí)現(xiàn):當(dāng)用戶登錄失敗時(shí),顯示一個(gè)持續(xù)1.5S的錯(cuò)誤提示。
## 初始化
首先我們?cè)赩層增加提示信息:
```html
+++ b/first-app/src/app/login/login.component.html
@@ -11,5 +11,8 @@
<input type="password" class="form-control" id="exampleInputPassword1"
[(ngModel)]="teacher.password" name="password">
</div>
+ <div class="mb-3">
+ <p class="alert alert-danger" *ngIf="showError">用戶名或密碼錯(cuò)誤,請(qǐng)重新輸入!</p>
+ </div>
<button type="submit" class="btn btn-primary">登錄</button>
</form>
```
并在C層中初始化屬性:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -13,6 +13,11 @@ export class LoginComponent implements OnInit {
@Output()
beLogin = new EventEmitter<Teacher>();
+ /**
+ * 是否顯示錯(cuò)誤信息
+ */
+ showError = false;
+
constructor(private httpClient: HttpClient) {
}
```
### 測(cè)試
增加相應(yīng)的單元測(cè)試代碼:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -53,4 +53,9 @@ describe('LoginComponent', () => {
component.teacher = {username: '張三', password: 'codedemo.club'} as Teacher;
component.onSubmit();
});
+
+ fit('顯示錯(cuò)誤', () => {
+ fixture.autoDetectChanges();
+ component.showError = true;
+ });
});
```

## 延遲
相較于一直顯示錯(cuò)誤,僅顯示1.5秒的錯(cuò)誤信息,可以有效的提升用戶的使用感受。在技術(shù)上,我們需要一項(xiàng)能夠延時(shí)1.5S的方法。在此我們給出原生的`Timer`方法。
### setTimeout
js中有兩個(gè)原生的延時(shí)執(zhí)行方法,分別是`setTimeout`以及`setInterval`。在執(zhí)行該延時(shí)方法時(shí)js自動(dòng)啟動(dòng)異步機(jī)制。我們前面的章節(jié)中多次使用了http請(qǐng)求。在javascript中,我們把這種http請(qǐng)求又稱為資源請(qǐng)求,javascript會(huì)有兩種情況下啟用異步機(jī)制,分別為:我們這里即將使用的延時(shí)(`setTimeout`、`setInterval`),以及資源請(qǐng)求。
所以如果在筆試中被問及javascript的異步機(jī)制時(shí),我們大概知道怎么開頭了吧。
`setTimeout(function, delayTime)`方法,將在`delayTime(毫秒)`后執(zhí)行`function`,比如:
```javascript
console.log(new Date());
setTimeout(() => console.log(new Date(), 'run'), 1000);
```
則`console.log`將在1秒后執(zhí)行:

利用此特性在C層中定義`setShowError()`方法如下:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -41,4 +41,15 @@ export class LoginComponent implements OnInit {
.subscribe(teacher => this.beLogin.emit(teacher),
error => console.log('發(fā)生錯(cuò)誤, 登錄失敗', error));
}
+
+ /**
+ * 延遲顯示錯(cuò)誤信息
+ */
+ showErrorDelay(): void {
+ this.showError = true;
+ setTimeout(() => {
+ console.log('1.5秒后觸發(fā)');
+ this.showError = false;
+ }, 1500);
+ }
}
```
### 測(cè)試
**注意**:在測(cè)試延遲時(shí),必須保證當(dāng)前項(xiàng)目中我們僅使用一個(gè)`fit`,即僅有一個(gè)測(cè)試用例生效,多個(gè)`fit`的情況下將看不到延遲效果。
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -56,6 +56,6 @@ describe('LoginComponent', () => {
fit('顯示錯(cuò)誤', () => {
fixture.autoDetectChanges();
- component.showError = true;
+ component.showErrorDelay();
});
});
```
測(cè)試結(jié)果卻讓我們表示遺憾:

雖然控制臺(tái)中成功的打印一1.5秒后觸發(fā)了相應(yīng)的方法,但V層中的錯(cuò)誤提示卻未消失。在此,我們簡(jiǎn)單的解釋下為什么單元測(cè)試中并未很好的支持`setTimeout`等延遲的延時(shí)。
## 單元測(cè)試延遲
如果被測(cè)的代碼包含延遲方法,則在單元測(cè)試中該方法看似會(huì)**失敗**。這是由以下幾點(diǎn)決定的:
### 測(cè)試定位
單元測(cè)試的定位為:使用代碼來測(cè)試代碼的正確性,使用代碼來保證代碼的正確性。該測(cè)試代碼在生產(chǎn)項(xiàng)目中是被自動(dòng)執(zhí)行的,單元測(cè)試的目的是防止在生產(chǎn)項(xiàng)目開發(fā)了一個(gè)新功能的同時(shí)誤殺掉歷史的老功能。從這個(gè)角度上來,單元測(cè)試的代碼將在開發(fā)新功能的代碼在投交給團(tuán)隊(duì)長(zhǎng)被執(zhí)行一遍,如果執(zhí)行沒有報(bào)錯(cuò),則說明新功能沒有破壞歷史功能。
這個(gè)過程往往是自動(dòng)化的。而這個(gè)自動(dòng)化的過程,應(yīng)該規(guī)避項(xiàng)目中的延遲。比如我們?cè)谏a(chǎn)中有個(gè)鎖屏功能,實(shí)現(xiàn)的是如果用戶10分鐘內(nèi)未操作系統(tǒng) ,則進(jìn)行鎖屏。如果在單元測(cè)試中支持這個(gè)10分鐘的鎖屏操作,那么再驗(yàn)證其功能是否正常時(shí),則需要等待10分鐘。如果項(xiàng)目中有多個(gè)10分鐘呢?
顯示我們不能接觸一個(gè)單元測(cè)試跑上2個(gè)的小時(shí)的情況,所以在單元測(cè)試中并不會(huì)等待代碼中的setTimeout方法。
### 變更檢測(cè)
我們?cè)趩卧獪y(cè)試中習(xí)慣性的加入了`fixture.autoDetectChanges();`,我們前面講過它的功能是當(dāng)C層發(fā)生變化時(shí)重新對(duì)V層進(jìn)行渲染,在`ng s`時(shí)該功能默認(rèn)開啟,而在`ng t`時(shí)該功能默認(rèn)關(guān)閉。
在開啟該功能時(shí),angular是借助了一個(gè)叫做`zone.js`的偉大軟件實(shí)現(xiàn)了變更檢測(cè),這種檢測(cè)即不損失效率,又可以在數(shù)據(jù)變化的第一時(shí)間內(nèi)得到通知。該`zone.js`對(duì)`setTimeout`等方法做了些手腳,以達(dá)到監(jiān)聽的目的。所以即使我們?cè)赻ng t`中開啟了`fixture.autoDetectChanges();`,但由于一些**特殊的設(shè)置**,在`ng t`時(shí),Angular也無法感知到`setTimeout`中變更的組件屬性`showError`。
## 解決方案
鑒于剛剛提出的原因,如果我們想在單元測(cè)試中感知到`setTimeout`中的屬性變化,則需要Angular提供了`ngZone`,實(shí)際上Angular官方也是推薦我們這么做的:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -1,4 +1,4 @@
-import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import {Component, EventEmitter, NgZone, OnInit, Output} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Teacher} from '../entity/teacher';
@@ -18,7 +18,7 @@ export class LoginComponent implements OnInit {
*/
showError = false;
- constructor(private httpClient: HttpClient) {
+ constructor(private httpClient: HttpClient, private ngZone: NgZone) {
}
ngOnInit(): void {
@@ -47,9 +47,11 @@ export class LoginComponent implements OnInit {
*/
showErrorDelay(): void {
this.showError = true;
- setTimeout(() => {
- console.log('1.5秒后觸發(fā)');
- this.showError = false;
- }, 1500);
+ this.ngZone.run(() => {
+ setTimeout(() => {
+ console.log('1.5秒后觸發(fā)');
+ this.showError = false;
+ }, 1500);
+ });
}
}
```
## tick()
實(shí)際上,即使我們使用上述方法達(dá)到了在`ng t`中觀測(cè)`setTimeout`方法中屬性變化的目的。但這也單元測(cè)試的定位相違背,因?yàn)槲覀儾]有解決使用代碼再短時(shí)間內(nèi)測(cè)試代碼的效果。
Angular提供的`tick()`允許我們?cè)趩卧獪y(cè)試中模擬將時(shí)鐘向前推進(jìn)一些時(shí)間,從而馬上執(zhí)行在本應(yīng)該在某些時(shí)間后才能執(zhí)行的代碼。預(yù)使用該方法,則需要將`fakeAsync()`方法傳入`fit`:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -1,4 +1,4 @@
-import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {FormsModule} from '@angular/forms';
@@ -54,8 +54,8 @@ describe('LoginComponent', () => {
component.onSubmit();
});
- fit('顯示錯(cuò)誤', () => {
+ fit('顯示錯(cuò)誤', fakeAsync(() => {
component.showErrorDelay();
fixture.autoDetectChanges();
- });
+ }));
});
```
然后加入`tick()`以模似時(shí)鐘推進(jìn):
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -1,4 +1,4 @@
-import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing';
+import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {LoginComponent} from './login.component';
import {FormsModule} from '@angular/forms';
@@ -55,7 +55,23 @@ describe('LoginComponent', () => {
});
fit('顯示錯(cuò)誤', fakeAsync(() => {
+ // 初始化不顯示錯(cuò)誤提醒
+ expect(component.showError).toBe(false);
+ fixture.detectChanges();
+
+ // 立即顯示錯(cuò)誤提醒
component.showErrorDelay();
+ expect(component.showError).toBe(true);
+ console.log(new Date());
+ fixture.detectChanges();
+
+ // 將時(shí)鐘模擬向前推進(jìn)15000MS
+ tick(15000); ??
+ console.log(new Date());
+ fixture.detectChanges();
+
+ // 斷言錯(cuò)誤提醒消息
+ expect(component.showError).toBe(false);
fixture.autoDetectChanges();
}));
});
```
這里我們模擬推進(jìn)15秒 ??,以更好的在控制臺(tái)中觀察信息:

此時(shí)控制臺(tái)中先后打印了日志,在輸出date時(shí)發(fā)現(xiàn)時(shí)鐘的確被推進(jìn)了。而我們清楚,先后兩條日志打印的時(shí)間間隔絕對(duì)沒有15秒,這就是`tick()`方法的作用。
## 功能完成
最后,我們?cè)谟脩舻卿浭r(shí),調(diào)用`showErrorDelay()`方法:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -39,7 +39,10 @@ export class LoginComponent implements OnInit {
'http://angular.api.codedemo.club:81/teacher/login',
{headers: httpHeaders})
.subscribe(teacher => this.beLogin.emit(teacher),
- error => console.log('發(fā)生錯(cuò)誤, 登錄失敗', error));
+ error => {
+ console.log('發(fā)生錯(cuò)誤, 登錄失敗', error);
+ this.showErrorDelay();
+ });
}
/**
```
然后啟用用戶登錄單元測(cè)試,嘗試使用個(gè)錯(cuò)誤的用戶名密碼來查看效果吧:
```typescript
fit('onSubmit 用戶登錄', () => {
// 啟動(dòng)自動(dòng)變更檢測(cè)
fixture.autoDetectChanges();
component.teacher = {username: '張三', password: 'codedemo.club' ??} as Teacher;
component.onSubmit();
});
```
將其變更為錯(cuò)誤的密碼 ??。

??

## ng t與生命周期
我們?cè)谇懊娴恼鹿?jié)中接觸了組件生命周期的概念,所謂的生命周期即組件由出生到死記的整個(gè)過程。用術(shù)語來講是組件由實(shí)例化到被銷毀的整個(gè)過程:
* ? 執(zhí)行構(gòu)造函數(shù),實(shí)例化組件實(shí)例;
* ? 檢測(cè)是否存在`ngOnInit()`方法,有則執(zhí)行一次。
* ? 解析V層代碼;
* ? 解析在V層中使用的變量。
既然是由實(shí)例化到組件被銷毀,那么生命周期中也必然存在銷毀組件一步:
* ? 執(zhí)行構(gòu)造函數(shù),實(shí)例化組件實(shí)例;
* ? 檢測(cè)是否存在`ngOnInit()`方法,有則執(zhí)行一次。
* ? 解析V層代碼;
* ? 解析在V層中使用的變量。
* ? 在當(dāng)前組件不被需要的時(shí),銷毀組件。
本節(jié)我們結(jié)結(jié)合當(dāng)前的單元測(cè)試情況來查看下組件的銷毀情況以及何時(shí)銷毀組件:
`ng t`在執(zhí)行時(shí),如果遇到多個(gè)測(cè)試單元(`it`或`fit`)被執(zhí)行時(shí),`ng t`在執(zhí)行某個(gè)單元測(cè)試當(dāng),會(huì)銷毀前面已執(zhí)行單元測(cè)試中創(chuàng)建的組件。在組件被銷毀時(shí),Angular將自動(dòng)調(diào)用組件的`ngOnDestroy`方法,該方法被聲明在`OnDestroy`接口中,在Angular的開發(fā)規(guī)范,當(dāng)組件中存在`ngOnDestroy`時(shí),則需要聲明實(shí)現(xiàn)了`OnDestroy`接口:
```typescript
+++ b/first-app/src/app/login/login.component.ts
@@ -1,4 +1,4 @@
-import {Component, EventEmitter, NgZone, OnInit, Output} from '@angular/core';
+import {Component, EventEmitter, NgZone, OnDestroy, OnInit, Output} from '@angular/core';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Teacher} from '../entity/teacher';
@@ -7,7 +7,7 @@ import {Teacher} from '../entity/teacher';
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
-export class LoginComponent implements OnInit {
+export class LoginComponent implements OnInit, OnDestroy {
teacher = {} as Teacher;
@Output()
@@ -22,6 +22,11 @@ export class LoginComponent implements OnInit {
}
ngOnInit(): void {
+ console.log('組件初始化執(zhí)行1次: ngOnInit');
+ }
+
+ ngOnDestroy(): void {
+ console.log('組件被銷毀時(shí)執(zhí)行一次:ngOnDestroy');
}
onSubmit(): void {
```
此時(shí),讓我們隨意將另一個(gè)`it`變更為`fit`,比如將`login.component.spec.ts`中對(duì)用戶登錄的測(cè)試的`it`變更為`fit`,則可以在測(cè)試的界面中看到進(jìn)行了兩項(xiàng)測(cè)試:

同時(shí)在控制臺(tái)中查看到如下信息:

由控制臺(tái)可以輕易看出,在先后執(zhí)行兩個(gè)`ng t`時(shí),組件初始化方法被執(zhí)行了兩次,同時(shí)被銷毀了一次。這是由于我們單元測(cè)試中的`beforeEach`語句決定的,`beforeEach`意為在執(zhí)行每個(gè)測(cè)試單元前執(zhí)行:
```typescript
beforeEach(() => {
// 實(shí)例化組件
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
```
為了保證每個(gè)測(cè)試單元與其它的測(cè)試單元互不干擾,Jasmine會(huì)在每個(gè)測(cè)試單元執(zhí)行前都執(zhí)行一次`beforeEach`中的代碼。同時(shí)Angular中有個(gè)節(jié)約資源的原則:當(dāng)組件被檢測(cè)到**不被使用**時(shí),將發(fā)起對(duì)組件的銷毀操作。而每個(gè)測(cè)試單元執(zhí)行前的`component = fixture.componentInstance;`都會(huì)導(dǎo)致`component`被重新賦值,這也直接使得`component`變量以前指向的組件實(shí)例被Angular認(rèn)為是**不被使用**的,既而Angular對(duì)組件進(jìn)行銷毀,從而觸發(fā)了`ngOnDestroy`方法。

其實(shí)不僅如此,Jasmine在執(zhí)行多個(gè)單元測(cè)試時(shí),其最終也將釋放對(duì)V層`dom`的控制權(quán)。其直接導(dǎo)致的后果是當(dāng)用戶名密碼錯(cuò)誤時(shí),V層并沒有顯示相應(yīng)的錯(cuò)誤提示。
除`beforEach`方法外,Jasmine還支持在每個(gè)測(cè)試單元測(cè)試結(jié)束后執(zhí)行的`afterEach`方法:
```typescript
+++ b/first-app/src/app/login/login.component.spec.ts
@@ -74,4 +74,8 @@ describe('LoginComponent', () => {
expect(component.showError).toBe(false);
fixture.autoDetectChanges();
}));
+
+ afterEach(() => {
+ console.log('after all');
+ });
});
```
加入`afterAll`方法,控制臺(tái)信息如下:

使用`tick()`模擬時(shí)鐘推進(jìn)的方法將不存在**單元測(cè)試結(jié)束**后組件相關(guān)代碼仍然執(zhí)行的情況,請(qǐng)自行驗(yàn)證。
## 本節(jié)作業(yè)
學(xué)習(xí)firefox調(diào)試器的使用的方法,按步執(zhí)行單元測(cè)試` fit('顯示錯(cuò)誤', fakeAsync(() => {`的代碼,觀察界面發(fā)生的變化:

| 名稱 | 地址 |
| --------------------------- | ------------------------------------------------------------ |
| 一篇關(guān)于ngZone的文章 | [https://blog.kevinyang.net/2019/02/14/ng-ngzone/](https://blog.kevinyang.net/2019/02/14/ng-ngzone/) |
| ngZone官方文檔(原文) | [https://angular.io/guide/zone](https://angular.io/guide/zone) |
| JS定時(shí)器 | [https://www.runoob.com/w3cnote/js-timer.html](https://www.runoob.com/w3cnote/js-timer.html) |
| fakeAsync | [https://www.angular.cn/api/core/testing/fakeAsync](https://www.angular.cn/api/core/testing/fakeAsync) |
| Tick | [https://www.angular.cn/api/core/testing/tick](https://www.angular.cn/api/core/testing/tick) |
| 在firefox中debug TypeScript | [https://hacks.mozilla.org/2019/09/debugging-typescript-in-firefox-devtools/](https://hacks.mozilla.org/2019/09/debugging-typescript-in-firefox-devtools/) |
| what is zone | [https://www.youtube.com/watch?v=3IqtmUscE_U&t=150s](https://www.youtube.com/watch?v=3IqtmUscE_U&t=150s) |
| 本節(jié)源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step5.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step5.1.zip) |
- 序言
- 第一章 Hello World
- 1.1 環(huán)境安裝
- 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 請(qǐng)求后臺(tái)數(shù)據(jù)
- 2.2.1 HttpClient
- 2.2.2 請(qǐng)求數(shù)據(jù)
- 2.2.3 模塊與依賴注入
- 2.2.4 異步與回調(diào)函數(shù)
- 2.2.5 集成測(cè)試
- 2.2.6 本章小節(jié)
- 2.3 新增教師
- 2.3.1 組件初始化
- 2.3.2 [(ngModel)]
- 2.3.3 對(duì)接后臺(tái)
- 2.3.4 路由
- 2.4 編輯教師
- 2.4.1 組件初始化
- 2.4.2 獲取路由參數(shù)
- 2.4.3 插值與模板表達(dá)式
- 2.4.4 初識(shí)泛型
- 2.4.5 更新教師
- 2.4.6 測(cè)試中的路由
- 2.5 刪除教師
- 2.6 收尾工作
- 2.6.1 RouterLink
- 2.6.2 fontawesome圖標(biāo)庫(kù)
- 2.6.3 firefox
- 2.7 總結(jié)
- 第三章 用戶登錄
- 3.1 初識(shí)單元測(cè)試
- 3.2 http概述
- 3.3 Basic access authentication
- 3.4 著陸組件
- 3.5 @Output
- 3.6 TypeScript 類
- 3.7 瀏覽器緩存
- 3.8 總結(jié)
- 第四章 個(gè)人中心
- 4.1 原型
- 4.2 管道
- 4.3 對(duì)接后臺(tái)
- 4.4 x-auth-token認(rèn)證
- 4.5 攔截器
- 4.6 小結(jié)
- 第五章 系統(tǒng)菜單
- 5.1 延遲及測(cè)試
- 5.2 手動(dòng)創(chuàng)建組件
- 5.3 隱藏測(cè)試信息
- 5.4 規(guī)劃路由
- 5.5 定義菜單
- 5.6 注銷
- 5.7 小結(jié)
- 第六章 班級(jí)管理
- 6.1 新增班級(jí)
- 6.1.1 組件初始化
- 6.1.2 MockApi 新建班級(jí)
- 6.1.3 ApiInterceptor
- 6.1.4 數(shù)據(jù)驗(yàn)證
- 6.1.5 教師選擇列表
- 6.1.6 MockApi 教師列表
- 6.1.7 代碼重構(gòu)
- 6.1.8 小結(jié)
- 6.2 教師列表組件
- 6.2.1 初始化
- 6.2.2 響應(yīng)式表單
- 6.2.3 getTestScheduler()
- 6.2.4 應(yīng)用組件
- 6.2.5 小結(jié)
- 6.3 班級(jí)列表
- 6.3.1 原型設(shè)計(jì)
- 6.3.2 初始化分頁
- 6.3.3 MockApi
- 6.3.4 靜態(tài)分頁
- 6.3.5 動(dòng)態(tài)分頁
- 6.3.6 @Input()
- 6.4 編輯班級(jí)
- 6.4.1 測(cè)試模塊
- 6.4.2 響應(yīng)式表單驗(yàn)證
- 6.4.3 @Input()
- 6.4.4 FormGroup
- 6.4.5 自定義FormControl
- 6.4.6 代碼重構(gòu)
- 6.4.7 小結(jié)
- 6.5 刪除班級(jí)
- 6.6 集成測(cè)試
- 6.6.1 惰性加載
- 6.6.2 API攔截器
- 6.6.3 路由與跳轉(zhuǎn)
- 6.6.4 ngStyle
- 6.7 初識(shí)Service
- 6.7.1 catchError
- 6.7.2 單例服務(wù)
- 6.7.3 單元測(cè)試
- 6.8 小結(jié)
- 第七章 學(xué)生管理
- 7.1 班級(jí)列表組件
- 7.2 新增學(xué)生
- 7.2.1 exports
- 7.2.2 自定義驗(yàn)證器
- 7.2.3 異步驗(yàn)證器
- 7.2.4 再識(shí)DI
- 7.2.5 屬性型指令
- 7.2.6 完成功能
- 7.2.7 小結(jié)
- 7.3 單元測(cè)試進(jìn)階
- 7.4 學(xué)生列表
- 7.4.1 JSON對(duì)象與對(duì)象
- 7.4.2 單元測(cè)試
- 7.4.3 分頁模塊
- 7.4.4 子組件測(cè)試
- 7.4.5 重構(gòu)分頁
- 7.5 刪除學(xué)生
- 7.5.1 第三方dialog
- 7.5.2 批量刪除
- 7.5.3 面向?qū)ο?/a>
- 7.6 集成測(cè)試
- 7.7 編輯學(xué)生
- 7.7.1 初始化
- 7.7.2 自定義provider
- 7.7.3 更新學(xué)生
- 7.7.4 集成測(cè)試
- 7.7.5 可訂閱的路由參數(shù)
- 7.7.6 小結(jié)
- 7.8 總結(jié)
- 第八章 其它
- 8.1 打包構(gòu)建
- 8.2 發(fā)布部署
- 第九章 總結(jié)