AG-Grid实战:从基础配置到高级功能,打造高性能企业级表格

1. 从“能用”到“好用”:AG-Grid配置的核心价值

如果你用过AG-Grid,大概率会经历这样一个过程:一开始,你只是把它当成一个能展示数据的普通表格,把数据扔进去,列定义好,页面能渲染出来就算成功。但随着业务深入,产品经理开始提需求了——“这个列能不能固定宽度?”“用户需要批量选择行来操作。”“表格最后要加个合计行,而且要能跟着数据变。”这时候,你发现AG-Grid的配置项多到让人眼花缭乱,官方文档虽然详尽,但更像一本字典,缺乏一个从“能用”到“好用”的实战路径。

我最初接触AG-Grid时,也踩过不少坑。比如,我以为设置了checkboxSelection: true就能实现行选择,结果发现复选框根本没显示;又比如,想给表格加个底部合计行,照着某个过时的教程折腾了半天,结果样式错乱,性能还奇差。这些经历让我意识到,AG-Grid的强大之处不在于它提供了多少功能,而在于如何将这些功能通过合理的配置组合起来,形成一个既满足业务需求,又具备良好用户体验和性能的表格组件。

今天这篇笔记,我们就聚焦在那些让表格从“能看”到“好用”的核心配置上。我会结合我自己的项目实践,详细拆解如何定义列、实现行选择(尤其是复选框模式)、灵活控制行高列宽、添加置顶或底部的合计行、实现行分组以及高效的客户端排序。这些功能几乎覆盖了中后台管理系统表格需求的80%,掌握它们,你就能应对绝大多数场景。

2. 列定义的艺术:不仅仅是字段映射

很多人觉得列定义(columnDefs)就是把数据对象的字段名(field)映射到表头(headerName)而已。这当然没错,但这只是最基础的一层。AG-Grid的列定义是一个高度灵活的配置对象,它决定了每一列如何展示、如何交互以及如何计算。

2.1 基础定义与类型推断

最基本的列定义如下所示。AG-Grid会根据你提供的field,自动去数据对象(rowData)中寻找同名属性来填充单元格。

const columnDefs = [ { headerName: '员工ID', field: 'id' }, { headerName: '姓名', field: 'name' }, { headerName: '部门', field: 'department' }, { headerName: '薪资', field: 'salary' }, { headerName: '入职日期', field: 'joinDate' } ];

这里有一个容易被忽略但很重要的点:类型推断。AG-Grid会尝试根据你首次传入的数据来推断列的数据类型,这会影响默认的排序、过滤等行为。例如,对于salary字段,如果你传入的数据是数字[5000, 8000],AG-Grid会将其识别为数字列,排序时会按数值大小进行。但如果你的数据源里混入了字符串(比如有些数据是‘5,000’),排序结果就会出乎意料。因此,我强烈建议对于明确类型的列,使用type属性进行声明:

{ headerName: '薪资', field: 'salary', type: 'numericColumn' }, { headerName: '入职日期', field: 'joinDate', type: ['dateColumn', 'nonEditableColumn'] }

type可以是一个字符串,也可以是一个数组,用于应用一组预定义或自定义的列类型。AG-Grid内置了‘textColumn’‘numericColumn’‘dateColumn’‘nonEditableColumn’等类型,它们预置了排序器、过滤器、单元格编辑器等配置。

2.2 自定义渲染器与格式化器

当默认的文本展示无法满足需求时,就需要用到cellRenderer(单元格渲染器)和valueFormatter(值格式化器)。这是列定义中最能体现灵活性的部分。

  • valueFormatter:用于在数据展示前进行格式化,不改变原始数据。它接收原始值,返回一个用于显示的字符串。这非常适合处理数字、日期等格式。

    { headerName: '薪资', field: 'salary', type: 'numericColumn', // 将数字格式化为货币形式,例如 5000 -> ¥5,000.00 valueFormatter: params => { if (params.value == null) return ''; return '¥' + params.value.toLocaleString('zh-CN', { minimumFractionDigits: 2 }); } }, { headerName: '入职日期', field: 'joinDate', type: 'dateColumn', // 将日期对象格式化为 YYYY-MM-DD 字符串 valueFormatter: params => { if (!params.value) return ''; const date = new Date(params.value); return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; } }
  • cellRenderer:用于完全自定义单元格的渲染内容,可以返回任何有效的React/Vue/Angular组件或HTML字符串。当你需要在单元格内放置按钮、进度条、图标等内容时,就必须用它。

    { headerName: '状态', field: 'status', // 根据状态值渲染不同的标签和颜色 cellRenderer: params => { const status = params.value; let color, text; switch(status) { case 'active': color = 'green'; text = '在职'; break; case 'inactive': color = 'gray'; text = '离职'; break; case 'pending': color = 'orange'; text = '待入职'; break; default: color = 'black'; text = '未知'; } return `<span style="color:${color}; font-weight:bold;">${text}</span>`; } }, { headerName: '操作', // 操作列通常没有对应的数据字段 cellRenderer: params => { // 假设我们使用框架(如React),这里可以返回一个组件 // 这里用简化的HTML字符串示例 return `<button onclick="handleEdit('${params.data.id}')">编辑</button> <button onclick="handleDelete('${params.data.id}')">删除</button>`; } }

注意:在cellRenderer中使用内联事件(如onclick)在复杂的单页应用中不是最佳实践,事件可能无法绑定。更推荐的方式是使用AG-Grid的框架特定组件(如AgGridReactcellRendererFramework)或在网格初始化后通过AG-Grid的API来管理事件。

2.3 列宽与最小/最大宽度

列宽的控制我们放在后面的章节详细讲,但在列定义里,有几个相关属性需要提前了解:

  • width: 设置列的初始像素宽度。
  • minWidth: 列的最小像素宽度,用户无法拖拽得比这个更窄。
  • maxWidth: 列的最大像素宽度。
  • suppressSizeToFit: 布尔值,设为true可以阻止该列在调用api.sizeColumnsToFit()时自动调整大小。

一个完整的、考虑了类型、格式化和基础样式的列定义示例可能长这样:

const columnDefs = [ { headerName: '员工ID', field: 'id', width: 100, minWidth: 80, type: 'numericColumn' }, { headerName: '姓名', field: 'name', width: 120, tooltipField: 'name' // 当文本过长时,悬停显示完整内容 }, { headerName: '薪资', field: 'salary', type: 'numericColumn', width: 130, valueFormatter: params => params.value ? '¥' + params.value.toLocaleString('zh-CN') : '', comparator: (valueA, valueB) => valueA - valueB, // 自定义排序逻辑,虽然numericColumn默认就是这个 filter: 'agNumberColumnFilter' }, // ... 其他列 ];

3. 行选择:单行、多行与复选框模式

行选择是交互式表格的基石。AG-Grid提供了灵活的选择配置,支持单选、多选,并且与复选框深度集成。

3.1 启用行选择

首先,需要在网格的全局选项(gridOptions)中启用行选择:

const gridOptions = { // 启用行选择 rowSelection: 'multiple', // 可选 'single', 'multiple' // 其他配置... };
  • rowSelection: 'single':只能选择一行,新选择会替换旧选择。
  • rowSelection: 'multiple':可以选择多行,配合Ctrl(或Cmd)键进行多选,配合Shift键进行范围选择。这是最常用的模式。

仅仅这样,用户可以通过点击行来选中,但视觉反馈不强,且无法方便地进行全选或反选。因此,我们通常需要引入复选框。

3.2 集成复选框选择

在AG-Grid中,复选框是作为一列来处理的。你需要在columnDefs中添加一个特殊的列定义。

const columnDefs = [ // 复选框列,通常放在第一列 { headerName: '', // 表头可以为空 field: 'ag-Grid-AutoColumn', // 这是一个特殊字段,非必需 checkboxSelection: true, // 关键属性,启用复选框 headerCheckboxSelection: true, // 在表头也显示一个复选框,用于全选/全不选当前页 headerCheckboxSelectionFilteredOnly: true, // 表头复选框是否只选中过滤后的行(强烈建议设为true) width: 50, pinned: 'left', // 固定在左侧,不随横向滚动消失 suppressMenu: true, // 不显示该列的菜单 filter: false, // 不显示该列的过滤器 sortable: false // 该列不可排序 }, // ... 其他业务列 ];

关键属性解析:

  • checkboxSelection: true:这行代码告诉AG-Grid在该列每个单元格渲染一个复选框。
  • headerCheckboxSelection: true:在列头部也渲染一个复选框。点击它可以切换当前页面所有行的选中状态。
  • headerCheckboxSelectionFilteredOnly: true:这是极其重要的一个配置。如果设为false(默认),点击表头复选框会选中数据源中的所有行(包括不在当前过滤或分页视图中的行)。这通常不是用户想要的行为,而且当数据量大时会导致性能问题。设为true后,表头复选框只影响当前过滤后可见的行,逻辑上更合理。

3.3 获取与处理选中行数据

配置好之后,你需要通过AG-Grid的API来获取用户选中的行。

// 假设你已经获取了gridApi的引用 const selectedNodes = gridApi.getSelectedNodes(); const selectedData = selectedNodes.map(node => node.data); // 或者直接获取选中的数据 const selectedData = gridApi.getSelectedRows(); console.log('选中的数据:', selectedData); // 接下来可以将 selectedData 用于批量删除、导出等操作

实操心得:

  1. 性能考虑:在rowSelection: 'multiple'模式下,如果数据量极大(数万行),频繁调用getSelectedRows()或选中/取消选中大量行可能会影响性能。可以考虑使用selectionChanged事件进行节流处理,或者对于超大数据集,采用服务器端选择模式。
  2. 与分页/虚拟滚动的交互:AG-Grid的分页或虚拟滚动只会渲染可视区域的行。但选中状态是保存在数据节点(RowNode)上的,即使某行当前不可见,只要它被选中过,其选中状态依然保留。这符合预期。
  3. 默认选中:你可以通过设置行数据中某个节点的selected属性为true来默认选中某些行,但必须在数据提供给网格之前设置好。或者,在gridReady事件后,使用gridApi.getRowNode(id).setSelected(true)来动态选中。

4. 精细控制:行高与列宽

表格的视觉舒适度很大程度上取决于行高和列宽的设置。AG-Grid提供了多种策略。

4.1 设置行高

行高可以通过gridOptions进行全局设置,也可以通过getRowHeight回调进行动态控制。

  • 全局固定行高

    const gridOptions = { rowHeight: 42, // 所有行固定为42像素高 // 或者通过CSS变量设置,更灵活 // 需要在全局CSS中定义:.ag-theme-alpine { --ag-row-height: 42px; } };
  • 动态行高:当行内容高度不一致时(例如,某列文本换行,或使用了自定义渲染器),需要使用getRowHeight

    const gridOptions = { getRowHeight: params => { // 根据行数据动态计算高度 if (params.data.description && params.data.description.length > 100) { // 描述字段长的行,给更高高度 return 80; } // 返回 null 或 undefined 则使用默认行高 return 42; } };

    注意:动态行高会轻微影响滚动性能,因为AG-Grid需要为每一行计算高度。对于超大数据集需谨慎使用。

4.2 列宽策略:自动、固定与响应式

列宽管理是表格布局的核心。AG-Grid提供了几种模式:

  1. 固定宽度(Fixed Width):在columnDefs中直接为每一列设置width。这是最直接的方式,但无法适应容器宽度的变化。

    { headerName: 'ID', field: 'id', width: 80 }
  2. 自动调整列宽(sizeColumnsToFit:这是最常用的方法,让所有列自动填充网格的可用宽度。

    // 在 gridReady 事件中调用 onGridReady(params) { this.gridApi = params.api; this.gridColumnApi = params.columnApi; // 让所有列自适应宽度以填满容器 this.gridApi.sizeColumnsToFit(); }

    工作原理:此方法会计算网格容器的可用宽度,然后根据每列的minWidthmaxWidth和初始width(或默认宽度)按比例分配空间。如果所有列的最小宽度之和超过容器宽度,会出现水平滚动条。

  3. 自动列宽(autoSizeColumns:根据列的内容自动调整每列的宽度,使其刚好能容纳内容(考虑表头和数据)。

    // 调整所有列 gridApi.autoSizeColumns(allColumnIds); // 调整指定列 gridApi.autoSizeColumns(['id', 'name', 'department']);

    适用场景:在数据加载完成后,希望列宽完全由内容决定时调用。注意,对于数据量大的列,计算所有行的内容宽度可能消耗性能。

  4. 混合策略:在实际项目中,我通常采用混合策略。

    • 对需要固定宽度的列(如操作列、状态列)设置明确的widthsuppressSizeToFit: true
    • 对主要的内容列(如名称、描述)不设width或设一个较宽的初始width,并设置合理的minWidthmaxWidth
    • gridReady和窗口resize事件中调用sizeColumnsToFit(),让这些内容列自适应剩余空间。
    const columnDefs = [ { headerName: '操作', field: 'actions', width: 150, suppressSizeToFit: true, pinned: 'right' }, { headerName: '姓名', field: 'name', minWidth: 100, maxWidth: 200 }, { headerName: '描述', field: 'desc', minWidth: 150 } ]; onGridReady(params) { this.gridApi = params.api; this.gridApi.sizeColumnsToFit(); window.addEventListener('resize', () => { setTimeout(() => this.gridApi.sizeColumnsToFit()); }); }

5. 聚合数据:置顶合计行与底部合计行

在财务、报表类应用中,合计行是刚需。AG-Grid提供了两种主要方式来实现:“置顶合计行”和“底部合计行”,它们在实现原理和适用场景上有所不同。

5.1 底部合计行(Pinned Bottom Row)

这是最直观的合计行实现方式。它本质上是一行被“钉”在表格底部的内容,不参与排序和过滤,始终可见。

配置方法:

const gridOptions = { // ... 其他配置 pinnedBottomRowData: [{}], // 放置一个空对象占位,具体值通过 processRowData 或 getRowStyle 等计算 }; // 更常见的做法是,在数据加载后,动态计算并设置合计行数据 function updateBottomPinnedRow() { // 假设我们计算薪资总和与平均薪资 let totalSalary = 0; let rowCount = 0; gridApi.forEachNode(node => { if (node.group) return; // 跳过分组行 totalSalary += node.data.salary || 0; rowCount++; }); const avgSalary = rowCount > 0 ? totalSalary / rowCount : 0; // 构建合计行数据对象,字段名需要与 columnDefs 对应 const totalRow = { // 普通列可以显示合计标签或留空 name: '合计/平均', // 对需要求和的列,放入计算值 salary: `总额: ¥${totalSalary.toLocaleString()} | 平均: ¥${avgSalary.toFixed(2)}`, // 其他列... }; // 设置置底行数据 gridApi.setPinnedBottomRowData([totalRow]); } // 在数据加载后或数据变化时调用 updateBottomPinnedRow();

样式自定义:通常需要为合计行添加特殊样式,如加粗、背景色。可以通过gridOptionsgetRowStyle回调实现。

const gridOptions = { getRowStyle: params => { if (params.node.rowPinned) { // 被钉住的行(顶部或底部) return { fontWeight: 'bold', backgroundColor: '#f5f5f5' }; } return null; } };

优点:实现简单,直观,始终可见。缺点它不属于主数据行,因此无法直接使用AG-Grid内置的聚合函数(aggFunc),所有计算逻辑需要手动实现。当数据过滤或排序时,需要重新计算并更新这行数据(监听filterChangedsortChanged事件)。

5.2 置顶合计行与行分组中的聚合

“置顶合计行”在配置上与底部类似,使用pinnedTopRowData。但更强大、更符合AG-Grid哲学的方式是使用行分组(Row Grouping)聚合函数(Aggregation Functions)

AG-Grid的聚合功能设计用于在分组时,对子行的数据进行汇总(如求和、求平均、计数等)。我们可以利用这个特性,创建一个虚拟的“根分组”来实现全局合计。

步骤1:启用分组和聚合columnDefs中,为需要聚合的列指定aggFunc

const columnDefs = [ { headerName: '部门', field: 'department', rowGroup: true, hide: true }, // 按部门分组,并隐藏该列(因为会显示在分组面板) { headerName: '姓名', field: 'name' }, { headerName: '薪资', field: 'salary', type: 'numericColumn', // 定义聚合函数 aggFunc: 'sum', // 可选:为聚合结果指定值格式化器 valueFormatter: params => params.value ? '¥' + params.value.toLocaleString('zh-CN') : '' }, { headerName: '平均薪资', field: 'salary', // 同一字段可以使用不同的聚合函数 aggFunc: 'avg', valueFormatter: params => params.value ? '¥' + params.value.toFixed(2) : '' } ];

步骤2:配置分组显示选项gridOptions中配置如何显示分组。

const gridOptions = { // 启用分组 groupDisplayType: 'groupRows', // 或 'custom' // 自动展开所有分组到指定级别,-1为全部展开 groupDefaultExpanded: -1, // 在分组行上显示聚合结果 groupIncludeFooter: true, // 在分组底部显示一个聚合行(小计) groupIncludeTotalFooter: true, // 为顶级分组(所有数据)显示一个总计行 // 或者,使用 groupTotalRow 进行更精细控制 // groupTotalRow: 'bottom' // 在底部显示总计行 };

步骤3:查看效果启用上述配置后,AG-Grid会:

  1. 根据rowGroup: true的列(如department)对数据进行分组。
  2. 在每个分组行的底部(如果groupIncludeFooter: true)显示该分组内数据的聚合结果(小计)。
  3. 在表格的最底部(如果groupIncludeTotalFooter: true)显示所有数据的聚合结果(总计)。这个总计行,功能上就相当于一个强大的、自动计算的“底部合计行”。

对比与选择:

  • 手动pinnedBottomRowData:适合简单的、自定义的合计显示,或者合计逻辑非常复杂,无法用内置aggFunc表达的情况。需要手动维护计算和更新。
  • 分组聚合总计行:利用AG-Grid内置能力,自动处理过滤、排序后的数据聚合。性能更好,代码更简洁,且与分组功能天然结合。是实现标准合计功能的首选

6. 组织数据:行分组功能详解

行分组(Row Grouping)是将具有相同值的行折叠在一起显示的功能,非常适合用于分层查看数据,比如按部门、地区、日期等维度查看。

6.1 基础分组配置

如上节所述,只需在columnDefs中将某列的rowGroup属性设为true即可启用分组。通常我们会将分组列隐藏(hide: true),因为它会以分组标题的形式展示在界面上,而不是作为一个普通列。

const columnDefs = [ { headerName: '大区', field: 'region', rowGroup: true, hide: true }, { headerName: '部门', field: 'department', rowGroup: true, hide: true }, { headerName: '姓名', field: 'name' }, { headerName: '薪资', field: 'salary', aggFunc: 'sum' } ]; const gridOptions = { groupDisplayType: 'groupRows', // 经典的分组行样式 groupDefaultExpanded: 1, // 默认展开第一级分组(大区) // 可选:在分组行内部显示聚合值 groupRowRendererParams: { // 例如,在分组标题后显示该组的薪资总和 innerRenderer: params => { const sum = params.node.aggData.salary; return ` (总薪资: ¥${sum.toLocaleString()})`; } } };

6.2 分组控制面板

AG-Grid默认提供一个分组拖拽面板,用户可以将列拖入该区域来动态创建分组。你可以通过rowGroupPanelShow属性控制其显示。

const gridOptions = { // 始终显示分组面板 rowGroupPanelShow: 'always', // 或者只在有分组列时显示 // rowGroupPanelShow: 'onlyWhenGrouping', // 或者不显示 // rowGroupPanelShow: 'never', };

6.3 维护分组状态与性能

当数据量很大时,频繁展开/折叠分组或修改分组列可能会引发性能问题。有几点需要注意:

  1. groupDefaultExpanded:不要轻易设置为-1(全部展开),尤其是数据量大的时候。让用户按需展开是更好的选择。
  2. 分组列排序:分组后的数据,其排序是在组内进行的。如果你希望对整个数据集排序后再分组,需要先对rowData进行排序,或者使用AG-Grid的服务端排序模式。
  3. 动态更新数据:如果数据是动态更新的(例如WebSocket推送),分组视图会自动更新。但如果你大量增删数据,调用api.refreshCells()api.setRowData()可能会导致分组状态(展开/折叠)丢失。更推荐使用事务更新(api.applyTransaction())来局部更新数据,以保持UI状态。

7. 客户端排序:快速、灵活的数据组织

AG-Grid的客户端排序非常高效,对于万级以下的数据,它能提供即时响应。配置和使用都很简单。

7.1 启用排序

在列定义或全局配置中启用排序:

// 方法1:全局启用所有列排序 const gridOptions = { defaultColDef: { sortable: true // 所有列默认可排序 } }; // 方法2:为特定列启用排序 const columnDefs = [ { headerName: '姓名', field: 'name', sortable: true }, { headerName: '薪资', field: 'salary', sortable: true }, { headerName: '入职日期', field: 'joinDate', sortable: true, comparator: dateComparator } ];

7.2 自定义排序逻辑

默认的排序规则对字符串、数字、日期通常够用。但遇到复杂情况(如自定义格式的字符串、包含单位的数值等),就需要comparator函数。

// 自定义日期排序比较器(假设 joinDate 是 'YYYY-MM-DD' 字符串) function dateComparator(date1, date2) { const d1 = date1 ? new Date(date1).getTime() : 0; const d2 = date2 ? new Date(date2).getTime() : 0; return d1 - d2; } // 自定义字符串排序(例如,处理带有前缀的编号 'EMP-001') function customIdComparator(id1, id2) { const num1 = parseInt(id1.replace('EMP-', ''), 10) || 0; const num2 = parseInt(id2.replace('EMP-', ''), 10) || 0; return num1 - num2; } const columnDefs = [ { headerName: '员工编号', field: 'empId', sortable: true, comparator: customIdComparator }, { headerName: '入职日期', field: 'joinDate', sortable: true, comparator: dateComparator } ];

7.3 初始排序与多列排序

你可以在网格初始化时指定默认的排序状态。

const gridOptions = { // 初始按薪资降序,再按姓名升序排列 sortModel: [ { colId: 'salary', sort: 'desc' }, { colId: 'name', sort: 'asc' } ] };

用户可以通过Shift + Click列头来实现多列排序。AG-Grid会按照点击顺序应用排序优先级。

7.4 排序事件与状态获取

你可以监听排序变化,并获取当前的排序状态。

// 监听排序变化事件 gridOptions.onSortChanged = function(event) { const sortModel = event.api.getSortModel(); console.log('当前排序状态:', sortModel); // 可以将 sortModel 保存起来,用于恢复状态或发送到服务器 }; // 通过API获取当前排序模型 const currentSort = gridApi.getSortModel();

客户端排序的局限性:客户端排序一次性加载所有数据到内存中进行。对于超过5万行甚至更多的数据,排序操作可能会造成界面短暂的卡顿。在这种情况下,就需要考虑服务端排序,将排序参数(排序列、排序方向)发送到服务器,由服务器返回排序后的分页数据。AG-Grid通过serverSideDatasource完美支持这种模式,但配置相对复杂,需要前后端协同。