
简介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 classNameag-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.nameheaderName表头文案与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列多的时候会挤到看不清。实践里的做法是主键类、金额类、状态类固定宽度120180 之间只有文本类长字段客户名、备注、地址用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默认一次性把全量数据传给rowData1 万行以内体验良好内部管理页、报表、导出前的预览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。解决用不可变更新的方式替换rowDatasetState([...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会把列顺序也一并还原。要注意的是列定义发生过增删之后旧的状态里可能包含已经不存在的colIdapplyColumnState会静默忽略不会报错排查时容易疑惑。稳妥的做法是恢复前先做一次过滤只保留当前columnDefs里存在的colId。有一个细节我遇到过restoreView必须放在onGridReady或者onFirstDataRendered里放在组件函数体里直接调api还没初始化完调了没反应也不报错属于很难找到原因的静默失败。血泪经验是视图状态别只存过滤和列宽这两项。用户还可能改了分页大小、展开了分组、切换了主题密度这些也得一并存下来不然用户会觉得“记住是记住了但没记全”。从那以后我每次做带筛选的列表页都会在onGridReady之后强制走一遍“保存—刷新—恢复”的流程确认能完整还原才继续往下写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取