简介:ag-Grid Community 是面向 Web 前端开发者的开源数据网格组件资源包,适合使用 JavaScript、React、AngularJS 或 Web Components 构建后台管理、数据看板与复杂表格的开发者参考。它围绕大数据量虚拟滚动渲染、排序过滤分组、列宽调整与冻结、分页、单元格编辑校验、自定义渲染器、列定义和主题定制等能力组织源码与示例,可帮助读者理解网格初始化、数据绑定、运行时 API 改列及事件监听等常见用法。压缩包共 2000 个文件,约 190.14MB,以 js 与 ts 源码、html 示例页、css 主题样式、json 配置为主,并含 png、svg 图示与 md 文档,便于按模块查阅。已有 1464 人学习下载。包内保留较完整的示例页面与多套主题样式,可用于对照 API 搭建表格原型、验证跨框架集成方式、排查渲染与样式问题,也可作为前端数据展示方案的参考底稿。
1. ag-grid-community 是什么:为什么这个前端开源表格库值得先跑一遍
ag-grid-community是一个前端开源数据表格库的社区版本,通过 npm 安装后,在 React、Vue、Angular 或原生 JS 项目里都能直接接入。它解决的核心问题并不在于“把数组渲染成表格”,因为那部分自己写也不难;真正麻烦的是排序、过滤、分组、聚合、分页、列宽拖拽、列固定、行虚拟滚动这些功能都要一起支持,而且数据量上万条时也不能卡顿。社区版采用宽松的开源许可,绝大多数后台管理、报表、运营看板场景够用,不需要付费授权。适合谁?适合正在为“数据表格越写越难维护”而烦恼的前端,也适合做内部工具、给运营或财务做数据核对页面的团队。它并不是开箱即用的,默认样式和容器高度都需要自己处理;第一次接入如果遇到白屏,不必怀疑人生,后面会说明怎么处理。
2. 装库与最小可跑:三行依赖到一张真表格
把ag-grid-community跑起来,实际操作比想象中省事,但真正的坑不在安装,而在于“装完之后页面一片空白”和“表格挤成一条线”这两件事。这两件事占了新手翻车案例的大部分。这一章先把依赖装对、把最小骨架跑通,再处理主题和容器高度,让表格先正常显示出来,后面的配置才有意义。
2.1 依赖怎么挑:社区核心包与框架适配层
很多人第一次搜到的是ag-grid、ag-grid-react、@ag-grid-community/core这几个名字,容易装混。实际分工很清楚:ag-grid-community是与框架无关的核心渲染与逻辑包,ag-grid-react/ag-grid-vue3/ag-grid-angular才是对应框架的适配层。React 项目要装两个包,只装一个跑不起来。
# React 项目:核心包 + React 适配层 npm install ag-grid-community ag-grid-react # Vue3 项目则换成 # npm install ag-grid-community ag-grid-vue3装完之后先看node_modules/ag-grid-community/styles/目录,主题 CSS 就在里面。较新的版本也开始支持通过theme参数传主题对象,但 CSS 类的方式兼容性最好、迁移成本最低,我一般先用 CSS 类跑通,再评估要不要换新主题 API。
社区版和企业版的边界也需要提前了解,这会直接影响技术选型:行分组、聚合、透视表、Excel 导出、右键菜单里的部分高级项属于企业版能力,社区版里调用会提示需要授权。所以在做需求评审时,如果产品经理说“要像 Excel 一样拖个透视表出来”,你需要先确认这属于社区版范围还是需要付费,不要等写完才发现核心功能不可用。反过来,几万行数据的虚拟滚动、多列排序、列过滤、CSV 导出(社区版可自行实现)、单元格编辑这些,社区版完全够用。
提示:不要把
ag-grid-community和@ag-grid-community/core当成两个不同的库。它们是同一套包的不同入口形式,选一种安装,不要混装,否则会出现两份实例互相不认的问题。
2.2 React 里的最小可跑骨架
最小可跑结构只需要rowData和columnDefs两个属性。下面这段代码可以直接复制进一个 React 页面,改掉数据源就能跑。
import React, { useMemo, useState, useCallback } from 'react'; // 核心包与 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 OrderTable() { // 列定义:用 useMemo 固定引用,避免父组件重渲染时整表闪烁 const columnDefs = useMemo(() => [ { field: 'orderNo', headerName: '订单号', width: 180 }, { field: 'customer', headerName: '客户', flex: 1, minWidth: 120 }, { field: 'amount', headerName: '金额', width: 120, type: 'numericColumn' }, { field: 'status', headerName: '状态', width: 100 }, ], []); const [rowData] = useState([ { orderNo: 'SO20240101', customer: '某公司', amount: 12800, status: '已发货' }, { orderNo: 'SO20240102', customer: '某实验室', amount: 4500, status: '待审核' }, ]); // 网格初始化完成的回调,这里拿到的是操作表格的总入口 const onGridReady = useCallback((params) => { console.log('grid ready', params.api.getDisplayedRowCount()); }, []); return ( // 外层容器必须给高度,否则表格高度为 0 <div className="ag-theme-quartz" style={{ height: 480, width: '100%' }}> <AgGridReact rowData={rowData} columnDefs={columnDefs} defaultColDef={{ sortable: true, filter: true, resizable: true }} onGridReady={onGridReady} /> </div> ); }几个参数值得单独说清楚。defaultColDef是全局默认列配置,写了sortable、filter、resizable之后,每一列自动继承,不用在 20 个列定义里各写一遍;这也是绝大多数项目实际采用的写法。type: 'numericColumn'不是装饰,它会同时把该列右对齐并套用数字格式化,金额、数量这类字段不加它,视觉上会很别扭。onGridReady回调里的params.api是后续一切命令式操作的入口,包括刷新数据、导出、拿过滤条件,建议在项目初始化时就把它保存到 ref 或状态里。
至于引用稳定性,columnDefs如果每次渲染都重新生成,表格内部会认为列结构变了,做一次完整的列重建,表现就是拖好的列宽弹回去、排序状态丢失、表格闪一下。这个问题在企业项目里非常常见,一开始写的时候用useMemo包一层,后面能省下不少排查时间。
2.3 主题与容器高度:表空白和“挤成一条线”的来源
样式这一块有两个必查项,按顺序排掉,基本能解决 90% 的初始化问题。
第一是 CSS 有没有引全。只引ag-grid.css不引主题 CSS,表格会有结构但完全没有配色,边框和表头都正常,看起来只是“有点简陋”;反过来那种整块纯白、什么都看不到的,通常是一个都没引进来。
第二是容器高度。ag-grid的根元素是 100% 高度撑满父容器的,如果父容器高度是 auto 或者 0,表格就渲染成一条细线。这一点和很多表格组件的行为不一样,新手最容易在这里卡住。
/* 推荐写法:外层容器显式给高度,内层表格自适应 */ .table-wrapper { height: calc(100vh - 160px); /* 减去页头、工具栏的固定高度 */ width: 100%; min-height: 320px; /* 小屏兜底,避免压扁到不可用 */ } /* 若在 flex 布局里,父级链路都要能被压缩,否则高度算不出来 */ .page-body { display: flex; flex-direction: column; min-height: 0; /* 这一行是 flex 容器高度塌陷的解药 */ }min-height: 0这一行看着不起眼,但只要页面是display: flex嵌套结构,缺了它父容器就只会被内容撑开、不会按剩余空间收缩,于是calc(100vh - 160px)算出来的高度失效。排查时如果发现表格高度总是比预期大一圈或者出现双滚动条,先回头检查这条链路。
还有一点容易忽略:主题类名要加在直接包裹AgGridReact的那个 div 上,加在更外层的祖先节点上有时能生效、有时不能,取决于层级里有没有别的样式覆盖。统一加在直接父级,能少一类难以复现的样式问题。
3. 列定义与数据:把业务字段翻译成表格语言
能显示之后,接下来要面对的是实际业务字段。接口通常不会直接给出field、headerName所需的字段,往往要做字段映射、格式化、状态渲染、操作按钮渲染。这一层如果设计得清晰,后面新增一列只要加一行配置;设计得混乱,每次加列都会牵动组件渲染逻辑。
3.1 影响使用体验的列配置项
下面这张表列出的配置项,基本覆盖了 80% 的列配置需求,建议作为团队内固定配置约定:
| 配置项 | 作用 | 常见取值与建议 |
|---|---|---|
field | 绑定数据字段 | 支持点号路径,如customer.name |
headerName | 表头文案 | 与field分离,便于后端字段名不变、前端显示中文化 |
width/flex/minWidth | 宽度策略 | 固定列用width,弹性列用flex,搭配minWidth防止挤压 |
type | 类型预设 | numericColumn右对齐、dateColumn便于日期处理 |
sortable/filter | 排序与过滤 | 一般不逐列写,放defaultColDef里 |
pinned | 固定列 | 'left'常用于勾选列与主键列 |
hide | 默认隐藏 | 配合列选择面板做“可选列”功能 |
valueFormatter | 显示格式化 | 金额千分位、状态码转文案,不改变原始值 |
cellRenderer | 自定义渲染 | 渲染标签、徽章、操作按钮 |
cellClass/cellClassRules | 单元格样式 | 按数值区间或状态上色 |
editable | 可编辑 | 与onCellValueChanged配合做行内编辑 |
值得注意的是flex和width混用的情况。给所有列都设了width,表格总宽小于容器宽时会右侧留白;全设flex,列多的时候会挤到看不清。实践里的做法是:主键类、金额类、状态类固定宽度(120~180 之间),只有文本类长字段(客户名、备注、地址)用flex: 1配minWidth。
3.2valueGetter、valueFormatter、cellRenderer各管一段
这三个名字很像,很多人第一次都分不清,选错之后会带来一些后续麻烦:用格式化函数处理后端返回的复合字段,会导致排序和过滤失效。它们的职责边界其实很清楚:
const columnDefs = useMemo(() => [ { field: 'amount', headerName: '金额(元)', // valueFormatter 只改显示,原始值仍是数字,排序/过滤照常工作 valueFormatter: (p) => (p.value == null ? '-' : p.value.toLocaleString('zh-CN')), }, { // 后端把姓名拆成两列,用 valueGetter 合成一个虚拟字段 headerName: '客户联系人', colId: 'contact', valueGetter: (p) => [p.data.lastName, p.data.firstName].filter(Boolean).join(''), // 虚拟字段必须自己能提供比较逻辑,否则排序会按字符串瞎排 comparator: (a, b) => String(a).localeCompare(String(b), 'zh-CN'), }, { headerName: '状态', field: 'status', // cellRenderer 返回 JSX,可以放标签、按钮、链接 cellRenderer: (p) => ( <span style={{ color: p.value === '待审核' ? '#d46b08' : '#389e0d' }}> {p.value} </span> ), }, ], []);三条规则小结一下:只想改看起来的样子并且不打算用它排序过滤,可以选valueFormatter;字段不存在、需要拼接或计算出一个新值,可以选valueGetter,但要记得配合comparator,否则排序结果会不符合预期;要渲染组件级内容(按钮、标签、进度条)时,才应该用cellRenderer。
另外说一句valueFormatter里最容易出错的地方:返回null或者空数组时,单元格会显示空白,但这一行在 CSV 导出时也会跟着空。如果业务上要求“空值显示为 -”,就在 formatter 里显式兜底,不要在cellClassRules里处理,那样只会改变颜色,不改变文字。
3.3 引用稳定与useCallback:状态被重置的根源
上一章提过columnDefs的引用问题,这里展开说为什么它这么关键。表格内部对比列定义时用的是引用比较,只要数组换了新地址,它就按“列结构可能变了”处理,重建列对象,连带清掉列宽、排序、过滤、固定状态。父组件里任何一次useState更新都会让columnDefs重新生成,于是用户刚拖好的列宽就会立刻恢复原状。
// 反例:每次渲染都是新数组,表格内部判为列变更 // const columnDefs = [{ field: 'a' }, { field: 'b' }]; // 正例:依赖项写清楚,只有真正需要变的字段才进依赖 const columnDefs = useMemo(() => buildColumns(userRole, canEdit), [userRole, canEdit]); // 回调同理,内联箭头函数会让表格每次重挂监听 const onCellClicked = useCallback((e) => { if (e.colDef.field === 'orderNo') openDetail(e.data.orderNo); }, [openDetail]);有一个容易出错的边界:把columns的构建逻辑抽成函数放在组件内部,即使外面套了useMemo,函数本身仍然会随渲染重建,但只要useMemo的依赖没有变化就不会执行,问题不大。真正会出问题的是把rowData和columnDefs一起写进同一个useMemo,并且依赖里放了整个data对象,这时候数据一刷新列状态就没了。数据和列,分开 memo,这条建议在几个项目里都验证过,能省掉大量“为什么排序又没了”的排查时间。
4. 行模型与数据量:四种rowModelType的边界
数据量一上去,选型就要重新考虑。几百行以内,什么写法都行;几千行时开始感觉滚动轻微发涩;到几万行还开着autoHeight,浏览器基本上就卡死了。要避免这个,需要理解行模型(row model)是怎么回事——它是表格决定“当前该渲染哪些行、这些行从哪儿来”的机制。
4.1 四种行模型的适用边界
| 行模型 | 数据来源 | 适用数据量 | 典型场景 |
|---|---|---|---|
clientSide(默认) | 一次性把全量数据传给rowData | 1 万行以内体验良好 | 内部管理页、报表、导出前的预览 |
infinite | 分块向datasource拉取 | 十万级可用 | 日志列表、订单流水,滚动到底自动加载 |
viewport | 按可视区拉取 | 十万级以上 | 只关心当前屏幕内容的宽表 |
serverSide | 服务端完成排序过滤分组 | 百万级 | 企业版能力,社区版不可用 |
这里有个关键点必须说清楚:社区版不支持serverSide行模型。它是企业版特性之一。如果项目要做“服务端分页 + 服务端排序 + 服务端过滤 + 服务端分组”,用社区版就只能用infinite行模型,在getRows里自己把控排序过滤参数带到后端,把分页和排序粒度做粗一点。这不一定是能力不足,很多中后台的实际需求用infinite完全能覆盖,只是分页体验从“页码翻页”变成了“滚动加载”。
4.2infinite行模型的datasource写法
下面是一个可以直接套用的骨架,把fetch换成项目里的请求方法即可。注意三个细节:successCallback的第二个参数是总行数,不传的话表格不知道什么时候停;failCallback一定要调,否则请求挂了表格会一直转圈;块大小cacheBlockSize和后端分页大小要对齐,不对齐会出现重复行或者漏行。
const gridOptions = useMemo(() => ({ rowModelType: 'infinite', cacheBlockSize: 50, // 每块 50 行,需与后端 pageSize 一致 maxBlocksInCache: 10, // 最多缓存 10 块,超出淘汰,控制内存 infiniteInitialRowCount: 1, // 初始占位行数,避免滚动条一开始就是满的 columnDefs: [ { field: 'orderNo', headerName: '订单号', width: 180 }, { field: 'customer', headerName: '客户', flex: 1 }, ], datasource: { getRows(params) { const { startRow, endRow, sortModel, filterModel } = params; const pageSize = endRow - startRow; const page = Math.floor(startRow / pageSize) + 1; // 服务端排序字段需要做映射,不能直接把 sortModel 丢给后端 const sort = sortModel.map((s) => `${s.colId},${s.sort}`).join(';'); fetch(`/api/orders?page=${page}&size=${pageSize}&sort=${encodeURIComponent(sort)}`) .then((r) => r.json()) .then((res) => { // 第二个参数是总行数;返回 -1 表示还有未知数据,表格会继续请求 params.successCallback(res.rows, res.total); }) .catch(() => { // 失败必须回调,否则表格停在 loading 状态 params.failCallback(); }); }, }, }), []);这段代码里最值得推敲的是filterModel。表格传过来的过滤模型是按列组织的对象,像{ status: { filterType: 'text', type: 'equals', filter: '待审核' } }。直接把它序列化发给后端,大部分后端是接不住的,需要在中间做一次翻译层:文本过滤映射为like,数字过滤映射为区间,日期过滤映射为起止时间。这个翻译层建议单独写成一个纯函数,因为排查时你一定会想看“前端到底把什么条件发给了后端”。
注意:
infinite模式下,getRowId依然建议配上。没有稳定 id 时,滚动回来的块数据会按行索引对齐,一旦后端在两次请求之间插入了新数据,就会出现同一行显示两次的体验问题。
4.3 大数据量下的几个性能开关
数据在三千行以上、又不得不用clientSide时,以下几个配置的重要性就开始体现,成本很低,收益明显。
<AgGridReact rowData={rowData} columnDefs={columnDefs} getRowId={(p) => p.data.orderNo} // 稳定主键,增量更新和选中态都靠它 rowBuffer={10} // 可视区外多渲染的行数,滚动时减少白屏 suppressColumnVirtualization={false} // 列多时保持虚拟化,别关 suppressRowVirtualization={false} animateRows={false} // 数据频繁刷新时关掉动画,省 CPU />getRowId是最值得加的一条。有了它,表格能在数据刷新时按主键做 diff,只更新变化的行,选中状态、展开状态、编辑中的单元格都能保住。没有它,每次刷新都是一次全量重建,选中会丢、滚动位置可能跳。
rowBuffer调大能减少快速滚动时的空白区,但会提高渲染成本,10 到 20 之间是个比较稳的区间。animateRows在演示里好看,实际业务中每次数据刷新都要重播动画,几万行的场景下会占用明显的主线程时间,我会直接关掉。
还有一件事需要说明:autoHeight和虚拟滚动是不兼容的。开启autoHeight之后表格会放弃虚拟化、渲染全部行,三千行以上就会明显卡顿。如果业务需要“行高自适应内容”,正确的做法是用getRowHeight返回函数或者固定行高配合wrapText加autoHeight的替代方案,而不是打开全局的autoHeight开关。
5. 避坑笔记:五个现场与对应排查路径
下面这五条都是我在实际项目里处理过的问题,按“现象 → 原因 → 解决”写,方便你在遇到同类表现时直接对照。
5.1 白屏与容器高度塌陷
现象:组件挂上了,控制台没报错,但页面上什么都看不到,或者只有一条细线。
原因:绝大多数是父容器高度为 0 或者 auto。表格根节点使用 100% 高度,父级没高度它就撑不开。少数情况是主题 CSS 没引进来,表格画出来了但没有配色,视觉上和背景融为一体。
解决:先打开开发者工具检查包裹 div 的computed height,如果是 0 或者 auto,就往上逐层查父级;在 flex 布局里补min-height: 0;确认ag-grid.css和主题 CSS 都引入了,并且主题类名加在直接父级上。
5.2 数据更新了表格不动
现象:接口返回了新数据,console.log打出新数组,但表格里还是旧内容。
原因:常见有两种情况。一是把rowData存进了useRef或者直接改了原数组的某一项,引用没变,表格判断不出变化;二是直接调了api.setRowData(),而项目用的是较新的版本,这个方法的语义已经变了,推荐走setGridOption。
解决:用不可变更新的方式替换rowData(setState([...list])或map出新数组);或者显式调用api.setGridOption('rowData', nextRows)。配了getRowId的情况下,新数组里只有部分对象不同也能正确做增量,不需要重建整表。
5.3 排序、过滤、列宽一刷新就复位
现象:用户调好排序和筛选,页面里任何一个无关的状态更新(打开弹窗、切个 tab)之后,全部回到初始状态。
原因:columnDefs或rowData每次渲染都生成新引用,表格内部判定为结构变化,重建列对象。另一种情况是把表格组件放在了条件渲染的key会变的位置,本质上是被卸载重建了。
解决:columnDefs用useMemo固定,依赖只放真正影响列结构的变量;回调函数用useCallback;检查父级有没有给表格套一个会变的key。如果确实需要保留用户的视图状态,用getColumnState/setColumnState或者过滤模型做持久化,不要指望组件内部帮你保留。
5.4 单元格编辑改了值但保存不到
现象:单元格能双击编辑,输入完失焦,看起来变了,但提交时拿到的还是旧数据。
原因:rowData里的对象是浅拷贝或者直接来自接口缓存,onCellValueChanged里没有把新值写回数据源;也有一种情况是列上没开editable,用户看到的“编辑”其实是cellRenderer里自己放了一个 input,那个 input 和表格的数据模型之间根本没有连接。
解决:在onCellValueChanged里根据event.data和event.colDef.field更新数据源,走不可变更新;如果确实需要在单元格里放自定义输入框,就必须自己接onChangewriteback,或者用官方的cellEditor机制,别只放个 input 当装饰品。
5.5 滚动卡顿与内存持续上涨
现象:数据量上来之后快速滚动掉帧,长时间停留页面内存持续增长。
原因:cellRenderer里返回了重量级组件(带图表、带复杂样式的标签),或者每次渲染都在cellRenderer里现场创建组件实例;另一种是infinite模式下maxBlocksInCache没设,块数据无限缓存。
解决:cellRenderer尽量返回轻量结构,复杂组件用useCallback包裹的稳定引用;重内容改用cellRendererSelector按需加载;infinite模式显式设置maxBlocksInCache,一般 10 到 20 块够用;关掉animateRows;列数超过 30 时保持列虚拟化开启,别为了“想一次导出全部”而关掉它。
提示:排查性能问题时不要只看 React DevTools 的提交次数,表格内部有大量非 React 的 DOM 操作。用浏览器性能面板录一段滚动过程,看主线程的 long task 落在渲染还是业务回调上,路径会清楚很多。
6. 进阶:过滤联动与视图状态持久化
真正投入使用的表格,通常都需要“表格外的搜索框能过滤表格”以及“用户上次的筛选条件要记住”。这两件事用到的 API 都不复杂,但组合方式有讲究。
外部搜索框最直接的做法是设置quickFilterText,它会在所有可见列里做模糊匹配:
const [keyword, setKeyword] = useState(''); <AgGridReact rowData={rowData} columnDefs={columnDefs} quickFilterText={keyword} // 只让业务关心的列参与快速搜索,避免搜到隐藏的操作列 quickFilterMatcher={(quickFilterParts, rowNode) => { const d = rowNode.data || {}; return quickFilterParts.every((part) => [d.orderNo, d.customer, d.status].join(' ').includes(part) ); }} />quickFilterMatcher是这套能力里最有用的一环:默认的快速过滤会把行内所有字符串字段都扫一遍,隐藏列、内部 id、备注都会命中,用户搜“2024”结果冒出一堆莫名其妙的数据。显式指定参与搜索的字段,能省去不少疑问。
状态持久化则使用过滤模型和列状态两个 API:
// 保存:把当前过滤条件与列状态序列化进本地存储 function saveView(api, key) { const payload = { filter: api.getFilterModel(), columns: api.getColumnState(), // 含列宽、排序、显隐、固定 }; localStorage.setItem(key, JSON.stringify(payload)); } // 恢复:必须在网格初始化完成后调用,太早调 api 还没准备好 function restoreView(api, key) { const raw = localStorage.getItem(key); if (!raw) return; const { filter, columns } = JSON.parse(raw); api.setFilterModel(filter); if (columns) api.applyColumnState({ state: columns, applyOrder: true }); }getColumnState()拿到的信息包括列宽、排序方向、是否隐藏、是否固定,恢复时applyOrder: true会把列顺序也一并还原。要注意的是,列定义发生过增删之后,旧的状态里可能包含已经不存在的colId,applyColumnState会静默忽略,不会报错,排查时容易疑惑。稳妥的做法是恢复前先做一次过滤,只保留当前columnDefs里存在的colId。
有一个细节我遇到过:restoreView必须放在onGridReady或者onFirstDataRendered里,放在组件函数体里直接调,api还没初始化完,调了没反应也不报错,属于很难找到原因的静默失败。
血泪经验是:视图状态别只存过滤和列宽这两项。用户还可能改了分页大小、展开了分组、切换了主题密度,这些也得一并存下来,不然用户会觉得“记住是记住了,但没记全”。从那以后我每次做带筛选的列表页,都会在onGridReady之后强制走一遍“保存—刷新—恢复”的流程,确认能完整还原才继续往下写业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取