# 数据验证 本节我们对新建班级时的班级名称进行非空验证,力求达到以下目标: 1. 模拟API模拟后台对数据进行验证。 2. 班级名称为空时,不允许点击提交按钮。 3. 未选择班主任时,不允许点击提交按钮。 ### MockApi 一个好的MockApi往往会处理一些逻辑,特别是在数据的校验方面。而完成数据校验的前提是获取到这些数据。在第2小节中我们引入了能够模似后台API的MockApi,版本为`0.0.3`。本节中我们需要一些`0.0.5`版本的新特性,则需要对原依赖进行一些升级操作。 ## 升级依赖 维护一个项目使其**不过时**是一种负责任的行为。相信在大家学习此教程时,执行`npm install`或多或少的都会看到一些依赖过时或有风险的提示: ```bash found 2 moderate severity vulnerabilities run `npm audit fix` to fix them, or `npm audit` for details ``` 升级这些依赖的方法有很多,在此给出一种虽然笨但有效的方法。 我们知道`package.json`中控制着当前项目的所有依赖,那么我们当然可以简单的直接修改`package.json`中相关依赖的代码了。比如我们打开`package.json`将MockApi依赖由`0.0.3`版本修改为`0.0.5`版本。 ```json +++ b/first-app/package.json @@ -20,7 +20,7 @@ "@angular/platform-browser-dynamic": "~11.0.9", "@angular/router": "~11.0.9", "@fortawesome/fontawesome-free": "^5.15.2", - "@yunzhi/ng-mock-api": "0.0.3", + "@yunzhi/ng-mock-api": "0.0.5", "bootstrap": "^4.6.0", "jquery": "^3.5.1", "popper.js": "^1.16.1", ``` 然后在项目根目录中执行`npm install`来重新安装项目依赖,此时`@yunzhi/ng-mock-api`的版本便成功更新于`0.0.5`了。 使用`npm install`命令时会下载一些依赖至项目根目录的`node_modules`文件夹中。我们在其下可以找到一个`@yunzhi/ng-mock-api`文件夹。 ``` panjie@panjies-Mac-Pro first-app % tree node_modules/@yunzhi -L 2 node_modules/@yunzhi └── ng-mock-api ├── README.md ├── bundles ├── esm2015 ├── fesm2015 ├── lib ├── package.json 👈 ├── public-api.d.ts ├── testing ├── yunzhi-ng-mock-api.d.ts └── yunzhi-ng-mock-api.metadata.json ``` 该文件夹中的`package.json`记录着当前依赖的具体信息,也包括版本号。 ```json "sideEffects": false, "typings": "yunzhi-ng-mock-api.d.ts", "version": "0.0.5" 👈 } ``` 除此以外,还可以搜索`npm update package`或`npm update dependencies`来获取更多关于升级项目依赖的知识: ![image-20210319144803328](https://img.kancloud.cn/cb/68/cb684dc14cb076a97e990c43981b49d9_2094x184.png) ## 数据校验 创建MockApi时,我们将模拟的结果放到了`result`属性上: ```typescript class ClazzMockApi implements MockApiInterface { getInjectors(): ApiInjector<any>[] { return [ { method: 'POST', url: 'clazz', result: { 👈 id: 1, name: '保存的班级名称', createTime: 1234232, teacher: { id: 1, name: '教师姓名' } } } ]; } } ``` `result`属性除可设置为一般的变量以外,还可以将其设置为函数: ```typescript result: () => { return { id: 1, name: '保存的班级名称', createTime: 1234232, teacher: { id: 1, name: '教师姓名' } }; } ``` 这两种写法效果一致,运行单元测试我们当得到相同的结果: ![image-20210319093210533](https://img.kancloud.cn/2b/00/2b00fe07a1d2cda0e0ecc17f151d2393_1244x198.png) 但设置为函数时,我们在函数上指定相关参数,这些参数的值可以帮助我们实现更靠近其实环境的逻辑: ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -1,7 +1,7 @@ import {AddComponent} from './add.component'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; -import {ApiInjector, MockApiInterceptor, MockApiInterface} from '@yunzhi/ng-mock-api'; +import {ApiInjector, MockApiInterceptor, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api'; import {FormsModule} from '@angular/forms'; describe('clazz add with mockapi', () => { @@ -42,7 +42,7 @@ class ClazzMockApi implements MockApiInterface { { method: 'POST', url: 'clazz', - result: () => { + result: (urlMatches: string[], options: RequestOptions) => { return { id: 1, name: '保存的班级名称', ``` 其中`urlMatches`为请求URL的相关信息,`options`包括了请求主体、请求headers等信息。我们此时预实现请求的数据验证功能,则正好需要这个请求主体: ```typescript result: (urlMatches: string[], options: RequestOptions) => { + console.log('接收到了数据请求,请求主体的内容为:', options.body); ``` ![image-20210319151625649](https://img.kancloud.cn/9f/cf/9fcf4718544b388233896f5a643b66cb_1318x230.png) 数据接收到后:当数据符合要求,则返回数据;如果不符合要求,则抛出一个异常。 ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -44,6 +44,15 @@ class ClazzMockApi implements MockApiInterface { url: 'clazz', result: (urlMatches: string[], options: RequestOptions) => { console.log('接收到了数据请求,请求主体的内容为:', options.body); + const clazz = options.body; + if (!clazz.name || clazz.name === '') { + throw new Error('班级名称未定义或为空'); + } + + if (!clazz.teacher || !clazz.teacher.id) { + throw new Error('班主任ID未定义'); + } + return { id: 1, name: '保存的班级名称', ``` 该异常将导致新班级保存失败: ![image-20210319152607494](https://img.kancloud.cn/9f/07/9f0775ff90a336fd49d06dd61263ea2f_1032x106.png) ### 完善模拟数据 下面我们再对模拟返回数据进行修正,让其返回更**合理**的模拟数据: ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -1,7 +1,7 @@ import {AddComponent} from './add.component'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; -import {ApiInjector, MockApiInterceptor, MockApiInterface, RequestOptions} from '@yunzhi/ng-mock-api'; +import {ApiInjector, MockApiInterceptor, MockApiInterface, randomNumber, RequestOptions} from '@yunzhi/ng-mock-api'; import {FormsModule} from '@angular/forms'; describe('clazz add with mockapi', () => { @@ -52,13 +52,13 @@ class ClazzMockApi implements MockApiInterface { if (!clazz.teacher || !clazz.teacher.id) { throw new Error('班主任ID未定义'); } - + return { - id: 1, + id: randomNumber(), name: '保存的班级名称', - createTime: 1234232, + createTime: new Date().getTime(), teacher: { - id: 1, + id: clazz.teacher.id, name: '教师姓名' } }; ``` 使用`randomNumber()`来生成一个随机的数字作为返回班级的id,这更贴近于生产环境;`createTime`设置为当前的时间,使数据看起来更真实,而`teacher`的`id`使入接收到的`id`,也是必要的。这样一样,返回数据看起来就正常的多了。 此时,我们在测试提交的教师id时,在也不必拘泥于**张三**、**李四**对应的`id=1`,`id = 2`了。在单元测试中完全可以设置一个随机教师id: ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -29,6 +29,8 @@ describe('clazz add with mockapi', () => { }); fit('在MockApi下完成组件测试Submit', () => { + component.clazz.name = '测试班级名称'; + component.clazz.teacherId = randomNumber(); component.onSubmit(); }); }); ``` 如此一来,一个合格的后台API替身便诞生了: ![image-20210319153043698](https://img.kancloud.cn/4d/13/4d135c95ecc9cb5790067fa38a661dc6_1392x336.png) ## V层测试 在启用fixture的自动检测变更后,如果未输入班级名称或未选择教师就点击保存按钮的话,将在控制台得到相应的错误信息: ```typescript +++ b/first-app/src/app/clazz/add/add.component.mock-api.spec.ts @@ -28,11 +28,15 @@ describe('clazz add with mockapi', () => { fixture.detectChanges(); }); - fit('在MockApi下完成组件测试Submit', () => { + it('在MockApi下完成组件测试Submit', () => { component.clazz.name = '测试班级名称'; component.clazz.teacherId = randomNumber(); component.onSubmit(); }); + + fit('should create', () => { + fixture.autoDetectChanges(); + }); }); /** ``` ![image-20210319153352871](https://img.kancloud.cn/72/c9/72c9461ad1ab9bd06a929eea1e4d8e7f_1582x570.png) 这几乎与真正的后台表示无异。 ## disabled Angular的FormsModule支持button标签的disabled属性,当此属性为true时按钮不可用,为false时按钮可用。我们可以利用此特性,实现名称为空、未选择班主任时的禁止提交功能。 ```html +++ b/first-app/src/app/clazz/add/add.component.html @@ -19,7 +19,9 @@ </div> <div class="mb-3 row"> <div class="col-sm-10 offset-2"> - <button class="btn btn-primary">保存</button> + <button class="btn btn-primary" + [disabled]="clazz.name === '' || clazz.teacherId === null">保存 + </button> </div> </div> </form> ``` 此时,再启用fixture的自动检测变更后,V层中的按钮将只在班级名称不为空且选择了teacher后可用。 ![image-20210319140010101](https://img.kancloud.cn/92/3b/923bc32f17ea0839cc9c00bdc2c9c9f8_1468x314.png) 填写班级名称,并选择班主任后: ![image-20210319140036645](https://img.kancloud.cn/75/6d/756d9107cdf13616182e4b7f283aaf61_1100x328.png) 在模拟后台API的数据校验以及V层按钮disabled的表现下,我们再也不怕客户不按照我们指定的使用方式来使用系统了。 ## 本节作业 请实现以下效果: 1. 初始化时提示名称及班主任不能为空: ![image-20210319154000907](https://img.kancloud.cn/d7/aa/d7aa3b558ba78d7cbad593ba43db4f69_1478x328.png) 2. 填写完相应的内容后,提示消失: ![image-20210319140036645](https://img.kancloud.cn/75/6d/756d9107cdf13616182e4b7f283aaf61_1100x328.png) | 名称 | 链接 | | -------- | ------------------------------------------------------------ | | 本节源码 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.1.4.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.1.4.zip) |