为了尽早的查看在真实后台配合下的效果,我们在此进行集成测试。
## 设置路由
在整个项目中集成某个组件的方法是为该方法定制一个对应的路由。这样一来Angular便可以根据浏览器URI地址来找到对应的组件。
在上个章节中,我们使用了惰性加载的方式加了班级模块,这在生产项目中是非常有必要的。惰性加载避免了用户启动时过多的加载项目文件,增加了整个应用的启动速度。在网络不理想的情况下将大幅提升用户的使用感受。
为此我们仍然使用惰性加载的方式来加载学生模块的各个组件。
```typescript
+++ b/first-app/src/app/app-routing.module.ts
@@ -18,6 +18,10 @@ const routes: Routes = [
return m.ClazzModule;
})
},
+ {
+ path: 'student',
+ loadChildren: () => import('./student/student.module').then(m => m.StudentModule)
+ },
{
```
然后在`StudentModule`中进行相应的路由设置。
### 路由模块
观察App模块与Clazz模块的路由设置不难得出:App模块使用了单独的路由模块`AppRoutingModule`,Clazz模块直接将路由信息设置到了`ClazzModule`中。其实这两种方式都是可以的,在实际的使用过程中,习惯于用哪种方式都可以,只需要保持整个项目的风格统一即可。
在此,我们展示如果手动在学生模块中建立路由模块,并将其应用到学生模块中。
使用`shell`来到学生模块所在文件夹,并执行如下命令:
```bash
panjie@panjies-iMac student % ng g m studentRouting --flat
Your global Angular CLI version (11.2.13) is greater than your local version (11.0.7). The local Angular CLI version is used.
To disable this warning use "ng config -g cli.warnings.versionMismatch false".
CREATE src/app/student/student-routing.module.ts (200 bytes)
```
该命令将为我们当前文件夹下创建一个`student-routing.module.ts`文件:
```bash
panjie@panjies-iMac student % tree -L 1
.
├── add
├── student-routing.module.ts 👈
├── student.component.css
├── student.component.html
├── student.component.spec.ts
├── student.component.ts
└── student.module.ts
```
文件内容如下:
```typescript
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
@NgModule({
declarations: [],
imports: [
CommonModule
]
})
export class StudentRoutingModule {
}
```
接着将该文件内容修改为:
```typescript
import {NgModule} from '@angular/core';
import {Route, RouterModule} from '@angular/router';
import {StudentComponent} from './student.component';
import {AddComponent} from './add/add.component';
const routes = [
{
path: '',
component: StudentComponent
},
{
path: 'add',
component: AddComponent
}
] as Route[];
@NgModule({
imports: [
RouterModule.forChild(routes)
],
exports: [RouterModule]
})
export class StudentRoutingModule {
}
```
该模块中的`imports`中引入了`RouterModule.forChild(routes)`,只所以能这样使用是由于`forChild()`方法的返回值类型也相当于`RouterModule` 。如果直接`RouterModule`,则会得到一个空路由配置;但`forChild()`方法可以接收一个路由数组,该方法将返回一个具有路由配置的`RouterModule`。
该模块接着在`exports`中声明了`RouterModule`,其作用是将配置了路由的`RouterModule`抛出去(声明为公有),这样以来当其它模块引入本模块时,则相当于一并引入了配置了路由的`RouterModule`。
### `?`与`!`
现在执行`ng s`,将得到如下错误警告(针对增加学生组件):
```bash
Error: add/add.component.html:6:63 - error TS2531: Object is possibly 'null'.
6 <small class="text-danger" *ngIf="formGroup.get('name').invalid">
~~~~~~~
```
稍微翻译一下得知它在告诉我们说:在执行`formGroup.get('name').invalid`由于`formGroup.get('name')`的值可以是`null`,所以有可能发生`在null上调用invalid属性`的异常。
上述信息是Angular根据类型检查为我们提示的错误,追其原由是由于我们在C层定义`formGroup`的类型是`FormGroup`,而`FormGroup`中的`get()`方法的返回值声明如下(了解即可):
```typescript
/**
* Retrieves a child control given the control's name or path.
*
* @param path A dot-delimited string or array of string/number values that define the path to the
* control.
*
* @usageNotes
* ### Retrieve a nested control
*
* For example, to get a `name` control nested within a `person` sub-group:
*
* * `this.form.get('person.name');`
*
* -OR-
*
* * `this.form.get(['person', 'name']);`
*/
get(path: Array<string | number> | string): AbstractControl | null; 👈
```
该方法声明其返回值可能为`null`,这也是为什么Angular会出现上述提示的原因:既然`get()`的返回值可能为`null`,那么一旦其值为`null`则必然会发生在`null`上读取`xxx`属性的异常。虽然对当前学生新增组件而言,我们在C层的构造函数中对`formGroup`的各项进行了初始化从而避免执行`get()`方法时返回`null`,但这个逻辑对于Angular的语法检查器而言太难了。Angular的语法检查仅能够做到根据返回值的类型进行推导(这已经很伟大了)。
此时我们有两种方案来解决当前警告,第一种是使用`?`标识符,它表示:如果当前值是一个对象,则继续执行;否则中断执行:
```html
+++ b/first-app/src/app/student/add/add.component.html
@@ -3,7 +3,7 @@
<label class="col-sm-2 col-form-label">名称</label>
<div class="col-sm-10">
<input type="text" class="form-control" formControlName="name">
- <small class="text-danger" *ngIf="formGroup.get('name').invalid">
+ <small class="text-danger" *ngIf="formGroup.get('name')?.invalid">
名称不能为空
</small>
</div>
@@ -11,11 +11,11 @@
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">学号</label>
<div class="col-sm-10">
- {{formGroup.get('number').errors | json}}
+ {{formGroup.get('number')?.errors | json}}
<input type="text" class="form-control" formControlName="number">
- <small class="text-danger" *ngIf="formGroup.get('number').invalid">
- <span *ngIf="formGroup.get('number').errors.required">学号不能为空</span>
- <span *ngIf="formGroup.get('number').errors.numberExist">该学号已存在</span>
+ <small class="text-danger" *ngIf="formGroup.get('number')?.invalid">
+ <span *ngIf="formGroup.get('number')?.errors?.required">学号不能为空</span>
+ <span *ngIf="formGroup.get('number')?.errors?.numberExist">该学号已存在</span>
</small>
</div>
</div>
@@ -23,9 +23,9 @@
<label class="col-sm-2 col-form-label">手机号</label>
<div class="col-sm-10">
<input type="text" class="form-control" formControlName="phone">
- {{formGroup.get('phone').invalid}}
- {{formGroup.get('phone').errors | json}}
- <small class="text-danger" *ngIf="formGroup.get('phone').invalid">
+ {{formGroup.get('phone')?.invalid}}
+ {{formGroup.get('phone')?.errors | json}}
+ <small class="text-danger" *ngIf="formGroup.get('phone')?.invalid">
手机号格式不正确
</small>
</div>
```
第二种方式是使用`!`标识符,表示:我确认当前值必然是一个对象,请忽略掉对非object的判断:
```typescript
+++ b/first-app/src/app/student/add/add.component.html
@@ -40,7 +40,7 @@
<label class="col-sm-2 col-form-label">班级</label>
<div class="col-sm-10">
<app-clazz-select formControlName="clazzId"></app-clazz-select>
- <small class="text-danger" *ngIf="formGroup.get('clazzId').invalid">
+ <small class="text-danger" *ngIf="formGroup.get('clazzId')!.invalid">
必须选择班级
</small>
</div>
```
在生产项目中,应该根据实际情况来决定是使用`?`还是`!`。以当前学生新增组件为例,在执行`formGroup.get('xxx')`应该返回一个对象,如果没有对象返回的话就说明我们的C层代码逻辑错了(比如漏写了),则此时应该使用`!`;而代码`formGroup.get('number').errors.numberExist`中的`errors`则会在发生错误时才会有值,未发生错误的话其值则是`null`,此时则应该使用`?`标识符。
所以`formGroup.get('number').errors.numberExist`的正确写法应当是`formGroup.get('number')!.errors?.numberExist`。
请按上述规范修正学生新增组件中的V层代码。
### 引入路由
有了单独的路由模块后,便可以将其引入到学生模块中了:
```typescript
+++ b/first-app/src/app/student/student.module.ts
@@ -5,6 +5,7 @@ import {ReactiveFormsModule} from '@angular/forms';
import { StudentComponent } from './student.component';
import {PageModule} from '../clazz/page/page.module';
import {RouterModule} from '@angular/router';
+import {StudentRoutingModule} from './student-routing.module';
@NgModule({
@@ -13,7 +14,8 @@ import {RouterModule} from '@angular/router';
CommonModule,
ReactiveFormsModule,
PageModule,
- RouterModule
+ RouterModule,
+ StudentRoutingModule
]
})
export class StudentModule {
```
路由引入完成后,打开浏览器并访问[http://localhost:4200/student](http://localhost:4200/student)来检验下自己的成果吧。
![image-20210609101123561](6.assets/image-20210609101123561.png)
### 设置菜单
最后我们完善下菜单,使得点击“学生管理”时跳转http://localhost:4200/student:
```html
+++ b/first-app/src/app/nav/nav.component.html
@@ -18,7 +18,7 @@
<a class="nav-link" routerLink="clazz">班级管理</a>
</li>
<li class="nav-item">
- <a class="nav-link" href="#">学生管理</a>
+ <a class="nav-link" routerLink="student">学生管理</a>
</li>
<li class="nav-item" routerLinkActive="active">
<a class="nav-link" routerLink="personal-center">个人中心</a>
```
## 测试
点击新增按钮后,将在控制台得到以下信息:
![image-20210609103921300](6.assets/image-20210609103921300.png)
其实该错误信息我们的IDE早早的就在提示我们:
![image-20210609103951209](6.assets/image-20210609103951209.png)
产生该错语的原因是由于当前模块并不认识`app-clazz-select`选择器。而产生该错误的原因是由于当前模块并没有引入`app-clazz-select`选择器所在的模块:
```typescript
+++ b/first-app/src/app/student/student.module.ts
@@ -6,6 +6,7 @@ import { StudentComponent } from './student.component';
import {PageModule} from '../clazz/page/page.module';
import {RouterModule} from '@angular/router';
import {StudentRoutingModule} from './student-routing.module';
+import {ClazzSelectModule} from '../clazz/clazz-select/clazz-select.module';
@NgModule({
@@ -15,7 +16,8 @@ import {StudentRoutingModule} from './student-routing.module';
ReactiveFormsModule,
PageModule,
RouterModule,
- StudentRoutingModule
+ StudentRoutingModule,
+ ClazzSelectModule
]
})
export class StudentModule {
```
引入模块后错误消失(可能需要重启`ng s`)。
接着进行新增功能的测试,发现点击保存按钮后控制台虽然打印成功,但却未进行跳转,修正如下:
```typescript
+++ b/first-app/src/app/student/add/add.component.ts
@@ -3,6 +3,7 @@ import {FormControl, FormGroup, Validators} from '@angular/forms';
import {YzValidators} from '../../yz-validators';
import {YzAsyncValidators} from '../../yz-async-validators';
import {StudentService} from '../../service/student.service';
+import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'app-add',
@@ -12,7 +13,8 @@ import {StudentService} from '../../service/student.service';
export class AddComponent implements OnInit {
formGroup: FormGroup;
- constructor(private studentService: StudentService, private yzAsyncValidators: YzAsyncValidators) {
+ constructor(private studentService: StudentService, private yzAsyncValidators: YzAsyncValidators,
+ private router: Router, private route: ActivatedRoute) {
this.formGroup = new FormGroup({
name: new FormControl('', Validators.required),
number: new FormControl('', Validators.required, yzAsyncValidators.numberNotExist()),
@@ -34,7 +36,7 @@ export class AddComponent implements OnInit {
clazzId: number
};
this.studentService.add(student)
- .subscribe(success => console.log('保存成功', success),
+ .subscribe(success => this.router.navigate(['../'], {relativeTo: this.route}),
error => console.log('保存失败', error));
}
```
跳转修正后保存完学生后正常回跳到了学生列表界面。
随后测试删除、批量删除、分页功能等功能。
![image-20210609155050370](https://img.kancloud.cn/18/39/1839701ccab5c120e0385ccf9c3e4847_2394x1006.png)
测试结果功能正常,集成测试结束。
## 本节资源
| 链接 | 名称 |
| ------------------------------------------------------------ | -------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.6.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.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 发布部署
- 第九章 总结