一、背景与应用场景
在现代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与虚拟滚动结合,我们可以进一步优化大数据表格的性能。以下是具体的实现步骤:
- 确保项目中已经安装了Angular Material和CDK。
- 定义数据接口和数据源。
- 创建自定义CDK Table,定义表格的列和内容。
- 使用
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元素的数量,提高了页面的性能。在实际应用中,我们要注意数据加载和更新、样式调整以及兼容性等问题。掌握这种技术,可以让我们在处理大数据表格时更加得心应手,为用户提供更好的体验。
评论
围绕“Angular Material大数据表格卡顿:自定义CDK Table与虚拟滚动结合突破渲染性能瓶颈”参与讨论