本节我们展示一种更加简单的批量删除方法,它的代码量并没有增加,但思索量却会小很多。无论你是科班出身,还是通过其它途径加入到计算机科学与工程这个领域的,相信一定都学习过面向对象。而且,我们当前的Angular便是完全的面向对象的思想。提到面向对象,大家脑海中想到的最多的可能就是定义几个类、几个接口、将方法与属性封装起来等。
如果面向对象仅仅是定义几个类、几个接口,那显然不足以支撑它如此大的名气。本节中,我们将以面向对象的思想重写上节中的删除功能。
## Student对象
在学生列表中,每行学生其实对应了一个学生对象。
![image-20210608081519817](https://img.kancloud.cn/97/39/973915b2bcfb105cfa4d64209ba4c824_2514x218.png)
显示的学生的姓名、学号等信息则可以认为是学生的属性,而每行这个小小的选择框可以被点击与用户进行交互 ,则可以认为是学生对象的方法。
那么是否可以按面向对象的思想,为Student加一个删除被点击的方法呢?
## 建立方法
找到`Student`实体,添加一个删除被点击的方法如下:
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -42,4 +42,8 @@ export class Student {
this.email = data.email as string;
this.clazz = data.clazz as Clazz;
}
+
+ public onDeleteClick(): void {
+ console.log('delete click');
+ }
}
```
我们接下来修改学生列表组件的V层,测试一下:
```html
+++ b/first-app/src/app/student/student.component.html
@@ -20,7 +20,7 @@
</thead>
<tbody>
<tr *ngFor="let student of pageData.content; index as index">
- <td><input type="checkbox" (click)="onCheckboxClick(index)"></td>
+ <td><input type="checkbox" (click)="student.onDeleteClick()"></td>
<td>{{index + 1}}</td>
<td>{{student.name}}</td>
<td>{{student.number}}</td>
```
当点击选择框时,在控制台得到了如下异常:
![image-20210608082934788](https://img.kancloud.cn/ba/9c/ba9c1c8ee1b87483c7d4f7f391fff2e3_1796x434.png)
提示说:`onDeleteClick`不是一个方法。无论我们怎么检查语法或是重启`ng t`,都将还是这个错误。
要想彻底的弄清楚这个问题,还要深入学习下`JSON对象`与`对象`间的区别。
## 测试代码
学习计算机最大的优势在于其实验成本极低。相较于土木、化工、机器、汽车等传统行业,我们可以使用极低的成本来验证自己的想法。这种极低的成本当然也可以应用到代码示例上。
我们在`student.service.spec.ts`中新建一个测试方法,以代码的方式深入学习下`JSON对象`与`对象`的区别:
首先,我们建立一个测试用例:
```typescript
fit('JSON对象与对象', () => {
});
```
然后在测试用例中新建一个`Test`类,该类有两个属性`id`与`name`:
```typescript
fit('JSON对象与对象', () => {
class Test {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
});
```
接着,为该类增一个`sayHello()`方法:
```typescript
fit('JSON对象与对象', () => {
class Test {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
sayHello(): void {
console.log('hello');
}
}
});
```
基础的准备工作做完以后,我们依据该类建立一个对象,并调用对象上的`sayHello()`方法:
```typescript
+++ b/first-app/src/app/service/student.service.spec.ts
@@ -81,5 +81,11 @@ describe('StudentService', () => {
console.log('hello');
}
}
+
+ const test = new Test(1, '123');
+ console.log(test.id);
+ console.log(test.name);
+ test.sayHello();
});
});
```
单元测试通过,在`bash`中打印了对象上的属性`id`、`name`的值,并且成功的调用了`sayHello`方法:
```bash
LOG: 1
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: '123'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: 'hello'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
```
> `bash`与浏览器控制台查看打印信息大同小异。
### JSON对象
接下来,我们再尝试增加一个`JSON对象`:
```typescript
test.sayHello();
+
+ const jsonString = '{"id": 2, "name": "456"}';
+ const jsonTest = JSON.parse(jsonString) as Test;
+ console.log(jsonTest.id);
+ console.log(jsonTest.name);
+ jsonTest.sayHello();
});
```
此时控制台在执行`sayHello()`方法时得到了一个异常:
```bash
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: 2
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
LOG: '456'
Firefox 89.0 (Mac OS 10.15): Executed 0 of 66 SUCCESS (0 secs / 0 secs)
Firefox 89.0 (Mac OS 10.15) StudentService JSON对象与对象 FAILED
TypeError: jsonTest.sayHello is not a function in main.js (line 2385)
```
该异常类型与我们在学生对象上调用`onDeleteClick()`方法的错误类型相同,异常得以重现:
![image-20210608082934788](https://img.kancloud.cn/ba/9c/ba9c1c8ee1b87483c7d4f7f391fff2e3_1796x434.png)
上述异常的关键点在于在声明`jsonTest`时,我们使用了`JSON.parse(jsonString) as Test;`而这个`as`代表`看做`。潜台词是:你可能并不是这个类型,但是没有关系,我在这把你看做这个类型。这种`看做`的方式被编译器认同,所以在编译的阶段可以顺利通过。
我们刚刚使用 `JSON.parse(jsonString) as Test;`来将字符串`jsonString`转换为了一个对象,该对象使用`as`关键字并声明为`Test`。而我们把这种通过字符串转换过来的对象,称为`JSON对象`。
字符串只所以可以通过`JSON.parse(jsonString)`来转换为JSON对象,是由于该字符串符合转换为`JSON对象`的规范:
![image-20210608100049930](https://img.kancloud.cn/01/a6/01a61d44dca8a263fec497818e001c5d_1174x182.png)
如果不符合转换为`JSON对象`的规范,则会发生转换异常:
![image-20210608100208061](https://img.kancloud.cn/d6/a8/d6a8201c8e63595cd5d265ace5d9a8ec_1480x238.png)
也就是说:只有符合`JSON对象`转换规范的字符串才能够使用`JSON.parse()`方法将其转换为对象。我把这种符合转换规范的字符串称为`JSON字符串`,而通过`JSON字符串`转换过来的对象称为`JSON对象`。
最后我们注释掉删除刚刚触发异常的代码后继续学习。
```typescript
- jsonTest.sayHello();
+ // jsonTest.sayHello();
```
### as
往往为了开发方便,我们还会根据`JSON字符串`的内容使用`as`关键字将其声明为某个特定的类型。比如我们常用的`httpClient.get<T>`中的`T`便是这个作用。此处的泛型`T`仅仅是说,可以把后台返回的数据看做是`T`,该`T`的类型本质上是个`JSON对象`,其仅具体`T`类型的属性,但却不具体`T`类型的方法。
由于在前台的交互过程中,我们并没有办法对后台的代码进行约束。所以在与后台对接时,只能是按照API的规范将返回值看做某个类型,而后台具体返回的是不是这个类型,还需要在真实的与后台对接时才能够判断出。
在这种特定的场景下,`as`是最适用不过的了。但由于`as`在类型上的灵活性,应该避免滥用,比如我们借助于`as`,完全可以这么写:
```typescript
const a = '123' as any as number[];
a.push(123);
```
这种写法在编译时同样不会出错,但在运行时则必然出错。
## HttpClient.get<T>()
以`HttpClient`的`get<T>()`方法为例,其实质的作用是进行Http请求,然后将请求的结果在内部使用` JSON.parse()`以及`as`关键字将其**看做**`T`返回。所以使用`httpClient.get<T>()`等方法得到的数据的本质上是个不具有任何方法的`JSON对象`。
所以若要使用`Student`类上的`onDeleteClick()`方法,则需要将`JSON对象`转换为`对象`。同时由于我们早早的就在`Stduent`类中声明了如下构造函数:
```typescript
constructor(data = {} as
{
id?: number,
name?: string,
number?: string,
phone?: string,
email?: string,
clazz?: Clazz
}) {
```
该函数中的参数`data`恰恰也是通过`as`关键字来**看做**一个**对象**,被看做的**对象**上只拥有属性(id, name,...)而不具备任何方法,所以`data`完全可以看做是一个`JSON对象`。
也就是说,通过`Student`的构造函数,可以将一个`JSON对象`转换为`对象`。
思想有了,写代码便成了最简单的事情:
```typescript
+++ b/first-app/src/app/student/student.component.ts
@@ -32,7 +32,10 @@ export class StudentComponent implements OnInit {
this.studentService.pageOfCurrentTeacher({
page,
size: this.size
- }).subscribe(data => this.pageData = data);
+ }).subscribe(data => {
+ data.content = data.content.map(d => new Student(d));
+ this.pageData = data;
+ });
}
```
上述语句便使得`data.content`中的每一项都是一个Student对象,而不是看做Stduent对象的JSON对象了。
此时,当我们再次点击某条数据前的选择框时在控制台得到了预期的效果:
```typescript
LOG: 'delete click'
```
## 完善功能
对象中的方法被触发后,我们在`Student`类中再增加一个以`_`打头的属性,以`_`打头代表该属性该属性与后台不对接,是一个前台的特有属性。
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -4,6 +4,11 @@ import {Clazz} from './clazz';
* 学生.
*/
export class Student {
+ /**
+ * 是否被选中
+ */
+ _checked = false;
+
```
此时我们使用的IDE将报一个语法错误:
![image-20210608111526422](https://img.kancloud.cn/db/18/db1861278a41f4eb2115483cb4d7cc84_2228x406.png)
它在说`TSLInt`报了一个语法错误:变量的名字必须是以下三种(小驼峰、大驼峰、大写字母和下划线)情况之一。
## TSLint
在编程的世界时,以`lint`打头的大多的作用都是语法检查。所以如果你使用了是其它编程的语言,也可以使用`lint`后缀来查找相应的语法检查器,比如`PHPLint`或是`JavaLint`。
所以`TSLint`顾名思义它是一个`typescript`的语法检查器。
同时每个语法检查器都会有一个相应的配置文件,而`TSLint`的配置文件则是位于项目根目录的`tslint.json`:
```bash
panjie@panjies-iMac first-app % tree -L 1
.
├── README.md
├── angular.json
├── e2e
├── karma.conf.js
├── node_modules
├── package-lock.json
├── package.json
├── src
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json 👈
```
该文件中对在`rules -> variable-name -> options`上对变量名做出了限制,在此我们增加一项:允许变量名以下划线`_`打头:
```json
+++ b/first-app/tslint.json
@@ -108,7 +108,8 @@
"options": [
"ban-keywords",
"check-format",
- "allow-pascal-case"
+ "allow-pascal-case",
+ "allow-leading-underscore"
]
},
```
此时`Stduent`中的`_checked`字段的语法错误将会自动消失。
## 完善功能
有了`_checked`属性后,便可以在`onDeleteClick()`方法来改变这个属性:
```typescript
+++ b/first-app/src/app/entity/student.ts
@@ -49,6 +49,6 @@ export class Student {
}
public onDeleteClick(): void {
- console.log('delete click');
+ this._checked = !this._checked;
}
}
```
然后在组件中根据该属性遍历出要删除的学生:
```typescript
b/first-app/src/app/student/student.component.ts
/**
* 批量删除按钮被点击
*/
onBatchDeleteClick(): void {
const beDeleteIds = this.pageData.content.filter(s => s._checked).map(d => d.id); ①
if (beDeleteIds.length === 0) {
Report.warning('出错啦', '请先选择要删除的学生', '返回');
} else {
Confirm.show('请确认', '该操作不可逆', '确认', '取消',
() => {
// 调用批量删除
this.studentService.batchDelete(beDeleteIds)
.subscribe(() => {
this.loadData(this.page);
});
});
}
}
```
①中我们连续使用了`filter()`以及`map()`方法。最终得到了一个待删除的id数组。
![image-20210608114419061](https://img.kancloud.cn/70/a9/70a9c4c99fab3412776781cd79957a05_916x184.png)
同时,此时原组件中的`onCheckboxClick()`等已经完成了历史使命,可能退出历史舞台了:
```typescript
+++ b/first-app/src/app/student/student.component.ts
@@ -15,8 +15,6 @@ export class StudentComponent implements OnInit {
page = 0;
size = environment.size;
- beDeletedIndexes = new Array<number>();
-
constructor(private studentService: StudentService) {
}
@@ -58,18 +56,6 @@ export class StudentComponent implements OnInit {
this.loadData(this.page);
}
- /**
- * checkbox被点击
- * @param index 索引值
- */
- onCheckboxClick(index: number): void {
- if (this.beDeletedIndexes.indexOf(index) === -1) {
- this.beDeletedIndexes.push(index);
- } else {
- this.beDeletedIndexes = this.beDeletedIndexes.filter(i => i !== index);
- }
- }
-
/**
* 批量删除按钮被点击
*/
```
最后进行单元测试及集成测试,功能正常。
## 小结
本节我们深入学习了`JSON对象` 与`对象`,两者的区别大概可以概括为:JSON对象是长的像对象的数据集合,它就像一个`1:1`的汽车模型,除了不会动以外真实汽车有的它都有;有对象就是一个真正的汽车,该汽车除了有方向盘、发动机等属性外,还可以在公路上驰骋。
面向对象的思想在于一切皆对象,面象对象的封装性决定了其是属性与方法的混合体。属性是其对象内部的各个状态,比如一个人的年龄、身高都是属性,而方法则是该对象具有的功能。
在计算机的世界时,实践出真知永不过时。从来没有一门学问是有捷径可以走的,变道超车也仅仅停留在记者的新闻稿里。我们相信,脚踏实地就是最佳的捷径。在这条捷径上学而时习之就是我们最有效的学习方法。
> 学而时习之,不亦说乎:时常能够使用学习到的知识来指导实践,不是一件令人心生喜悦的事吗?
## 本节资源
| 链接 | 名称 |
| ------------------------------------------------------------ | ------------------- |
| [https://github.com/mengyunzhi/angular11-guild/archive/step7.5.1.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.5.1.zip) | 本节源码 |
| [https://zhuanlan.zhihu.com/p/29119549](https://zhuanlan.zhihu.com/p/29119549) | JSON对象与对象 |
| [https://palantir.github.io/tslint/](https://palantir.github.io/tslint/) | TSLint |
| [https://eslint.org/](https://eslint.org/) | ESLint |
| [https://ts.xcatliu.com/basics/type-assertion.html](https://ts.xcatliu.com/basics/type-assertion.html) | Typescript 类型断言 |
- 序言
- 第一章 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 发布部署
- 第九章 总结