ag-grid为什么要区分好几种模型策略(clientSide、serverSide、infinite、viewport),它们有什么区别?

361 阅读6分钟

引言

ag-Grid 提供多种行模型(Row Model)的核心原因在于应对不同规模数据和场景的技术需求。虽然社区版(ag-grid-community)和企业版(ag-grid-enterprise)在功能上存在差异,但模型设计的本质区别源于技术实现需求,不仅仅是为了区分不同功能进行收费:

  1. 数据量级差异:万级与百万级数据的处理策略不同
  2. 计算位置选择:前端计算与服务端计算的性能权衡
  3. 实时性要求:静态数据与流式数据的更新机制差异

本文将从技术实现角度解析各模型的特性和适用场景。

本文以ag-grid v33版本为准

一、核心概念对比:Client-Side vs Server-Side

1. 数据处理模式差异

四大模型核心差异

模型类型数据加载方式内存管理适用场景企业版依赖
Client-Side全量加载浏览器内存<10万行静态数据
Infinite分页预加载滚动缓存池中等规模可滚动数据
Server-Side服务端计算+按需加载服务端内存百万级OLAP场景
Viewport视窗动态加载可视区域维护实时流数据(如股票行情)

2. 架构原理示意图

graph TD
    subgraph ag-Grid 架构图
        A[Grid Core] --> B[Grid API]
        A --> C[Column Definitions]
        A --> D[Row Models]
        A --> E[Events]
        A --> F[Rendering Engine]
        
        D --> D1[Client-Side Row Model]
        D --> D2[Server-Side Row Model]
        D --> D3[Infinite Row Model]
        D --> D4[Viewport Row Model]
        
        B --> B1[Sorting]
        B --> B2[Filtering]
        B --> B3[Pagination]
        B --> B4[Selection]
        
        E --> E1[Row Click]
        E --> E2[Cell Edit]
        E --> E3[Model Updated]
        
        F --> F1[DOM Renderer]
        F --> F2[Virtual Scroll]
        F --> F3[Cell Renderers]
        F --> F4[Cell Editors]
        
        subgraph Data Flow
            G[Data Source] -->|Client-Side| H[rowData]
            G -->|Infinite/Server-Side/Viewport| I[dataSource/serverDataSource/viewportDataSource]
            H --> D1
            I --> D2
            I --> D3
            I --> D4
            J[Server] -->|HTTP| I
        end
        
        subgraph State Management
            K[Filter Model]
            L[Sort Model]
            M[Pagination State]
            N[Row Selection]
            B2 --> K
            B1 --> L
            B3 --> M
            B4 --> N
        end
        
        D1 -->|On| F2
        D2 -->|Off| F2
        D3 -->|Partial| F2
    end
    
    style A fill:#4CAF50,stroke:#333
    style D fill:#2196F3,stroke:#333
    style B fill:#FF9800,stroke:#333
    style E fill:#9C27B0,stroke:#333
    style F fill:#E91E63,stroke:#333
    style G fill:#00BCD4,stroke:#333
    style K fill:#8BC34A,stroke:#333
    style J fill:#607D8B,stroke:#333

二、Client-Side Model

ag-grid的设计概念是如果使用了clientSide,那么你应该一次性将所有数据传给ag-gird,因为后续的排序、过滤等功能都是在前端完成。如果数据实在太多,可以在前端做假分页。

技术特性

  • 全量数据驻留内存:浏览器需加载完整数据集
  • 前端计算开销:排序/过滤/分组均由JavaScript处理
  • 内存限制:Chrome单个页面内存限制约1.4GB(10万行数据约占用200MB)

基础配置模板

// 没有特别指明rowModelType,则默认值就是clientSide
const gridOptions = {
    rowData: [...],
    columnDefs: [],
}

分页

const gridOptions = {
    rowData: [...],
    columnDefs: [],
    pagination: true,  // 开启分页
    paginationAutoPageSize: true, // 根据ag-grid表格高度计算每一页可显示的行数pageSize
    paginationPageSizeSelector: [100, 200, 400], // 设置可选择的分页设置,默认是[20, 50, 100]
    paginationPageSize: 200, // 设置当前选择的分页设置,默认是20
    paginationNumberFormatter(params) { // 设置分页面板格式化
        return "[" + params.value.toLocaleString() + "]";
    } 
}

自定义分页面板

    const [current, setCurrent] = useState(0);
    const gridRef = useRef<AgGridReact<IOlympicData>>(null);
    const gridOptions = {
        ...
        pagination: true,
        suppressPaginationPanel: true, // 禁用分页面板
    }
    
    const onPaginationChanged = useCallback(() => {
        const current = gridRef.current!.api.paginationGetCurrentPage() + 1;
        setCurrent(current);
    }, []);
    
    const onChange = (pageNumber) => {
        gridRef.current!.api.paginationGoToPage(pageNumber);
    }
    
    return (
        <div>
            <AgGridReact<IData> 
                ref={gridRef} 
                rowData={rowData} 
                columnDefs={columnDefs} 
                defaultColDef={defaultColDef} 
                owSelection={rowSelection} 
                paginationPageSize={500} 
                paginationPageSizeSelector={paginationPageSizeSelector} 
                pagination={true} 
                suppressPaginationPanel={true}  // 禁用分页面板
                suppressScrollOnNewData={true} // 在页面更改时不要滚动到顶部,保持在之前滚动的位置
                onGridReady={onGridReady} 
                onPaginationChanged={onPaginationChanged} 
            />
            <Pagination current={current} total={total} onChange={onChange}  />
        </div>
    )

二、Infinite Model

infiniteclientSide一样,也是社区版提供的功能。

当我们看到infinite这个名字的时候,往往会产生误解,误以为只能进行上拉加载更多的操作。infinite英文翻译为无限。官方解释是:无限滚动允许网格根据网格的滚动位置从服务器懒加载行。在最简单形式中,用户滚动越多,加载的行就越多 但是它其实不仅仅如此,同样可以通过pagination配置,做到常规的分页功能。

那么如果我们使用clientSide模式,且也配置了pagination。和使用了infinite,且配置了pagination的方式有什么区别呢?

首先,clientSide并不在乎数据的来源在哪,只要rowData提供了数据就行,后续所有的排序、过滤等操作都只在前端完成。

infinite模式不同,你可以把它看成是serverSide的简化版,它也是需要后端接口支持的,而且默认情况下,排序、过滤都是会重新查询接口的。

有一种情况需要特殊讨论一下,如果隐藏了一列,是否需要重新请求数据?假设这一列没有进行过过滤或排序的操作,则不需要重新请求数据,如果进行了过滤或排序的操作,就需要重新请求数据了。

核心限制

  • 仅支持简单分页加载
  • 服务端过滤/排序需手动实现
  • 不支持分组聚合

基础配置模板

const gridOptions = {
    rowModelType: 'infinite',
    columnDefs: [],
    dataSource: {
        rowCount: undefined,
        getRows(params) {
                fetch('api/list', { 
                    method: 'post', 
                    body: JSON.stringify(params.request), 
                    headers: { 'Content-Type': 'application/json; charset=utf-8' } 
                })
                .then(httpResponse => httpResponse.json()) 
                .then(response => { 
                    params.successCallback(response.rows, response.lastRow); 
                 }) 
                 .catch(error => { 
                     console.error(error); 
                     params.failCallback(); 
                 })
        }
    }
}

可以看到,clientSide是通过rowData属性为表格提供数组数据的,而infinite,包括后面的serverSideviewport都是类似于dataSource的方式提供的获取数据的函数。

分页

const gridOptions = {
    ...
    pagination: true,             // 开启分页
    paginationAutoPageSize: true, // 根据ag-grid表格高度计算每一页可显示的行数pageSize
}
    

三、Server-Side Model

serverSideinfinite类似,但是功能比infinite更多。还支持分组和树形数据的动态加载。从使用效果看,ag-grid提供serverSide,是为了封装一系列的接口请求,让用户使用起来更简单。它定义了数据的查询、过滤、排序等接口参数的定义。使用的时候,只需要按照它规定的参数传递给后端,就啥也不用管了。

基础配置模板

const gridOptions = {
    rowModelType: 'serverSide',
    columnDefs: [
        { field: 'id', filter: 'agNumberColumnFilter' },
        { field: 'timestamp', filter: 'agDateColumnFilter' },
        { field: 'value', aggFunc: 'avg' }
    ],
    serverDataSource: {
        rowCount: undefined,
        getRows(params) {
                fetch('api/list', { 
                    method: 'post', 
                    body: JSON.stringify(params.request), 
                    headers: { 'Content-Type': 'application/json; charset=utf-8' } 
                })
                .then(httpResponse => httpResponse.json()) 
                .then(response => { 
                    params.successCallback(response.rows, response.lastRow); 
                 }) 
                 .catch(error => { 
                     console.error(error); 
                     params.failCallback(); 
                 })
        }
    }
};

四、Viewport Model

如果你不知道你要不要使用Viewport Model,那么你就不应该使用Viewport Model。在这个模式下,ag-grid只渲染当前可见的行。它适用于需要展示大量的实时数据。

基础配置模板

function createViewportDataSource(server) {
    class ViewportDataSource {
        ...实现一个可以实时刷新数据的class
    }
}

const gridOptions = {
    rowModelType: 'serverSide',
    columnDefs: [
        { field: 'id', filter: 'agNumberColumnFilter' },
        { field: 'timestamp', filter: 'agDateColumnFilter' },
        { field: 'value', aggFunc: 'avg' }
    ],
    viewportDatasource: createViewportDataSource(server),
};

五、决策流程图与选型建议

1. 模型选型决策树

graph TD
    A[总数据量] --> B{小于10万行?}
    B -->|是| C[Client-Side]
    B -->|否| D{需要以下功能?<br/>- 服务端计算<br/>- 动态分组<br/>- 实时聚合}
    D -->|是| E[Server-Side]
    D -->|否| F{滚动加载?}
    F -->|是| G[Infinite]
    F -->|否| H{高频更新?}
    H -->|是| I[Viewport]

实际场景实际分析

在实际场景中,我们公司还是使用了clientSide + 自定义实现分页。一个是因为serverSide需要后端接口支持,不能为了前端组件的需求而要求后端所有的接口改一遍。

另外也是因为我们的场景是后台管理系统。页面的结构一般是上部分是查询表单,下部分是表格。查询表格已经包含了过滤的需求了,而serverSide模式下,不支持表格的Quick Filter,且会与上部分的查询表格需求重复了。

链接

官网模型对比文档

官方对比图

image.png