记一位用2000个表单实现表格的逆天同事

6,396 阅读8分钟

前言

为了隐私保护,某些功能会进行字母模糊处理。

本文使用到的主要技术是Angular

在两年前,公司入职了一位“曾经在公司就职,后跳槽,现在又回公司”的同事,以下简称为Z。

那个时候我初入社会阅历不足,还以为是在外深造回来报答公司的大佬。

我老婆后来的评价则是:“他离开你们公司,如果混得好的话为什么要回来?”

Z加入了我所负责的公司后台项目,但基本不会与我同时负责一个模块,都是各做各的,公司也没有代码审核,所以我从未实际了解过他的技术和水平。

同时Z向我提问的时候也从未称呼过我的名字,都是用“诶”来代替,让我非常不舒服,我也一度用“大佬的高傲”来说服自己不要小心眼。

直到这件事的发生……

初见端倪

在一起共事了半年后,Z转到了另一个项目,我便独自负责后台开发以及对维护他所写的功能。

其中有一块功能模块叫“ZC设计”(也是今天的主角),代码极其复杂也没有任何注释,我只是偶尔进行一些细节的添加和修改,也并没有去专门研读过代码的含义(也不太想去读)。

在某一天产品经理告诉我,ZC设计卡死了,只有某条数据会卡死

要知道我们的系统虽然有点儿屎山,但从未出现过卡死甚至卡顿,我眉头一皱感觉事情并不简单。

进入产品所给的链接后,我的浏览器也成功卡死了……

因为项目只有我一个人负责,所以我不得不担起重任,开始第一次认真阅读ZC设计的代码

直面屎山

ZC设计中有一个功能叫主料,用于记录衣服的各种信息,大约十几个字段,一个ZC设计可能有几十条主料。

而ZC设计可以看到所有主料,并对主料的各种信息进行增删查改。

有一定相关经验的人到这里应该已经想到如何实现了,这不是用一个表格就可以实现了吗

当我真正看了一遍代码之后,给我直接整懵逼了。

即使代码极其复杂臃肿,我也能看出来,实现的方式是给每一行的每一个字段设置了一个带有form表单的Input框,如果有120条数据,那么这个页面仅主料就有接近2000个表单

让我们来看看他具体是怎么实现的

原来的屎山代码已经被我重写了,所以这里使用的是另一个未重写(因为没卡死过)的相似页面的代码进行举例,原代码只会比这个更复杂臃肿。

首先,Z创建了一个表单组sizeFieldsForm

public sizeFieldsForm: FormGroup;
​
constructor(
    private formBuilder: FormBuilder
)

然后在页面加载的时候初始化

ngOnInit(): void {
    this.sizeFieldsForm = this.formBuilder.group({
        sizeFields: this.formBuilder.array([]),
    });
}

编写了一个获取表单、获取数据的方法

getSizeFormFields(): FormArray {
    return this.sizeFieldsForm.get('sizeFields') as FormArray;
}

Z根据表单的数据处理编写了方法,例如:

新增数据操作

addNewSizeFields(fieldIndex: any): void {
    if (fieldIndex == 'row') {  // 新建空白行
        this.getSizeFormFields().push(this.newSizeField(null));
    } else if (typeof fieldIndex == 'object') { // 根据接口数据动态创建form
        this.getSizeFormFields().push(this.newSizeField(fieldIndex));
    } else {
        // 复制当前行增加新一行数据
        const copyFields = this.getSizeFormFields().controls[fieldIndex].value;
        this.getSizeFormFields().push(this.newSizeField(copyFields));
    }
}

插入数据操作

spliceSizeFields(fieldIndex: any): void {
    const data = this.sizeFieldsForm.value.sizeFields;
    data.splice(fieldIndex + 1, 0, {
        id1: "",
        id2: "",
        id3: "",
        id4: "",
        id5: "",
        id6: "",
        id7: "",
        id8: "",
        id9: "",
        position: 0
    });
    this.saveData();
}

删除一行数据

removeSizeField(fieldIndex: any): void {
    this.getSizeFormFields().removeAt(fieldIndex);
}

在读取接口数据的时候,Z是如何将数据添加进表单的呢?请看

// 获取数据
const ccData = materialsData.cc_text;
​
if (ccData && ccData.length > 0) {
    ccData.forEach(item => {
        this.addNewSizeFields(item);
    });
} else { // 无数据初始化空白行
    for (let i = 0; i < 5; i++) {
        this.addNewSizeFields('row');
    }
}

通过遍历将数据一条一条插进去……

但是在我重写的屎山代码中,它是将每个信息一个一个赋值表单的

也就是120条主料的数据,在数据加载的时候它会执行2000次赋值表单操作

并且在每次数据操作的时候都会进行刷新读取,再次执行这2000次操作

哥们儿,你在干什么?

刚刚我所展示是仅仅是表格数据(虽然它并不是一个表格,但是这样的描述可能更好理解),这个表格还有一个动态可以自由填写的表头。

我们的Z大佬是这样写的:

size1 = new FormControl('xs'); // 自定义size
size2 = new FormControl('s'); // 自定义size
size3 = new FormControl('m'); // 自定义size
size4 = new FormControl('l'); // 自定义size
size5 = new FormControl('xl'); // 自定义size
size6 = new FormControl('2xl'); // 自定义size
size7 = new FormControl('3xl'); // 自定义size

啪的一下先来7个表单,然后写7条毫无意义的注释。

通过接口进行赋值:

if (materialsData.size_title.size1) { 
    this.size1.setValue(materialsData.size_title.size1); 
}
if (materialsData.size_title.size2) { 
    this.size2.setValue(materialsData.size_title.size2);
}
if (materialsData.size_title.size3) { 
    this.size3.setValue(materialsData.size_title.size3); 
}
if (materialsData.size_title.size4) { 
    this.size4.setValue(materialsData.size_title.size4);
}
if (materialsData.size_title.size5) { 
    this.size5.setValue(materialsData.size_title.size5); 
}
if (materialsData.size_title.size6) { 
    this.size6.setValue(materialsData.size_title.size6); 
}
if (materialsData.size_title.size7) { 
    this.size7.setValue(materialsData.size_title.size7); 
}

最后再来看看html代码,反正我是看的头痛,不知道你们看的头不头痛。

<!-- 表格部分 -->
<div class="purchase_table table wrap">

    <h2> 尺寸表 <span style="font-size:14px;margin-left: 10px;">( 模板要求不超过60行,超过行数无法保存到excel )</span> </h2>
    <!-- 表格主体部分 -->
    <div class="box_header list_box" fxLayout="row" fxLayoutAlign="space-around center">
        <span class="id1" fxFlex="10">产品部件</span>
        <span class="id2" fxFlex="10">部位</span>

        <mat-form-field class="id3" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size1" placeholder="输入尺码/x/L" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id4" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size2" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id5" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size3" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id6" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size4" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id7" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size5" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id8" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size6" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>

        <mat-form-field class="id8" appearance="outline" fxFlex="10">
            <input matInput [formControl]="size7" placeholder="输入尺码" style="text-align: center;">
        </mat-form-field>
        <span class="id10" fxFlex="10">操作</span>
    </div>

    <form [formGroup]="sizeFieldsForm">
        <div formArrayName="sizeFields">
            <ng-container *ngFor="let item of getSizeFormFields().controls; let sizeFieldIndex = index ">
                <ng-container [formGroupName]="sizeFieldIndex">
                    <div class="list_box" fxLayout="row" fxLayoutAlign="space-around center">
                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id1">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id2">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id3" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id4" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id5" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id6" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id7" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id8" style="text-align: right;">
                        </mat-form-field>

                        <mat-form-field appearance="outline" fxFlex="10">
                            <input matInput formControlName="id9" style="text-align: right;">
                        </mat-form-field>

                        <div fxFlex="10" fxLayout="row" fxLayoutAlign="center center">
                            <a (click)="copyData(sizeFieldIndex)">复制</a>
                            <a class="ml-12" (click)="pasteData(sizeFieldIndex)">粘贴</a>
                            <a class="ml-12" (click)="spliceSizeFields(sizeFieldIndex)">插入</a>
                            <a class="ml-12" (click)="removeSizeField(sizeFieldIndex)"
                               style="color: red;">删除</a>
                        </div>
                    </div>
                </ng-container>

            </ng-container>

        </div>
        <div fxLayout="row" fxLayoutAlign="center center" style="margin-top:15px;">
            <button mat-raised-button class="mat-accent mt-5" (click)="addNewSizeFields('row')"> + 添加数据</button>
        </div>
    </form>

</div>

我的实现

之所以写下这篇博客,是因为我最近的重写项目已经完成了ZC设计的开发,突然回想起这事儿,觉得有必要写一篇博客来好好吐槽一下。

Z的代码我重写了大概三天,是我工作生涯中最痛苦的三天,真的是精神上的究极折磨。

接下来我以上述的相同功能为例,来讲述一下我的实现代码

重写的时候领导要求尽可能不要让后端接口有所修改,所以字段名什么的就只能用原来的了

因为整个页面都存在接口的同一个字段中,所以我直接获取了该字段的数据并命名为materialsData,我所需要的数据都存在其中。

我使用的表格进行实现

第一步先实现表格

我为表格设计了两个数组,分别为字段名和默认值,通过循环的方式去进行渲染、赋值

sizes: string[] = [
    'size1',
    'size2',
    'size3',
    'size4',
    'size5',
    'size6',
    'size7',
];
sizesValue: string[] = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl'];

在获取接口数据的时候,首先获取所有数据materialsData,其次对表头进行赋值,如果为空就为默认值,否则就为接口中的值

const materialsData = res['data']['products_materials'];
this.materialsData = materialsData;
this.sizes.forEach((item: any, index: number) => {
    this.materialsData.size_title[item] = this.general.isEmpty(
        this.materialsData.size_title[item]
    )
        ? this.sizesValue[index]
    : this.materialsData.size_title[item];
});

html代码实现:

<thead>
    <tr>
        <th nzAlign="center">产品部位</th>
        <th nzAlign="center">部位</th>
        <th *ngFor="let item of sizes" nzAlign="center">
            <input class="size_input" nzSize="small" nz-input
                   [(ngModel)]="materialsData.size_title[item]" />
        </th>

        <th nzAlign="center">操作</th>
    </tr>
</thead>

到这一步,数据获取以及表头的实现就已经完成了

至于表格中的数据,那就更简单了,因为字段都是id1-di9,所以我直接创建了一个数组来存放这些字段:

ids: string[] = [
    'id1',
    'id2',
    'id3',
    'id4',
    'id5',
    'id6',
    'id7',
    'id8',
    'id9',
];

其次在表格主体中进行遍历渲染

<tbody>
    <tr *ngFor="let i of nzTable.data;let index=index">
        <td *ngFor="let id of ids" nzAlign="center">
            <input class="size_input" nzSize="small" nz-input [(ngModel)]="i[id]" />
        </td>
        <td>
            <div nz-flex nzGap="small">
                <button nz-button nzType="primary" nzSize="small" (click)="copyCC(i)">复制</button>
                <button nz-button nzType="primary" nzSize="small" [disabled]="!copiedCC"
                        (click)="pasteCC(index)">粘贴</button>
                <button nz-button nzType="primary" nzSize="small" (click)="insertCC(index)">插入</button>
                <button nz-button nzType="primary" nzSize="small" nzDanger
                        (click)="deleteCC(index)">删除</button>
            </div>
        </td>
    </tr>
</tbody>

而对于表格中的数据操作也是非常简单,在进行数据操作后直接对数组源数据进行一次赋值就可以了。

// 复制行
copyCC(row: any): void {
    this.copiedCC = { ...row }; // 浅拷贝存储数据
}

// 粘贴行
pasteCC(index: number): void {
    if (this.copiedCC) {
        const updatedData = [...this.materialsData.cc_text];
        updatedData.splice(index + 1, 0, { ...this.copiedCC });
        this.materialsData.cc_text = updatedData;
    }
}

// 插入空行
insertCC(index: number): void {
    const updatedData = [...this.materialsData.cc_text];
    updatedData.splice(index + 1, 0, this.createEmptyCC());
    this.materialsData.cc_text = updatedData;
}

// 删除行
deleteCC(index: number): void {
    const updatedData = [...this.materialsData.cc_text];
    updatedData.splice(index, 1);
    this.materialsData.cc_text = updatedData;
}

// 添加新行到末尾
addCC(): void {
    const emptyCC = this.createEmptyCC();
	this.materialsData.cc_text = [...this.materialsData.cc_text, emptyCC];
}

// 创建空行
private createEmptyCC(): any {
    return this.ids.reduce((row, id) => ({ ...row, [id]: '' }), {});
}

到这里就实现了相同的功能,虽然代码可读性似乎也没有那么高吧,但是总归没有那么臃肿了。

最后

ZC设计模块最初一共有多个页面,代码总共10400行左右。

在我重写之后,代码行数降到了4300行左右。

我知道代码的多少并不能决定代码的质量和性能,但就我上述的前后对比而言,你可以知道我的这4300行代码提升到底有多大……

现在Z同事已经完全脱离我的项目了,虽然我对他不爽但也没什么交集的地方,眼不见心不烦

写这篇博客仅仅是想记录曾经重写的我那最黑暗的三天……如果下一次Z同事还要回到我的项目,我一定会严重抗议!