重构是保持项目优秀的必经之路,在前面的小节中,我们发现分页模块在数据量大时表示的差强人意: ![image-20210522090115935](https://img.kancloud.cn/e1/ac/e1acb520c3475effd5d92c762184dede_1710x462.png) 上图的总页码数为21,这明显有些过长。本节我们尝试将其其默认的最大页码数设置为7。 当我们解决此类问题时,首要的任务的是重现错误。在重现错误的过程中,包括了对错误产生原因的猜想,以及最终对该猜想的验证(这个过程可能是不自觉的)。 ## 启动分页组件 我们来到分页组件所在的位置:`src/app/clazz/page`,找到其对应的单元测试文件。并添加一个测试用例: ```typescript +++ b/first-app/src/app/clazz/page/page.component.spec.ts @@ -68,4 +68,7 @@ describe('PageComponent', () => { expect(navHtml.style.visibility).toEqual('visible'); }); + fit('将总页码的最大数量控制在7页', () => { + + }); }); ``` 然后使用`ng t`来启动单元测试。 ## 分析问题 想修正或完善一些功能时,我们往往使用的按数据流的方向进行逆向推导的方法。比如当前出现的问题是分页数量过多,则应该先想看是直接生成html的V层的代码是哪些: ```html <li *ngFor="let p of pages①" [ngClass]="{active: currentPage === p}" class="page-item"> <span class="page-link" (click)="onPage(p)">{{p + 1}}</span> </li> ``` 由上述代码直接推导出,分页数据过多的原因是由于①标注的`pages`数组中的元素过多引起的。 然后接着向前推导:`pages`变量的值是由C层传过来的,则接下来应该找到`pages`变量在C层中的赋值情况: ```typescript pages: number[] = []; ① 👈 currentPage = 0; @Input() set page(page: Page<any>) { this.inputPage = page; ④ 👈 console.log('set page被调用'); console.log('当前页', this.inputPage.number); console.log('总页数', this.inputPage.totalPages); // 生成页数数组 this.pages = []; ② 👈 for (let i = 0; i < this.inputPage.totalPages; i++) { this.pages.push(i); ③ 👈 } // 设置当前页 this.currentPage = this.inputPage.number; } ``` 查阅C层的代码(可以使用ctrl+f进行快速查找),发现对`pages`变量进行设置的地点有3处;第①处为初始化、第②处为初始化,此两处操作会将`pages`清空,所以问题应该在第③处,即按照传入的`inputPage.totalPages`的值进行遍历,而`pages`最终的个数取决于`inputPage.totalPages`的值的大小。 按照逆向的理论继续向上找`inputPage.totalPages`的值是由 ④决定的,而④正好是`Input()`调用,即父组件传入。 所以最终得到以下结论:父组件向当前组件传入`page`时,将按传入的`page`对象上的`totalPages`的值的大小来初始化组件的页码。 ## 重现问题 那么如果想重现问题,则传入一个具有较大的`totalPages`的`page`对象即可,我们在测试用例中构造这个对象: ```typescript +++ b/first-app/src/app/clazz/page/page.component.spec.ts @@ -69,6 +69,11 @@ describe('PageComponent', () => { }); fit('将总页码的最大数量控制在7页', () => { - + component.page = { + number: 2, + size: 20, + totalPages: 20 + } as Page<any>; + fixture.autoDetectChanges(); }); }); ``` 问题成功被重现: ![image-20210607093427541](https://img.kancloud.cn/4a/b5/4ab56ec002309f298d8fcaacf20cf129_2500x302.png) ## 单元测试 在解决问题时,我们常常会“故此失彼”。所以在动手前,尽可能多的考虑一些情况是非常有必要的。我们在此给出几种分页情况: 第一种:当总页数小于7时能够准确显示,比如:`1 2 3 [4] 5`、`1 [2] 3` 第二种:总页数等于7时,比如:`1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]` 第三种:总页数大于7页,需要能够正确的处理以下各种情况: 第1页,共8页:`[1] 2 3 4 5 6 7` 第3页,共8页:`1 2 [3] 4 5 6 7` 第4页,共8页:`1 2 3 [4] 5 6 7` 第10页,共19页:`7 8 9 [10] 11 12 13` 第15页,共18页:`12 13 14 [15] 16 17 18` 第16页,共18页:`12 13 14 15 [16] 17 18` 第18页,共18页:`12 13 14 15 16 17 [18]` ### 测试用例 为了避免不小心把哪个功能给遗漏掉或KILL掉,可以为每个小功能点建立一个测试用例,然后使用断言的方法来对功能进行保证。在完善功能后可以统一的执行单元测试,当每个单元测试都顺利通过时,就说明整个功能开发的没有问题了。 #### 总页码小于7 `1 2 3 [4] 5`、`1 [2] 3` ```typescript fit('总页码小于7', () => { // 共5页,当前第4页 component.page = { number: 3, size: 20, totalPages: 5 } as Page<any>; fixture.detectChanges(); expect(component.pages.length).toBe(5); // 共3页,当前第2页 component.page = { number: 1, size: 20, totalPages: 3 } as Page<any>; fixture.detectChanges(); expect(component.pages.length).toBe(3); }); ``` 测试通过,说明当前代码可以满足此要求。无需对代码进行改动。 #### 总页数等于7时 `1 2 3 4 [5] 6 7`、`1 2 3 4 5 6 [7]` ```typescript fit('总页码等于7', () => { // 共7页,当前第5页 component.page = { number: 4, size: 20, totalPages: 7 } as Page<any>; fixture.detectChanges(); expect(component.pages.length).toBe(7); // 共7页,当前第7页 component.page = { number: 6, size: 20, totalPages: 7 } as Page<any>; fixture.detectChanges(); expect(component.pages.length).toBe(7); }); ``` 测试通过,说明当前代码可以满足此要求。无需对代码进行改动。 #### 总页数大于7页 第1页,共8页:`[1] 2 3 4 5 6 7` ```typescript fit('总页数大于7, 第1页,共8页', () => { // 共1页,当前第8页 component.page = { number: 0, size: 20, totalPages: 8 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); // 头是第1页 expect(component.pages[0]).toBe(0); // 尾是第7页 expect(component.pages.pop()).toBe(6); }); ``` 发生异常如下: ![image-20210607100555414](https://img.kancloud.cn/8b/d8/8bd86a4a7d634ac7076b889c0b4215b3_950x128.png) 这说明当前的代码已经不能够满足当前需求了。 虽然单元测试报错了,但我们并不着急下手写代码。这是由于只有当充分的了解所有的需求后,再上手写代码才是效率最高做无用功最少的。 第3页,共8页:`1 2 [3] 4 5 6 7` ```typescript fit('1 2 [3] 4 5 6 7', () => { // 共3页,当前第8页 component.page = { number: 2, size: 20, totalPages: 8 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); // 头是第1页 expect(component.pages[0]).toBe(0); // 尾是第7页 expect(component.pages.pop()).toBe(6); }); ``` 第4页,共8页:`1 2 3 [4] 5 6 7` ```typescript fit('1 2 3 [4] 5 6 7', () => { // 共4页,当前第8页 component.page = { number: 3, size: 20, totalPages: 8 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); // 头是第1页 expect(component.pages[0]).toBe(0); // 尾是第7页 expect(component.pages.pop()).toBe(6); }); ``` 第10页,共18页:`7 8 9 [10] 11 12 13` ```typescript fit('7 8 9 [10] 11 12 13`', () => { component.page = { number: 9, size: 20, totalPages: 18 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); expect(component.pages[0]).toBe(6); expect(component.pages.pop()).toBe(12); }); ``` 第15页,共18页:`12 13 14 [15] 16 17 18` ```typescript fit('12 13 14 [15] 16 17 18', () => { component.page = { number: 14, size: 20, totalPages: 18 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); expect(component.pages[0]).toBe(11); expect(component.pages.pop()).toBe(17); }); ``` 第16页,共18页:`12 13 14 15 [16] 17 18` ```typescript fit('12 13 14 15 [16] 17 18', () => { component.page = { number: 15, size: 20, totalPages: 18 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); expect(component.pages[0]).toBe(11); expect(component.pages.pop()).toBe(17); }); ``` 第18页,共18页:`12 13 14 15 16 17 [18]` ```typescript fit('12 13 14 15 16 17 [18]', () => { component.page = { number: 17, size: 20, totalPages: 18 } as Page<any>; fixture.detectChanges(); // 共显示7页 expect(component.pages.length).toBe(7); expect(component.pages[0]).toBe(11); expect(component.pages.pop()).toBe(17); }); ``` ## 解决问题 所有的单元测试都写完后,现在开始解决问题。这种先写单元测试再写功能代码的方法被称为:`TDD`,学名叫做测试驱动开发,即非常出名的 Test-Driven Development。 写了这么多单元测试用例以后,我使用以下代码来尝试解决当前最大数量为7的问题。 > [info] 建议先自己写写,最后再参考教程中的实现代码。相信教程中的代码也不是最简洁的,期待你给出更加简洁的实现方案。 ```typescript +++ b/first-app/src/app/clazz/page/page.component.ts @@ -22,10 +22,33 @@ export class PageComponent implements OnInit { console.log('set page被调用'); console.log('当前页', this.inputPage.number); console.log('总页数', this.inputPage.totalPages); + // 初始化最大页码,起始页码 + let maxCount; + let begin; + + if (this.inputPage.totalPages > 7) { + // 大于7页时,仅显示7页 + maxCount = 7; + + // 起始页为当前页-3.比如当前页为10,则应该由7页开始 + begin = this.inputPage.number - 3; + if (begin < 0) { + // 判断是否越界,可以删除下一行代码查看错误的效果 + begin = 0; + } else if (begin > this.inputPage.totalPages - 7) { + // 判断是否越界,可以删除下一行代码查看错误的效果 + begin = this.inputPage.totalPages - 7; + } + } else { + // 小于等于7页时,使用原算法。页码数为总页数,页码由0开始 + maxCount = this.inputPage.totalPages; + begin = 0; + } + // 生成页数数组 this.pages = []; - for (let i = 0; i < this.inputPage.totalPages; i++) { - this.pages.push(i); + for (let i = 0; i < maxCount; i++, begin++) { + this.pages.push(begin); } ``` 最终所有的单元测试全部通过,说明满足了所有的要求。 ![image-20210607103851567](https://img.kancloud.cn/bc/9a/bc9a6aae5296523d7421d0477e349949_1786x550.png) 最后移除所有的`fit`,看单元测试是否都成功通过。成功通过则说明我们当前的功能完善未对其它的组件造成影响。 ## 总结 本节中我们使用了`TDD`测试驱动开发的思想,先写了单元测试用例,最后补功的功能代码。这特别适用于某些输入与输出都比较简单,但逻辑实现稍微复杂的方法。 其实所有的方法都不是万能的,TDD的开发思想虽然好,但却并不适合于新手。但如若一直把自己当前新手来看待,将可能永远也用不到TDD的开发思想。所以我们建议是,在单元测试这里,看自己的能力能写多少写多少,在写的过程中,如果感觉特别难就换一种方式。写单元测试代码时间应该不大于写功能代码的2倍,如果时间超过了2倍,则应该考虑减小单元测试的难度。对于是先书功能代码还是单元测试代码的问题,则应该:哪个容易写哪个。 | 链接 | 名称 | | ------------------------------------------------------------ | --------------- | | [https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step7.4.5.zip) | 本节源码 | | [https://baike.baidu.com/item/TDD/9064369](https://baike.baidu.com/item/TDD/9064369) ---- 注意:它的视频错了 | TDD测试驱动开发 | | [https://zh.wikipedia.org/zh-hans/%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91](https://zh.wikipedia.org/zh-hans/%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91) | TDD测试驱动开发 |