一、背景与应用场景

在现代Web开发中,处理大数据表格是一个常见的需求。比如说,一个电商平台需要展示上万条商品信息,包括商品名称、价格、销量等;或者一个金融系统要呈现大量的交易记录,涵盖交易时间、金额、交易对象等。这些场景下,如果使用传统的表格渲染方式,页面往往会出现卡顿现象,用户体验极差。这时候,Angular Material的自定义CDK Table与虚拟滚动相结合的技术就派上用场了。

Angular Material是一套基于Angular框架的UI组件库,而CDK(Component Dev Kit)是Angular Material的基础工具集,它提供了构建自定义组件的能力。虚拟滚动则是一种优化渲染性能的技术,只渲染用户可见区域内的数据,从而减少了DOM元素的数量,提高了页面的响应速度。

二、传统表格渲染的问题

2.1 大量DOM元素的性能瓶颈

传统的表格渲染方式是将所有数据一次性渲染到页面上。假如我们有10000条数据,每条数据有10个字段,那么在页面上就会生成大量的<tr>和<td>元素。浏览器需要花费大量的时间来创建和渲染这些DOM元素,导致页面加载缓慢,甚至出现卡顿。

2.2 内存占用问题

大量的DOM元素不仅会增加渲染时间,还会占用大量的内存。当用户在页面上进行滚动、排序等操作时,浏览器需要频繁地重排和重绘,进一步加剧了性能问题。

三、自定义CDK Table

3.1 基础使用

首先,我们需要安装Angular Material和CDK:

ng add @angular/material

然后在项目中引入相关模块。假设我们要创建一个简单的商品表格,每个商品有id、name、price三个字段。

import { Component } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable, of } from 'rxjs';

// 定义商品接口
interface Product {
  id: number;
  name: string;
  price: number;
}

// 自定义数据源
class ProductDataSource extends DataSource<Product> {
  // 模拟商品数据
  private products: Product[] = [
    { id: 1, name: 'Product 1', price: 100 },
    { id: 2, name: 'Product 2', price: 200 },
    // 可以添加更多商品数据
  ];

  // 连接数据源
  connect(): Observable<Product[]> {
    return of(this.products);
  }

  // 断开数据源
  disconnect() { }
}

@Component({
  selector: 'app-product-table',
  template: `
    <table mat-table [dataSource]="dataSource">
      <!-- ID列 -->
      <ng-container matColumnDef="id">
        <th mat-header-cell *matHeaderCellDef> ID </th>
        <td mat-cell *matCellDef="let product"> {{product.id}} </td>
      </ng-container>
      <!-- 名称列 -->
      <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef> Name </th>
        <td mat-cell *matCellDef="let product"> {{product.name}} </td>
      </ng-container>
      <!-- 价格列 -->
      <ng-container matColumnDef="price">
        <th mat-header-cell *matHeaderCellDef> Price </th>
        <td mat-cell *matCellDef="let product"> ${{product.price}} </td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
  `
})
export class ProductTableComponent {
  // 显示的列名
  displayedColumns: string[] = ['id', 'name', 'price'];
  // 数据源
  dataSource = new ProductDataSource();
}

在上述代码中,我们定义了一个Product接口来表示商品数据,创建了一个ProductDataSource类来作为数据源。在组件的模板中,使用mat-table指令来创建表格,通过matColumnDef定义每一列的内容。

3.2 自定义样式和功能

我们可以根据需求对表格进行自定义样式和功能的添加。比如,为表格添加排序功能:

import { Component, ViewChild } from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { DataSource } from '@angular/cdk/collections';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

// 定义商品接口
interface Product {
  id: number;
  name: string;
  price: number;
}

// 自定义数据源
class ProductDataSource extends DataSource<Product> {
  // 模拟商品数据
  private products: Product[] = [
    { id: 1, name: 'Product 1', price: 100 },
    { id: 2, name: 'Product 2', price: 200 },
    // 可以添加更多商品数据
  ];

  constructor(private sort: MatSort) {
    super();
  }

  // 连接数据源
  connect(): Observable<Product[]> {
    const dataMutations = [
      of(this.products),
      this.sort.sortChange
    ];

    return new Observable(observer => {
      observer.next(this.products);
    }).pipe(map(() => {
      return this.getSortedData([...this.products]);
    }));
  }

  // 断开数据源
  disconnect() { }

  // 排序数据
  private getSortedData(data: Product[]): Product[] {
    if (!this.sort.active || this.sort.direction === '') {
      return data;
    }

    return data.sort((a, b) => {
      const isAsc = this.sort.direction === 'asc';
      switch (this.sort.active) {
        case 'id': return compare(a.id, b.id, isAsc);
        case 'name': return compare(a.name, b.name, isAsc);
        case 'price': return compare(a.price, b.price, isAsc);
        default: return 0;
      }
    });
  }
}

function compare(a: number | string, b: number | string, isAsc: boolean) {
  return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}

@Component({
  selector: 'app-product-table',
  template: `
    <table mat-table [dataSource]="dataSource" matSort>
      <!-- ID列 -->
      <ng-container matColumnDef="id">
        <th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
        <td mat-cell *matCellDef="let product"> {{product.id}} </td>
      </ng-container>
      <!-- 名称列 -->
      <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
        <td mat-cell *matCellDef="let product"> {{product.name}} </td>
      </ng-container>
      <!-- 价格列 -->
      <ng-container matColumnDef="price">
        <th mat-header-cell *matHeaderCellDef mat-sort-header> Price </th>
        <td mat-cell *matCellDef="let product"> ${{product.price}} </td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
  `
})
export class ProductTableComponent {
  // 显示的列名
  displayedColumns: string[] = ['id', 'name', 'price'];
  @ViewChild(MatSort, { static: true }) sort: MatSort;
  // 数据源
  dataSource: ProductDataSource;

  ngOnInit() {
    this.dataSource = new ProductDataSource(this.sort);
  }
}

在这个例子中,我们引入了MatSort模块,为表格的表头添加了排序功能。当用户点击表头时,表格会根据相应的列进行排序。

四、虚拟滚动技术

4.1 原理

虚拟滚动的核心原理是只渲染用户可见区域内的数据。当用户滚动页面时,动态地加载和渲染新的数据,同时销毁不可见区域的数据。这样可以大大减少DOM元素的数量,提高页面的性能。

4.2 在Angular中使用虚拟滚动

Angular Material提供了cdk-virtual-scroll-viewport指令来实现虚拟滚动。我们可以将其应用到之前的商品表格中:

import { Component } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable, of } from 'rxjs';

// 定义商品接口
interface Product {
  id: number;
  name: string;
  price: number;
}

// 自定义数据源
class ProductDataSource extends DataSource<Product> {
  // 模拟大量商品数据
  private products: Product[] = [];
  constructor() {
    for (let i = 0; i < 10000; i++) {
      this.products.push({ id: i, name: `Product ${i}`, price: i * 10 });
    }
  }

  // 连接数据源
  connect(): Observable<Product[]> {
    return of(this.products);
  }

  // 断开数据源
  disconnect() { }
}

@Component({
  selector: 'app-product-table',
  template: `
    <cdk-virtual-scroll-viewport itemSize="50">
      <table mat-table [dataSource]="dataSource">
        <!-- ID列 -->
        <ng-container matColumnDef="id">
          <th mat-header-cell *matHeaderCellDef> ID </th>
          <td mat-cell *matCellDef="let product"> {{product.id}} </td>
        </ng-container>
        <!-- 名称列 -->
        <ng-container matColumnDef="name">
          <th mat-header-cell *matHeaderCellDef> Name </th>
          <td mat-cell *matCellDef="let product"> {{product.name}} </td>
        </ng-container>
        <!-- 价格列 -->
        <ng-container matColumnDef="price">
          <th mat-header-cell *matHeaderCellDef> Price </th>
          <td mat-cell *matCellDef="let product"> ${{product.price}} </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *cdkVirtualFor="let row of dataSource.connect(); columns: displayedColumns;"></tr>
      </table>
    </cdk-virtual-scroll-viewport>
  `
})
export class ProductTableComponent {
  // 显示的列名
  displayedColumns: string[] = ['id', 'name', 'price'];
  // 数据源
  dataSource = new ProductDataSource();
}

在这个例子中,我们使用cdk-virtual-scroll-viewport指令包裹表格,设置itemSize为每行的高度。使用*cdkVirtualFor指令来循环渲染数据,这样只有可见区域内的数据会被渲染。

五、自定义CDK Table与虚拟滚动结合

5.1 实现步骤

将自定义CDK Table与虚拟滚动结合,我们可以进一步优化大数据表格的性能。以下是具体的实现步骤:

  1. 确保项目中已经安装了Angular Material和CDK。
  2. 定义数据接口和数据源。
  3. 创建自定义CDK Table,定义表格的列和内容。
  4. 使用cdk-virtual-scroll-viewport指令实现虚拟滚动。

5.2 完整示例

import { Component } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable, of } from 'rxjs';

// 定义商品接口
interface Product {
  id: number;
  name: string;
  price: number;
}

// 自定义数据源
class ProductDataSource extends DataSource<Product> {
  // 模拟大量商品数据
  private products: Product[] = [];
  constructor() {
    for (let i = 0; i < 10000; i++) {
      this.products.push({ id: i, name: `Product ${i}`, price: i * 10 });
    }
  }

  // 连接数据源
  connect(): Observable<Product[]> {
    return of(this.products);
  }

  // 断开数据源
  disconnect() { }
}

@Component({
  selector: 'app-product-table',
  template: `
    <cdk-virtual-scroll-viewport itemSize="50">
      <table mat-table [dataSource]="dataSource">
        <!-- ID列 -->
        <ng-container matColumnDef="id">
          <th mat-header-cell *matHeaderCellDef> ID </th>
          <td mat-cell *matCellDef="let product"> {{product.id}} </td>
        </ng-container>
        <!-- 名称列 -->
        <ng-container matColumnDef="name">
          <th mat-header-cell *matHeaderCellDef> Name </th>
          <td mat-cell *matCellDef="let product"> {{product.name}} </td>
        </ng-container>
        <!-- 价格列 -->
        <ng-container matColumnDef="price">
          <th mat-header-cell *matHeaderCellDef> Price </th>
          <td mat-cell *matCellDef="let product"> ${{product.price}} </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *cdkVirtualFor="let row of dataSource.connect(); columns: displayedColumns;"></tr>
      </table>
    </cdk-virtual-scroll-viewport>
  `
})
export class ProductTableComponent {
  // 显示的列名
  displayedColumns: string[] = ['id', 'name', 'price'];
  // 数据源
  dataSource = new ProductDataSource();
}

通过这种方式,我们可以在处理大量数据时,避免页面卡顿,提高用户体验。

六、技术优缺点

6.1 优点

  • 性能提升:虚拟滚动只渲染可见区域的数据,大大减少了DOM元素的数量,降低了浏览器的渲染压力,提高了页面的响应速度。
  • 可扩展性强:自定义CDK Table可以根据需求进行灵活的定制,添加排序、分页、过滤等功能。
  • 与Angular集成良好:Angular Material和CDK是基于Angular框架开发的,与Angular项目的集成非常方便,开发效率高。

6.2 缺点

  • 学习成本较高:Angular Material和CDK有一定的学习曲线,对于初学者来说,掌握这些技术需要花费一些时间。
  • 初始配置复杂:在使用自定义CDK Table和虚拟滚动时,需要进行一些初始配置,如定义数据源、设置表格列等,对于小型项目来说,可能会显得过于繁琐。

七、注意事项

7.1 数据加载和更新

在使用虚拟滚动时,要注意数据的加载和更新。当用户滚动页面时,需要动态地加载新的数据。同时,当数据发生变化时,要及时更新数据源,确保表格显示的是最新的数据。

7.2 样式调整

由于虚拟滚动只渲染可见区域的数据,可能会导致一些样式问题。例如,表头和表格内容的对齐问题。需要对样式进行适当的调整,确保表格的显示效果良好。

7.3 兼容性问题

在不同的浏览器和设备上,虚拟滚动的表现可能会有所不同。在开发过程中,需要进行充分的测试,确保在各种环境下都能正常工作。

八、文章总结

在处理大数据表格时,传统的表格渲染方式会导致页面卡顿,用户体验不佳。通过自定义CDK Table与虚拟滚动结合的技术,可以有效地突破渲染性能瓶颈。自定义CDK Table提供了灵活的定制能力,可以根据需求添加各种功能;虚拟滚动则只渲染可见区域的数据,减少了DOM元素的数量,提高了页面的性能。在实际应用中,我们要注意数据加载和更新、样式调整以及兼容性等问题。掌握这种技术,可以让我们在处理大数据表格时更加得心应手,为用户提供更好的体验。