简介:ag-Grid Community 是一套面向 React、AngularJS、Web Components 等主流前端栈的数据网格开源库,适用于后台管理、数据分析、实时监控等需要高效展示和操作大量表格数据的开发场景。它内置高性能虚拟渲染、排序、过滤、分组、分页、列宽调整与冻结、单元格编辑及自定义验证等丰富能力,同时支持自定义单元格渲染器、多主题和运行时 API,让开发者可以按业务需求自由扩展。资源包共 2000 个文件,约 190MB,其中以 js 与 ts 源码为核心,配合 html、css、php、json 等辅助文件,以及 png/svg 图标、mp4 演示和 md 说明文档,形成了从源码到应用的完整资源链。已有 1458 人学习下载,社区反馈活跃。通过整理好的示例工程和配置代码,读者能够快速完成安装初始化,掌握列定义、数据绑定、事件响应与 API 调用,并对照源码理解虚拟滚动、冻结列、分组等高级特性的实现思路,轻松集成到现有项目或进行二次开发。
1. 为什么生产环境里的列表页,我兜兜转转还是回到 ag-grid-community
我接过一个模拟项目X,列表页数据才三千行,原来的表格组件一拖滚动条就掉帧,排序还经常卡到白屏。换了几套方案都顾头不顾尾,最后老老实实换成 ag-grid-community。它解决的问题很直接:用虚拟滚动把渲染范围压到肉眼可见的几十行,排序、过滤、分页这些中后台刚需改成配置就能用,省得自己造轮子。它适合数据密集型管理后台、报表查询页面,也适合列需求经常变的产品团队。如果你是前端新手,下面这套步骤能让你半小时跑通第一版;如果你已经在用其它表格库,也不要急着跳过第 2 章,社区版和商业版的边界比你想的更让人容易返工。
2. 先看清楚社区版和商业版分界线:免费的这些够你用,别等上线前返工
ag-grid-community 是 ag-grid 向社区开放的那部分代码,遵循宽松的开源协议,可以免费商用。需要留意的是,官方同时有带 license 的 enterprise 版本。社区版并不是阉割到只能看,它已经把前端表格里八成刚需覆盖了;但少的那两成往往是你最想做的那种炫酷交互。所以我先帮你把边界画出来,免得需求聊到一半才发现功能要额外授权。
2.1 社区版自带的能力清单:排序、过滤、分页、虚拟滚动、单元格交互
我一般把社区版能力归纳成四块:数据展示、数据操作、数据更新、界面适配。对于大多数中后台页面,这四块够用到项目交付,不需要碰 enterprise 配置。
| 能力类型 | 具体功能 | 使用备注 |
|---|---|---|
| 数据展示 | 虚拟滚动、固定列、自动列宽、自定义单元格渲染 | 默认开启虚拟滚动,数据多时不至于整表渲染 |
| 数据操作 | 单列/多列排序、文本/数字/日期/集合过滤、客户端分页 | 多列排序需要按住 Shift 点击列头 |
| 单元格编辑 | 双击编辑、编辑器校验、提交前回调 | 需要列定义里声明 editable: true |
| 行选择 | Checkbox 复选、单选,获取选中行数据 | 配合 rowSelection 使用 |
| 导出 | CSV 导出 | 纯前端完成,不含样式和公式 |
排序、过滤、分页是 ag-grid-community 最常用的三件套。sortable 和 filter 都是列级配置,可以在每列单独开,也可以用 defaultColDef 一次铺满全表。客户端分页适合几千到两三万行的场景,前端只做切页,体验很顺。如果数据量再往上走,就需要考虑是不是让后端分页了,这一点在避坑章节我会详细展开。
2.2 商业版独有的能力,以及为什么我劝你先留好换版的口子
真正需要 license 的典型功能包括:行分组与聚合、透视表、主从表、树形数据、服务端行模型、范围选择、Excel 格式导出。社区版没有这些,至少我长期使用的版本里没有。难点不在于少功能,而在于你一开始把这些交互做成了业务需求,后续改版成本就高了。
我的建议是,第一版代码里尽量让业务层不直接和 ag-grid 的 enterprise API 耦合。具体做法是封装一个自己的 Table 组件,只对外暴露 columns、rows、pageSize 这类业务字段,内部不管以后换 community 还是 enterprise,页面其他代码都不需要大改。这样真到了需要分组的某一天,你只需要在封装层打开分组配置,而不是满仓库找散落的 grid 调用。
2.3 拿什么标准来判断“社区版够不够”:一张过滤清单
接到需求时,先问自己三个问题:数据会不会超过一页展示且需要跨页统计?需不需要把多列拖拽成组?导出文件是否必须 xlsx?如果答案都是否,社区版基本能扛。超过十万行且要求毫秒级过滤时,社区版虚拟滚动能渲染,但网络层不可能把全量数据拉到前端,这种场景才需要服务端行模型,社区版确实覆盖不了。
我个人的判断底线是这样的:只要业务里出现“行内展开子表”“拖拽字段分组”“导出带格式 Excel”这几种词,就默认预算是企业版;否则先拿 community 做原型。原型阶段不要用 enterprise 的 API 写死交互,否则后面要么加预算,要么自己硬造一个不稳定的替代品,两者都难受。
提示:社区版支持 CSV 导出,但不支持带格式的 .xlsx 导出。如果你的“导出”只是让用户拿数据,CSV 够用;如果领导要做格式化月报,尽早把 enterprise 列进预算。
3. 用 npm 快速跑通 ag-grid-community 的最小表格:从安装到数据刷新
这一章直接进入可复现步骤。我用 React 做示例,因为中后台项目里 React 和 Vue 占大多数;如果你用的是原生 JS 或 Vue,核心配置概念完全一致,只是组件标签写法不同。
3.1 安装与引入:社区版核心包和样式的最小组合
一个 React 项目,我会装两个包。第一个是核心库 ag-grid-community,第二个是 React 封装层 ag-grid-react。Vue 项目则换成对应的 vue 封装包,底层仍然是 ag-grid-community。
npm install ag-grid-community ag-grid-react装完之后样式很容易被忽略。ag-grid 的样式分成两层:基础样式 ag-grid.css 负责网格本身的布局和虚拟滚动容器,主题样式 ag-theme-quartz.css 负责颜色、边框、字体。两个都要引,少一个都有问题。
import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css";我见过有人只引了主题,结果滚动容器宽度计算异常,列头位置错乱。还有一个容易踩的点:主题 class 不是写在表格组件上,而是写在包裹表格的父 div 上,这个 class 会告诉 ag-grid 使用哪个主题变量,不写就退化成无样式状态。
3.2 最小 demo:列定义、行数据、网格容器一个都不能少
下面是最小可用示例。它包含了三个必要部分:rowData 行数据、columnDefs 列定义、包裹 Grid 的 div 容器。
import { useMemo, useState } from "react"; import { AgGridReact } from "ag-grid-react"; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-quartz.css"; export default function BaseGrid() { const [rowData] = useState([ { id: 1, name: "订单A", status: "SUCCESS", amount: 1000 }, { id: 2, name: "订单B", status: "PENDING", amount: 2300 }, ]); const columnDefs = useMemo( () => [ { field: "id", headerName: "单号", width: 80 }, { field: "name", headerName: "订单名称" }, { field: "status", headerName: "状态" }, { field: "amount", headerName: "金额" }, ], [] ); return ( <div className="ag-theme-quartz" style={{ height: 400, width: "100%" }}> <AgGridReact rowData={rowData} columnDefs={columnDefs} /> </div> ); }说明一下这段配置的逻辑。columnDefs 里 field 对应 rowData 里的字段名,headerName 决定列头显示什么,不写 headerName 时默认直接显示 field。外层 div 必须有高度,ag-grid 默认会撑满父容器尺寸,父容器高度为 0 时表格渲染不出来,这是很多新手踩的第一个坑。useMemo 缓存列定义是为了避免每次 render 都生成新数组,后面会详细讲。
3.3 数据刷新:replaceAll、setRowData 与 IMMUTABLE 该选哪个
表格跑起来之后,下一个问题就是如何更新数据。常见做法有三种:整表替换用 setRowData,需要局部增删改用 applyTransaction,还有一种是开启 IMMUTABLE 更新策略。
const gridRef = useRef(null); // 整表替换:后端每次返回全量新数据 const loadFirstPage = async () => { const rows = await fetchOrders({ page: 1 }); if (gridRef.current?.api?.setGridOption) { gridRef.current.api.setGridOption("rowData", rows); } else { gridRef.current.api.setRowData(rows); } }; // 局部更新:只更新变化的那几行 const updateChangedRows = (changedRows) => { gridRef.current?.api.applyTransaction({ update: changedRows }); };第一段代码里同时写了 setGridOption 和 setRowData,是因为 ag-grid 在较新版本里统一推荐 setGridOption 这样的新入口,但老项目里 setRowData 还在大量使用。兼容写法不会报错,升级成本也低。replaceAll 这个概念实际上等同于把 rowData 重新赋一个新数组,所以不要指望它做细粒度更新。
真正推荐的局部更新姿势是 applyTransaction。它的入参是 add、remove、update 三个数组,分别表示新增、删除、更新的行。它比整表替换高效得多,因为 grid 不需要重建整个行模型,只处理差量。但它要求每行有稳定唯一的主键,并且通过 getRowId 告诉 grid。
<AgGridReact ref={gridRef} rowData={rowData} columnDefs={columnDefs} getRowId={(params) => params.data.id} />getRowId 返回的值必须是业务里唯一的主键。如果拿不到稳定 id,applyTransaction 会更新错行,这是这类表格最容易出现的玄学问题。我的习惯是任何列表数据都在前端维护一个唯一 id 字段,不依赖数据库自增主键,避免跨页合并数据时冲突。
3.4 把排序、过滤、分页一次开齐的配置
开排序和过滤不需要每个列写一遍,用 defaultColDef 统一设置即可。这个配置会应用到所有列,除非某个列在自己的列定义里覆盖。
const defaultColDef = useMemo( () => ({ sortable: true, filter: true, resizable: true, }), [] ); <AgGridReact columnDefs={columnDefs} rowData={rowData} defaultColDef={defaultColDef} pagination paginationPageSize={20} rowSelection="multiple" />;sortable 控制列头是否可点击排序,filter 默认为每一列生成合适的过滤器。数字列会自动匹配数字过滤器,文本列自动匹配文本过滤器。如果要指定类型,可以写成 filter: "agNumberColumnFilter" 或 "agSetColumnFilter"。分页开启后默认每页 100 条,我一般会显式设置 paginationPageSize。rowSelection 用于开启多选,如果你还需要 checkbox,在列定义里加上 checkboxSelection: true。
提示:filter: true 不等于所有数据都能被正确过滤。日期字段必须用正确的日期格式,否则过滤器只能按字符串匹配,跨年份的筛选结果会让你怀疑人生。
4. 自定义单元格渲染与编辑:从“表格只展示”到“表格能操作”
真实业务里没有多少表格是纯展示的。状态码要转成中文,操作列要放按钮,金额要格式化,某些列还得允许用户双击编辑。这一章讲清楚三个最常见的自定义点。
4.1 用 valueFormatter 把后端返回的状态码转成可读文案
后端返回的状态字段往往是 SUCCESS、PENDING、FAILED 这类枚举,直接显示给业务方看非常不友好。最简单的做法是 valueFormatter,它接收原始值,返回你要展示的文本,不改动 rowData 本身。
const STATUS_TEXT = { SUCCESS: "已完成", PENDING: "排队中", FAILED: "失败", }; const columnDefs = [ { field: "status", headerName: "状态", valueFormatter: (params) => STATUS_TEXT[params.value] ?? params.value, }, ];valueFormatter 的第一个参数是 params,里面至少包含 value 和 data。value 是当前单元格原始值,data 是当前行完整对象。这里用空值合并运算符兜底,遇到没匹配上的枚举值就原样输出,避免出现一堆 undefined。如果你只要显示文本,用它就够了;如果要改样式,比如“失败”显示成红色,就不能靠 formatter,而是配置 cellClassRules 或自定义 cellRenderer。
4.2 写一个自定义 cellRenderer:操作列里的按钮和进度条
操作列是 cellRenderer 最常见的场景。这个函数组件接收 ag-grid 注入的 props,返回的 DOM 会被放进单元格。
const ActionCellRenderer = (props) => { const onDetail = () => { const latest = props.node.data; console.log("查看详情", latest.id); }; const onEdit = () => { props.api.startEditingCell({ rowIndex: props.node.rowIndex, colKey: "remark", }); }; return ( <div style={{ display: "flex", gap: "8px" }}> <button onClick={onDetail}>详情</button> <button onClick={onEdit}>编辑备注</button> </div> ); };在列定义里使用它:
{ field: "action", headerName: "操作", cellRenderer: ActionCellRenderer, width: 160, }props 里比较关键的字段是 value、data、node、api。node 是行节点,data 是当前行数据。我需要专门提醒一个坑:不要在 cellRenderer 创建时把 props.data 直接闭包进事件回调,排序、过滤、行复用之后,闭包里的引用可能还是旧行。正确做法是在点击事件内部通过 props.node.data 现取,这样拿到的永远是最新数据。startEditingCell 可以主动让某个单元格进入编辑状态,这里我用来让“编辑备注”按钮直接跳到 remark 列。
4.3 cellEditor 的最小实现:输入校验与失焦回填
如果你需要用户直接在表格里改数字或文本,就要写 cellEditor。ag-grid 的编辑器在 React 里建议用类组件实现,因为它底层需要实例上的 getValue 方法,函数组件没办法稳定暴露这个接口。
import { Component } from "react"; class NumericTextEditor extends Component { constructor(props) { super(props); this.state = { value: props.value ?? "" }; } getValue() { const num = Number(this.state.value); return Number.isFinite(num) ? num : this.state.value; } isCancelAfterEnd() { return this.state.value.trim() === ""; } render() { return ( <input autoFocus value={this.state.value} onChange={(e) => this.setState({ value: e.target.value })} style={{ width: "100%", boxSizing: "border-box" }} /> ); } }列定义里开启编辑:
{ field: "amount", headerName: "金额", editable: true, cellEditor: NumericTextEditor, }getValue 是提交时被调用的方法,返回值会作为新的单元格值写入行数据。这里我做了一层 Number 转换,保证数字列拿到的是数字而不是字符串。isCancelAfterEnd 返回 true 表示本次编辑取消,我用来处理空输入。编辑器默认 Enter 提交、Esc 取消,这是内置行为,不需要额外配置。写自定义编辑器时最容易漏的是样式,表格单元格里的 input 如果不设宽度,会出现内容溢出,所以我给 input 设置了 width: 100% 和盒模型修正。
5. ag-grid-community 性能与兼容性避坑:五条值得写进团队文档的排错记录
这个表格库整体稳定,但特定场景下的坑比较固定。每一条都是我先看到现象、再定位原因、最后给出解决办法的真实路径,希望你不用重走一遍。
5.1 现象:表格在隐藏 Tab 里初始化,切回来全部挤成一列
中后台系统特别爱用 Tab 页签,我遇到过表格放在非激活 Tab 里初始化,切回来整列宽度全部塌缩,滚动条也消失。原因是初始化时父容器处于 display: none 状态,宽度被浏览器算成 0,ag-grid 按照 0 宽度绘制了列布局,等 Tab 显示时它并不知道外部尺寸变了。
解决方式是在 Tab 切换完成后主动通知 grid 尺寸变化。
const handleTabShow = () => { setTimeout(() => { gridRef.current?.api.onGridSizeChanged(); }, 50); };延迟 50ms 是为了等 Tab 动画或样式切换完成,确保父容器已经有真实宽度。如果 Tab 是反复切换的那种,我建议在初始化时就判断当前 Tab 是否激活,不激活就先不渲染表格组件,等到激活再渲染,从根上避免这个测量问题。
5.2 现象:后端分页,前端点过滤只过滤了当前页
这个坑几乎是服务端分页标配。后端按 page 返回数据时,前端拿到的只是当前页的几十行,此时 ag-grid 社区版默认的过滤和排序都只作用在这几十行上,用户以为自己筛了全量,实际只筛了当前页,结果明显不对。
原因很明确:社区版没有服务端行模型,它不知道后面的数据长什么样。解决办法是把 filterModel 和 sortModel 传给后端,让后端在 SQL 层过滤排序。
const getRequestParams = () => { const filterModel = gridRef.current.api.getFilterModel(); const sortModel = gridRef.current.api.getSortModel(); return { page, pageSize, filterModel: JSON.stringify(filterModel), sortModel: JSON.stringify(sortModel), }; };后端拿到 filterModel 后需要做条件拼接。需要注意的细节是,onFilterChanged 和 onSortChanged 事件会在每次过滤排序变化时触发,如果直接在里面发请求,可能连续打出好几个重复请求。我的做法是加一个防抖定时器,等 300ms 内的最后一次变化再请求。
5.3 现象:自定义操作列按钮点击后拿到的是旧行数据或旧编号
表格排序、过滤后,行的显示顺序会变。ag-grid 为了性能会复用单元格 DOM,cellRenderer 实例可能不会重新创建,导致按钮回调里闭包的数据还是最初渲染那一行的数据。典型表现是点第一行的“详情”,弹出来的却是另一个人的单号。
原因就是事件闭包绑死了旧引用。解决方式分两步。第一,事件回调内部永远通过 props.node.data 去取当前数据,不要用 props.data 做闭包缓存。
// 错误示范:闭包锁定了渲染时的旧数据 const onDetail = () => { console.log(props.data.id); }; // 正确写法:点击时再取当前行数据 const onDetail = () => { console.log(props.node.data.id); };第二,如果发现更新行数据后按钮里的文字或样式没有变,这通常是 cellRenderer 没有刷新。强制方案是调用gridRef.current.api.redrawRows(),它会重建可视区域的行节点,代价比 refreshCells 大,但能解决顽固旧渲染。
5.4 现象:保存的筛选模型恢复不生效
很多系统需要让用户保存一组筛选条件,下次进来一键还原。我用 getFilterModel 保存,刷新页面再用 setFilterModel 恢复,结果有些列生效,有些列没反应,像是被静默吞掉了。
定位后发现 filterModel 里的 key 是列定义的 field,但恢复时必须保证对应列存在,并且列类型一致。如果列被隐藏了,或者列过滤器和保存时不是同一个类型,恢复就会失败。更稳妥的做法是把列状态和筛选模型一起保存,恢复时先恢复列状态,再设置 filterModel。
const saveGridState = () => { return { filterModel: gridRef.current.api.getFilterModel(), columnState: gridRef.current.columnApi.getColumnState(), }; }; const restoreGridState = (state) => { gridRef.current.columnApi.applyColumnState({ state: state.columnState }); gridRef.current.api.setFilterModel(state.filterModel); };applyColumnState 会恢复列的顺序、宽度、排序和显隐状态。顺序不能反,因为 setFilterModel 依赖过滤器已经存在。这个组合拳同样适用于把状态存到 localStorage 或 URL 参数里。
5.5 现象:单元格里塞富文本或图片,滚动时闪烁、位置飘
运营后台的备注列总想放一段带格式的 HTML,或者放一张缩略图。我试过直接在 cellRenderer 里返回 img 标签,结果滚动时图片重新加载,行高也被撑得忽高忽低,整个表格像抽风一样。
原因是虚拟滚动复用单元格 DOM,滚动过程中单元格内容频繁重建,图片加载时机不可控,行高变化又影响滚动位置。解决思路是把富文本和图片从表格核心区域挪出去。常见做法是把完整内容放在 tooltip 里,行内只显示纯文本摘要。
{ field: "remark", headerName: "备注", tooltipField: "remark", tooltipComponent: RichTooltip, }tooltip 在 hover 时再渲染,不会参与虚拟滚动的行高计算。如果图片一定要在列表里展示,那就固定一个宽高,并开启 suppressCellFlash 减少闪烁,同时给行设置固定 rowHeight。反正别让单元格高度动态变化,这是虚拟滚动最讨厌的情况。
6. 最后再补两刀:把 ag-grid-community 用成一套可复用模板
6.1 用 useMemo 缓存 columnDefs 和 defaultColDef
React 组件每次 render 都会执行函数体,如果直接写一个数组变量,ag-grid 会认为列定义是新的,可能触发不必要的重绘,严重时连排序状态都丢。我习惯把所有列相关配置都放进 useMemo。
const columnDefs = useMemo( () => [ { field: "name", headerName: "名称" }, { field: "status", headerName: "状态" }, ], [] ); const defaultColDef = useMemo( () => ({ sortable: true, filter: true, resizable: true }), [] );这个习惯成本极低,但能省掉大量“为什么表格自己刷新”的排查时间。依赖数组里如果有动态字段,比如权限控制,才需要把对应变量加进去。
6.2 把排序和筛选状态做成“视图切换”
业务方常见的诉求是“我要一键只看今日成功订单”。与其教他们手动配置一堆筛选条件,不如给表格做一个预置视图按钮。核心代码还是 getFilterModel 与 setFilterModel。
const applyTodaySuccessFilter = () => { const gridApi = gridRef.current.api; gridApi.setFilterModel({ createdAt: { type: "greaterThan", dateFrom: new Date().toISOString().slice(0, 10), }, status: { values: ["SUCCESS"] }, }); };注意日期过滤器的 model 结构要求列定义里 filter 是 agDateColumnFilter,并且字段名要和 filterModel 的 key 完全一致。我给一个业务配置了五六个这样的快捷按钮,全部是同一套逻辑,代码极其好维护。配合 5.4 的 saveGridState,还可以把每个按钮对应的完整网格状态存到 localStorage,刷新页面后一键还原。
我现在的习惯是:任何新列表页先按第 3 章的最小 demo 搭,再往上面加功能;凡是涉及行更新的逻辑,一律走 applyTransaction,不在业务里整行 push。这个习惯帮我避开了绝大多数刷新闪烁和误操作。希望帮到你。
本文还有配套的精品资源,点击获取