为了尽早的查看在真实后台配合下的效果,我们在此进行集成测试。 ## 设置路由 在整个项目中集成某个组件的方法是为该方法定制一个对应的路由。这样一来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) | 本节源码 |