基本的功能完成了但还有一些小问题,比如:如果添加了重名的课程,则需要到控制台中查看错误信息。 本节将采用异步验证器的方式判断添加的课程名是否重复,这样一来如果要添加的课程名已经存在于数据库中则直接在前台提示用户。 # 异步验证 在前面的章节中已经接触了required、minLength、maxLength三个**同步**验证器,只所以称为同步验证器是由于其验证过程直接发生在前台。而要添加的课程名称是否与数据库的课程名称发生冲突,则需要借助于后台进行判断。JS中有两种情况下执行异步操作,第一种情况是执行setTimeout方法时,第二种情况是发生资源请求时(与后台通讯)时。所以借助于后台才能验证成功的验证器被称为异步验证器。 新的知识点我们按由后到前的顺序逐点进行开发。 # 后台 对课程名称的验重需要接收课程名称,返回值的类型定义为boolean,当传入的名称已存在于数据库中的话返回true,当传入的名称在数据库中不存在话返回false。 接口规范如下: ``` GET /Course/existsByName?name=xxx true: 名称已存在 false: 名称不存在 ``` ## 仓库层 repository/CourseRepository.java ```java public interface CourseRepository extends CrudRepository<Course, Long> { /** * 课程名称是否存在 * @param name 课程名称 * @return true 存在 */ boolean existsByName(String name); } ``` ### 单元测试 新建对应的单元测试并初始化如下: repository/CourseRepositoryTest.java ```java @SpringBootTest @RunWith(SpringRunner.class) public class CourseRepositoryTest { @Autowired CourseRepository courseRepository; @Test public void existsByName() { // 生成随机字符串的课程名 // 调用existsByName方法,断言返回false // 新建课程,课程名用上面生成的随机字符串,保存课程 // 再次调用existsByName方法,断言返回true } } ``` 补充测试代码如下: repository/CourseRepositoryTest.java ```java @Test public void existsByName() { // 生成随机字符串的课程名 String name = RandomString.make(10); // 调用existsByName方法,断言返回false Assert.assertFalse(this.courseRepository.existsByName(name)); // 新建课程,课程名用上面生成的随机字符串,保存课程 Course course = new Course(); course.setName(name); this.courseRepository.save(course); // 再次调用existsByName方法,断言返回true Assert.assertTrue(this.courseRepository.existsByName(name)); } ``` 单元测试通过。 ## M层 在M层中仅仅做数据转发即可。 service/CourseService.java ```java /** * 名称是否存在 * @param name 课程名称 * @return true 存在 */ boolean existsByName(String name); ``` 实现类: service/CourseServiceImpl.java ```java @Override public boolean existsByName(String name) { return this.courseRepository.existsByName(name); } ``` ### 单元测试 service/CourseServiceImplTest.java ```java @Test public void existsByName() { String name = RandomString.make(10); Mockito.when(this.courseRepository.existsByName(name)).thenReturn(false); boolean result = this.courseService.existsByName(name); Assert.assertFalse(result); } ``` ## C层 controller/CourseController.java ```java @GetMapping("existsByName") public boolean existsByName(@RequestParam String name) { return this.courseService.existsByName(name); } ``` ### 单元测试 controller/CourseControllerTest.java ```java @Test public void existsByName() throws Exception { String name = RandomString.make(4); String url = "/Course/existsByName"; Mockito.when(this.courseService.existsByName(Mockito.eq(name))).thenReturn(false); this.mockMvc.perform(MockMvcRequestBuilders.get(url) .param("name", name)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string("false")) ; } ``` 后台的接口准备完毕后,开始进行前台表单的异步验证。 # 前台 在正式的书写异步验证器前,在进行一些准备工作:在M层中添加对应的existsByName方法。 ## M层 service/course.service.ts ```typescript /** * 课程名称是否存在 * @param name 课程名称 */ existsByName(name: string): Observable<boolean> { const url = this.url + '/existsByName'; return this.httpClient.get<boolean>(url, {params: {name}}); } ``` ### 单元测试 service/course.service.spec.ts ```typescript fit('existsByName', () => { const service: CourseService = TestBed.get(CourseService); const name = 'test'; let result; service.existsByName(name).subscribe((data) => { result = data; }); const testController = TestBed.get(HttpTestingController) as HttpTestingController; const request = testController.expectOne(req => req.url === 'http://localhost:8080/Course/existsByName'); expect(request.request.params.get('name')).toEqual('test'); expect(request.request.method).toEqual('GET'); }); ``` ## 异步验证器 来到src/app/course文件夹下建立validator文件夹,建立UniqueNameValidator验证器: ``` panjiedeMac-Pro:validator panjie$ ng g class UniqueNameValidator CREATE src/app/course/validator/unique-name-validator.spec.ts (208 bytes) CREATE src/app/course/validator/unique-name-validator.ts (37 bytes) ``` 异步验证器需要实现AsyncValidator接口中的validate方法,初始化如下: course/validator/unique-name-validator.ts ```typescript import {AbstractControl, AsyncValidator, ValidationErrors} from '@angular/forms'; import {Observable, of} from 'rxjs'; import {Injectable} from '@angular/core'; import {CourseModule} from '../course.module'; /** * 课程名称唯一性异步验证器 */ @Injectable({ providedIn: 'root' }) export class UniqueNameValidator implements AsyncValidator➊ { validate➊(control: AbstractControl➋): Promise<ValidationErrors➌ | null> | Observable<ValidationErrors➍ | null> { console.log(control); ➋ return of({uniqueName: true}); ➎ } } ``` * ➊ 实现AsyncValidator的validate方法。 * ➋ 验证器对应验证的表单内容。 * ➌ 返回值可以是promise(promise是Observable的简化版,有了Observable以后使用promise的频率较低) * ➍ 返回值也可以是Observable * ➎ 如验证通过则返回null。如果未通过则返回字符串格式的键值对(ValidationErrors) ### 测试 启动前后台,将此验证器添加到表单中的name字段上。 course/add/add.component.ts ```typescript constructor(private formBuilder: FormBuilder, private courseService: CourseService, private uniqueNameValidator: UniqueNameValidator ➊) { } ngOnInit() { this.formGroup = this.formBuilder.group({ name: ['', [Validators.minLength(2), Validators.required], this.uniqueNameValidator.validate➋] }); this.course = new Course(); } ``` * ➊ 注入异步验证器 * ➋ 添加到name字段的异步验证器中 课程名称输入`1`,同步验证器验证失败,未调用异步验证器: ![](https://img.kancloud.cn/19/92/19928ac39b43d117a98cd28c0ed6b8ee_650x324.png) 课程名称输入`1`,同步验证器验证通过,调用异步验证器中的validate方法,触发语句`console.log(control);`在控制台中输出了AbstractControl信息: ![](https://img.kancloud.cn/ba/2b/ba2bde2ecc9d222a4615f5cc5fb7ccb4_689x386.png) 查看详情: ![](https://img.kancloud.cn/13/a1/13a1e9294ab598f35c26dafe046e3bbb_473x606.png) 可见在AbstractControl中可以得到当前表单项的输入值,异步验证器的返回信息被添加到`errors`中。由此定义前台V层的提示信息如下: course/add/add.component.html ```html <small id="nameMinLength" *ngIf="formGroup.get('name').errors && formGroup.get('name').errors.minlength" class="form-text text-danger">课程名称不得少于2个字符</small> <small id="nameUnique" *ngIf="formGroup.get('name').errors && formGroup.get('name').errors.uniqueName" class="form-text text-danger">当前课程名已被占用</small> <div class="form-group"> ``` ![](.9_images/84.gif) ### 对接M层 完成课程是否存在的逻辑功能是由CourseService中的existsByName方法实现的,在验证器中注入CourseService以调用该功能 course/validator/unique-name-validator.ts ```typescript export class UniqueNameValidator implements AsyncValidator { static courseService: CourseService; ➊★ constructor(courseService: CourseService) { UniqueNameValidator.courseService = courseService; } validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors➋ | null> { return UniqueNameValidator.courseService➊.existsByName(control.value); ➌ return of({uniqueName: true}) ➋ ✘ } ``` * ➊ 由于this作用域的问题,需要将注入的courseService对UniqueNameValidator的静态变量courseService赋值。 * ➋ 异步验证器要求返回的数据流中的数据类型是字符串形式的`键值对` * ➌ CourseService中返回的数据流中的数据类型是boolean >[info] ★这个问题相对比较复杂,需要对ts中的this作用域有较深的理解,在此不进行深入讲解。在此不能使用this.courseService的原因是由于在name的异步验证器中使用了`this.uniqueNameValidator.validate`,这意味着将`validate`函数脱离了`uniqueNameValidator`对象单独使用。在调用`validate`函数时`this`将取决于被调用时的上下文【选学】。在生产环境训,还有另外一种更有效的定义异步验证器的方法,请在google中搜索`AsyncValidatorFn`以获取更多知识。教程中为了更贴近于angular的官方文档,使用了官方文档中的示例方法。 将exists返回的boolean类型的数据流变成`键值对`形式的数据流转发下却便需要借助RxJS的map操作符了。 # RxJS实践 在前面的章节中学习过:位于数据流中的转发者是可以通过操作符来对过境的数据进行转变的。 ![](.5_images/4620bf8a.png) ## map实践一 在RxJS中使用map操作符来完成数据格式的转换。 course/validator/unique-name-validator.ts ```typescript validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> { return this.courseService.existsByName(control.value) .pipe(map()); } ``` 比如将courseService.existsByName方法传输过来的boolean类型的数据,转换为`{uniqueName: true}`,则可以使用以下代码完成: course/validator/unique-name-validator.ts ```typescript validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> { return this.courseService.existsByName(control.value) .pipe(map➊((input➋) => { console.log(input); const output = {uniqueName: true}; return output; ➌ })); } ``` * ➊ map 操作符接收的参数类型为:回调函数 * ➋ 将源数据流做为input输入至回调函数中 * ➌ 将回调函数中的返回数据做为新的数据流向后进行转发 ![](https://img.kancloud.cn/76/cd/76cd9f4f9e9ed8331b0d02c652a81de5_879x444.png) 控制台中打印了input的值为false,说明input接收正确。在V层中显示了`当前课程名已被占用`的提示语句说明返回的数据流的确为`{uniqueName: true}` 。 查看数据流后,完成逻辑:若existsByName返回的值为true,说明该名称已被占用,则返回`{uniqueName: true}`;若若existsByName返回的值为false,说明该名称未被占用,则返回`null`。 course/validator/unique-name-validator.ts ```typescript .pipe(map((input) => { if (input) { return {uniqueName: true}; } else { return null; } })); ``` 重构如下: course/validator/unique-name-validator.ts ```typescript .pipe(map((input) => { return input ? {uniqueName: true} : null; })); ``` 在箭头函数中,如果函数体中仅有一行代码且以return打头,则还可以省略`{}`以及`return`进行如下缩写: ```typescript .pipe(map((input) => input ? {uniqueName: true} : null)); } ``` 同时若输入的参数个数为1,且无指定数据类型的需求时,还可以省略`()`: ```typescript .pipe(map(input => input ? {uniqueName: true} : null)); } ``` 删除回车符后变更为: ```typescript .pipe(map(input => input ? {uniqueName: true} : null)); ``` ### 测试 首先添加教师及班级基本数据,然后添加一个名称为test的班级。接着刷新页面,重新输入班级名称test ![](https://img.kancloud.cn/c2/e6/c2e6bae8a1caeae9c93f63ff041c586d_491x187.png) 测试通过。 # 单元测试 最后执行`ng test`对全局进行测试。 ``` ERROR in src/app/course/validator/unique-name-validator.spec.ts:5:12 - error TS2554: Expected 1 arguments, but got 0. 5 expect(new UniqueNameValidator()).toBeTruthy(); ~~~~~~~~~~~~~~~~~~~~~~~~~ src/app/course/validator/unique-name-validator.ts:16:15 16 constructor(courseService: CourseService) { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An argument for 'courseService' was not provided. ``` 在unique-name-validator.spec.ts中发生了语法错误: course/validator/unique-name-validator.spec.ts ```typescript describe('course -> validator -> niqueNameValidator', () => { it('should create an instance', () => { const courseService = new CourseStubService() as CourseService; expect(new UniqueNameValidator(courseService)).toBeTruthy(); }); }); ``` 错误: ![](https://img.kancloud.cn/62/0c/620c953e487482627aeab4fc8ec96834_653x147.png) 这是由于更新CourseService却没有对应更新其测试替身的原因造成的。 service/course-stub.service.ts ```typescript existsByName(name: string): Observable<boolean> { return of(false★); } ``` 再次测试全部通过。 >[success] ★如果将此处的返回值修改为true,则会触发其它2个单元测试的错误,你知道这是为什么吗? # 参考文档 | 名称 | 链接 | 预计学习时长(分) | | --- | --- | --- | | 源码地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.9](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step6.1.9) | - | | 异步验证器 | [https://www.angular.cn/guide/form-validation#async-validation](https://www.angular.cn/guide/form-validation#async-validation) | 10 | | map操作符 | [https://cn.rx.js.org/class/es6/Observable.js~Observable.html#instance-method-map](https://cn.rx.js.org/class/es6/Observable.js~Observable.html#instance-method-map) | 5 |