當所有的盲點都被消除后,最后的功能完成已然成為了最很簡單的一環。
## 服務與實體
在生產項目中,往往會使用服務來完成與后臺的交互工作,這在組件需要處理一些邏輯功能,或是需要與其它的服務進行交互時特別重要。
為此首先來到`src/app/service`文件夾創建StudentService:
```bash
panjie@panjies-iMac service % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/service
panjie@panjies-iMac service % ng g s student
CREATE src/app/service/student.service.spec.ts (362 bytes)
CREATE src/app/service/student.service.ts (136 bytes)
```
然后該服務器添加新增學生方法:
```typescript
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class StudentService {
constructor() {
}
/**
* 新增學生.
*/
add(): Observable<any> {
return of();
}
}
```
為了在后續的其它學生相關組件中更好的處理學生這個實體,在繼續進行之前,還需要來到`src/app/entity`文件夾,新建一個學生實體:
```bash
panjie@panjies-iMac entity % pwd
/Users/panjie/github/mengyunzhi/angular11-guild/first-app/src/app/entity
panjie@panjies-iMac entity % ng g class student
CREATE src/app/entity/student.spec.ts (158 bytes)
CREATE src/app/entity/student.ts (25 bytes)
```
然后在學生實體中定義屬性、加入構造函數以及特別重要的注釋:
```typescript
import {Clazz} from './clazz';
/**
* 學生.
*/
export class Student {
id: number;
/**
* 姓名.
*/
name: string;
/**
* 學號.
*/
number: string;
/**
* 手機號.
*/
phone: string;
/**
* email.
*/
email: string;
/**
* 班級.
*/
clazz: Clazz;
constructor(data = {} as
{
id?: number,
name?: string,
number?: string,
phone?: string,
email?: string,
clazz?: Clazz
}) {
this.id = data.id as number;
this.name = data.name as string;
this.number = data.number as string;
this.phone = data.phone as string;
this.email = data.email as string;
this.clazz = data.clazz as Clazz;
}
}
```
有了Student實體后,開始完成StudentService中的`add`方法。
## Add方法
添加學生時,需要接收姓名、學號、手機號、email、班級信息,故對參數初始化如下:
```typescript
+++ b/first-app/src/app/service/student.service.ts
@@ -1,5 +1,7 @@
import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
+import {Student} from '../entity/student';
+import {Clazz} from '../entity/clazz';
@Injectable({
providedIn: 'root'
@@ -12,7 +14,14 @@ export class StudentService {
/**
* 新增學生.
*/
- add(): Observable<any> {
- return of();
+ add(data: ①{name: string, number: string, phone: string, email: string, clazzId: number}): Observable<Student> ②{
+ const student = new Student({
+ name: data.name,
+ number: data.number,
+ phone: data.phone,
+ email: data.email,
+ clazz: new Clazz({id: data.clazzId})
+ })③;
+ return of(student)④;
}
}
```
- ① 當參數類型設置為`{}`,以后擴展增加新字段時更方便。
- ② 新增成功后臺將返回新增后的學生信息。
- ③ 使用`new Student()`的方法讓編譯器來對語法進行校驗,防止不小心出現的拼寫錯誤。
- ④ 在沒有MockApi以前,暫時返回student。
## MockApi
添加學生的API與添加教師、添加班級的Api一致,在此即使不給出后臺API的具體說明,相信我們也能夠書寫出正確的請求:
```typescript
+++ b/first-app/src/app/mock-api/student.mock.api.ts
@@ -1,5 +1,6 @@
-import {ApiInjector, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api';
+import {ApiInjector, MockApiInterface, randomNumber, RequestOptions} from '@yunzhi/ng-mock-api';
import {HttpParams} from '@angular/common/http';
+import {Student} from '../entity/student';
/**
* 學生模擬API.
@@ -21,6 +22,15 @@ export class StudentMockApi implements MockApiInterface {
return false;
}
}
- }];
+ }, {
+ method: 'POST',
+ url: '/student',
+ result: ((urlMatches: string[], options: RequestOptions) => {
+ const student = options.body as Student;
+ // 模擬保存成功后生成ID
+ student.id = randomNumber();
+ return student;
+ })
+ }
+ ];
}
}
```
如果你想使自己的MockApi能夠像真實的Api一樣可以校驗信息,則還可以適當的加入一些斷言,比如在新增學生時,要求必須傳入預新增學生的基本字段:
```typescript
result: ((urlMatches: string[], options: RequestOptions) => {
const student = options.body as Student;
+ Assert.isString(student.phone, student.email, student.number, student.name, '學生的基本信息未傳全');
+ Assert.isNumber(student.clazz.id, '班級id校驗失敗');
student.id = randomNumber();
return student;
})
```
此時將對該模擬后臺發起請求時,如果未傳入相應的信息,`HttpClient`則會接收到了一個`error`。我們借用StudentService的測試文件來測試下發起請求時如果沒有傳入特定的字段,怎樣來獲取這個`error`:
```typescript
+++ b/first-app/src/app/service/student.service.spec.ts
@@ -1,16 +1,26 @@
-import { TestBed } from '@angular/core/testing';
+import {TestBed} from '@angular/core/testing';
-import { StudentService } from './student.service';
+import {StudentService} from './student.service';
+import {MockApiTestingModule} from '../mock-api/mock-api-testing.module';
+import {HttpClient} from '@angular/common/http';
describe('StudentService', () => {
let service: StudentService;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ TestBed.configureTestingModule({
+ imports: [
+ MockApiTestingModule
+ ]
+ });
service = TestBed.inject(StudentService);
});
- it('should be created', () => {
+ fit('should be created', () => {
expect(service).toBeTruthy();
// TestBed.inject()可獲取到當前動態測試模塊的所有服務
+ const httpClient = TestBed.inject(HttpClient);
+ httpClient.post('/student', {})
+ .subscribe(success => console.log('success', success),
+ error => console.log('error', error));
});
});
```
當MockApi發生異常時,將會觸發`subscribe`中的`error`方法,這與正常的后臺請求報錯的方式一致:

## 返回預請求
有了MockApi后,我們在StudentService中發起這個請求:
```typescript
+++ b/first-app/src/app/service/student.service.ts
@@ -2,13 +2,14 @@ import {Injectable} from '@angular/core';
import {Observable, of} from 'rxjs';
import {Student} from '../entity/student';
import {Clazz} from '../entity/clazz';
+import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class StudentService {
- constructor() {
+ constructor(private httpClient: HttpClient) {
}
/**
@@ -22,6 +23,6 @@ export class StudentService {
email: data.email,
clazz: new Clazz({id: data.clazzId})
});
- return of(student);
+ // 將預請求信息返回
+ return this.httpClient.post<Student>('/student', student);
}
```
## 組件調用
其它工作準備完畢后,組件調用便成了最簡單的一環:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
import {YzAsyncValidators} from '../../yz-async-validators';
-import {HttpClient} from '@angular/common/http';
+import {StudentService} from '../../service/student.service';
@Component({
selector: 'app-add',
@@ -12,7 +12,7 @@ import {HttpClient} from '@angular/common/http';
export class AddComponent implements OnInit {
formGroup: FormGroup;
- constructor(private httpClient: HttpClient, private yzAsyncValidators: YzAsyncValidators) {
+ constructor(private studentService: StudentService, private yzAsyncValidators: YzAsyncValidators) {
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()),
@@ -26,6 +26,15 @@ export class AddComponent implements OnInit {
}
onSubmit(): void {
- console.log('submit');
+ const student = this.formGroup.value① as② {
+ name: string,
+ number: string,
+ phone: string,
+ email: string,
+ clazzId: number
+ };
+ this.studentService.add(student)
+ .subscribe(success => console.log('保存成功', success),
+ error => console.log('保存失敗', error));
}
}
```
- ① 可以使用`FormGroup.value`來獲取整個`FormGroup`中所有`FormControl`的值
- ② 需要注意的是,這個雖然可能使用`as`將其轉換為任意值,但這種轉換也帶來了一定的風險,比如我們在初始化`FormGroup`時,誤把`email`寫成了`emial`。
填寫完所有的字段后,保存成功。

其實有時候我們很難將`onSubmit()`一次性的書寫成功,比如我們以后需要加入保存成功后路由的跳轉信息。所以在開發過程中往往需要屢次點擊保存按鈕,而點擊該按鈕前卻需要將表單的所有字段輸全,這明顯是個重復的勞動,做為**懶人**的我們怎么能允許些類事情的發生。
如果在點擊保存按鈕前這些信息全部都為我們自動填寫好,那該多好呀。?? 還不快用單元測試?
```typescript
+++ b/first-app/src/app/student/add/add.component.spec.ts
@@ -8,6 +8,8 @@ import {getTestScheduler} from 'jasmine-marbles';
import {Observable, of} from 'rxjs';
import {map} from 'rxjs/operators';
import {LoadingModule} from '../../directive/loading/loading.module';
+import {randomString} from '@yunzhi/ng-mock-api/testing';
+import {randomNumber} from '@yunzhi/ng-mock-api';
describe('student -> AddComponent', () => {
let component: AddComponent;
@@ -40,6 +42,24 @@ describe('student -> AddComponent', () => {
fixture.autoDetectChanges();
});
+ fit('自動填充要新建的學生數據', () => {
+ // 固定寫法
+ getTestScheduler().flush();
+ fixture.detectChanges();
+
+ component.formGroup.setValue({
+ name: randomString('姓名'),
+ number: randomNumber().toString(),
+ phone: '13900000000',
+ email: '123@yunzhi.club',
+ clazzId: randomNumber(10)
+ });
+
+ // 固定寫法
+ getTestScheduler().flush();
+ fixture.autoDetectChanges();
+ });
+
it('理解map操作符', () => {
// 數據源發送數據1
const a = of(1) as Observable<number>;
```
在此單元測試代碼的支持上,我們再也不必手動地填寫這些數據了:

這絕對是個提升生產力的好方法。好了,就到這里,休息一會。
| 名稱 | 鏈接 |
| ----------------- | ------------------------------------------------------------ |
| HTMLElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) |
| HTMLButtonElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement) |
| 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step7.2.6.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.2.6.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 發布部署
- 第九章 總結