React 数据网格 - 完全指南





0/5 (0投票)
在本文中,我们将学习如何在 React 应用程序中集成数据网格。
大多数前端开发人员都熟悉这种情况:一个数据驱动应用程序的新项目启动了。每个人都确信设计必须尽可能简单明了。其核心是一个简单的表格 - 几列和许多行。但甚至在最小可行应用程序发布之前,就会发现简单的表格是不够的。利益相关者想要分页和过滤。设计师要求个性化和灵活性。这就是我们(开发人员)寻求现有数据网格帮助的时候。
在本文中,我们将学习如何在 React 应用程序中集成数据网格,并探讨将数据集成到网格中的一些最佳实践,从导入文件到连接 API 和数据库。
数据网格功能
数据网格与表格
在其最基本的形式下,数据网格可以看作一个表格——数据以行和列的形式表示。差异从基本功能(如滚动)开始。虽然表格除了固定的页眉(通常显示列定义)之外不会提供太多功能,但数据网格可以更复杂。排序(多列带优先级)和数据选择遵循相同的模式。后者现在是基于单元格而不是基于行。
我们还会在许多数据网格中找到的一个功能是数据导出功能。在最简单的情况下,这相当于剪贴板复制。然而,导出到 CSV 文件甚至打印报告在今天并没有太大区别。
总的来说,数据网格支持与标准电子表格应用程序(如 Excel)的互操作性,这可以提高生产力。结合实时更新和后端支持的协作技术,这使得数据网格成为真正的数据处理利器。微软在几乎所有其他在线数据编辑工具(如 Power BI)中使用 Excel 365 引擎并非巧合。
真正使数据网格与表格区分开来的功能是自定义单元格渲染和格式化功能。在这里,我们可以想到图表或其他丰富的可视化显示在特定单元格中。另一个例子是快速的视觉提示,如迷你图。
最后,但同样重要的是,对可访问性功能有很强的需求。数据网格支持单元格高亮显示、触摸支持、叠加图标和键盘导航,其功能接近甚至超过原生电子表格应用程序。
在 React 中构建自己的数据网格
React 生态系统包含数十个可用的数据网格组件。这些组件使您只需几行代码即可访问所有预打包的功能。在深入使用现有解决方案之前,让我们看看如何从头开始实现一个proper 的数据网格。
由于每个数据网格的核心都是一个表格,我们将从它开始。在 React 中设计表格有两种基本方法:
- 遵循典型的 HTML 抽象层,创建 TableContainer 等组件,并使用其子组件:TableHeader、TableFooter、TableRow 和 TableCell。
- 使用渲染属性和其他专用属性拥有一个单一的 Table 组件,用于调整目标渲染。
第一个选项是获得一个简单而风格一致的表格的绝佳方法,而第二个选项——一个带有渲染属性的 Table 组件——通过将大部分表示逻辑转移到抽象层,能够实现更多功能。因此,这是现有解决方案中通常采用的路径。
让我们看看第一种方法的一个简单实现,不包括错误处理和其他激动人心的高级功能。
import * as React from "react";
const TableContainer = ({ striped, children }) => (
<table className={striped ? "table-striped" : ""}>{children}</table>
);
const TableHeader = ({ children }) => <thead>{children}</thead>;
const TableBody = ({ children }) => <tbody>{children}</tbody>;
const TableRow = ({ children }) => <tr>{children}</tr>;
const TableCell = ({ children }) => <td>{children}</td>;
const MyTable = () => (
<TableContainer striped>
<TableHeader>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Name</TableCell>
<TableCell>Age</TableCell>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>1</TableCell>
<TableCell>Foo</TableCell>
<TableCell>21</TableCell>
</TableRow>
<TableRow>
<TableCell>2</TableCell>
<TableCell>Bar</TableCell>
<TableCell>29</TableCell>
</TableRow>
</TableBody>
</TableContainer>
);
这里的想法是,像 TableContainer 这样的单个组件可以通过其属性暴露所有不同的选项。因此,MyTable 组件可以直接使用这些属性,而不是通过晦涩的类名或奇怪的属性。
现在,遵循第二种方法,前面的例子看起来有点不同。
import * as React from "react";
const Table = ({ striped, columns, data, keyProp }) => (
<table className={striped ? "table-striped" : ""}>
<thead>
<tr>
{columns.map((column) => (
<th key={column.prop}>{column.label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row[keyProp]}>
{columns.map((column) => (
<td key={column.prop}>{row[column.prop]}</td>
))}
</tr>
))}
</tbody>
</table>
);
const MyTable = () => (
<Table
striped
keyProp="id"
columns={[
{ label: "ID", prop: "id" },
{ label: "Name", prop: "name" },
{ label: "Age", prop: "age" },
]}
data={[
{ id: 1, name: "Foo", city: "", age: 21 },
{ id: 2, name: "Bar", city: "", age: 29 },
]}
/>
);
如您所见,Table 组件中的逻辑更加抽象。渲染成本也更高。但是,这可以很好地控制和优化,例如通过使用 useMemo
等技术缓存部分内容。
这种方法最显著的优势无疑是数据驱动方面。您不必完全自己构建表格,只需插入一些数据即可获得渲染的表格。
您可以在这个版本的基础上构建一个完整的数据网格组件,利用相同的原理。然而,如今,从头开始构建自己的数据网格的理由非常少。
数据网格控件处理繁重的工作
与其重新发明轮子来以编程方式构建表格——并且仍然受限于 HTML 表格的限制——最好的选择是集成一个数据网格控件。有一些优秀的开源选择,包括:
- React Virtualized
- React Data Grid
- React Table
还有许多其他选择,每个都通常迎合其创建者特定的需求——就像开源项目通常那样。
虽然开源选项很吸引人,但像 Wijmo 这样的商业产品为 React 数据网格组件 提供了独特的优势。GrapeCity Wijmo 包含的 FlexGrid 是 React 最出色的即插即用数据网格。
一个优点是数据网格默认包含广泛的功能集。另一个是支持和持续开发的承诺。
一个基本 React 数据网格控件的实际应用
让我们从一个简单的数据网格可视化开始,它展示了一些数据,包括视觉提示。我将使用一些任意的日期和计数数据,代表我们都熟悉的类型的数据集,如下表所示:
年份 | 一月 | 二月 | 三月 | 四月 | 五月 | 六月 |
2016 | 20 | 108 | 45 | 10 | 105 | 48 |
2017 | 48 | 10 | 0 | 0 | 78 | 74 |
2018 | 12 | 102 | 10 | 0 | 0 | 100 |
2019 | 1 | 20 | 3 | 40 | 5 | 60 |
使用 React Data Grid,页面代码看起来如下:
import React from "react";
import ReactDataGrid from "react-data-grid";
import { Sparklines, SparklinesLine, SparklinesSpots } from "react-sparklines";
const Sparkline = ({ row }) => (
<Sparklines
data={[row.jan, row.feb, row.mar, row.apr, row.may, row.jun]}
margin={6}
height={40}
width={200}
>
<SparklinesLine
style={{ strokeWidth: 3, stroke: "#336aff", fill: "none" }}
/>
<SparklinesSpots
size={4}
style={{ stroke: "#336aff", strokeWidth: 3, fill: "white" }}
/>
</Sparklines>
);
const columns = [
{ key: "year", name: "Year" },
{ key: "jan", name: "January" },
{ key: "feb", name: "February" },
{ key: "mar", name: "March" },
{ key: "apr", name: "April" },
{ key: "may", name: "May" },
{ key: "jun", name: "June" },
{ name: "Info", formatter: Sparkline },
];
const rows = [
{ year: 2016, jan: 20, feb: 108, mar: 45, apr: 10, may: 105, jun: 48 },
{ year: 2017, jan: 48, feb: 10, mar: 0, apr: 0, may: 78, jun: 74 },
{ year: 2018, jan: 12, feb: 102, mar: 10, apr: 0, may: 0, jun: 100 },
{ year: 2019, jan: 1, feb: 20, mar: 3, apr: 40, may: 5, jun: 60 },
];
export default function ReactDataGridPage() {
return (
<ReactDataGrid
columns={columns}
rowGetter={(i) => rows[i]}
rowsCount={rows.length}
/>
);
}
要显示图表和其他图形,我需要依赖第三方库。在上面的例子中,我安装了 react-sparklines 来演示一个迷你图。列使用一个对象定义。对于迷你图,我回退到一个自定义格式化程序,没有后端字段。
结果如下所示:
创建高级 React 数据网格
现在,让我们使用 FlexGrid 显示相同的数据。对于大致相同的代码量,您可以获得更美观、更灵活的数据显示。页面代码现在看起来像这样:
import "@grapecity/wijmo.styles/wijmo.css";
import React from "react";
import { CollectionView } from "@grapecity/wijmo";
import { FlexGrid, FlexGridColumn } from "@grapecity/wijmo.react.grid";
import { CellMaker, SparklineMarkers } from "@grapecity/wijmo.grid.cellmaker";
import { SortDescription } from "@grapecity/wijmo";
const data = [
{ year: 2016, jan: 20, feb: 108, mar: 45, apr: 10, may: 105, jun: 48 },
{ year: 2017, jan: 48, feb: 10, mar: 0, apr: 0, may: 78, jun: 74 },
{ year: 2018, jan: 12, feb: 102, mar: 10, apr: 0, may: 0, jun: 100 },
{ year: 2019, jan: 1, feb: 20, mar: 3, apr: 40, may: 5, jun: 60 },
];
export default function WijmoPage() {
const [view] = React.useState(() => {
const view = new CollectionView(
data.map((item) => ({
...item,
info: [item.jan, item.feb, item.mar, item.apr, item.may, item.jun],
}))
);
return view;
});
const [infoCellTemplate] = React.useState(() =>
CellMaker.makeSparkline({
markers: SparklineMarkers.High | SparklineMarkers.Low,
maxPoints: 25,
label: "Info",
})
);
return (
<FlexGrid itemsSource={view}>
<FlexGridColumn header="Year" binding="year" width="*" />
<FlexGridColumn header="January" binding="jan" width="*" />
<FlexGridColumn header="February" binding="feb" width="*" />
<FlexGridColumn header="March" binding="mar" width="*" />
<FlexGridColumn header="April" binding="apr" width="*" />
<FlexGridColumn header="May" binding="may" width="*" />
<FlexGridColumn header="June" binding="jun" width="*" />
<FlexGridColumn
header="Info"
binding="info"
align="center"
width={180}
allowSorting={false}
cellTemplate={infoCellTemplate}
/>
</FlexGrid>
);
}
最值得注意的是,Wijmo 数据网格在 React 中以声明方式定义列。对于迷你图单元格,使用了 CollectionView
。使用 useState
,我可以缓存数据并在重新渲染之间保持其状态——无需昂贵的计算。
这里的默认结果具有类似于真实电子表格应用程序的外观。
由于数据网格是应用程序中最大的组件,因此对其进行懒加载是一个好习惯。如果您只在单个页面上使用数据网格,那么懒加载该特定页面就足够了,并避免了额外的复杂性。
import * as React from "react";
import { Switch, Route } from "react-router-dom";
const PageWithDatagrid = React.lazy(() => import("./pages/DatagridPage"));
export const Routes = () => (
<Switch>
{/* ... */}
<Route path="/datagrid" component={PageWithDatagrid} />
</Switch>
);
唯一的 requirement 是懒加载的模块必须有一个 proper 的默认导出。
export default function PageWithDatagrid() {
return /* ... */;
}
所有唯一的依赖项(例如,数据网格组件)都应包含在 side-bundle 中。此 side-bundle 将对启动性能产生重大影响。
加载数据的最佳实践
在这些示例中,我只是加载了一些硬编码的数据。在实际应用程序中,您很可能将动态数据从外部源获取,例如文件、数据库或 API。
虽然加载数据通常被认为主要是后端主题,但有一些前端方面的考虑需要讨论。最重要的是,拥有一个提供非绑定数据的 API 将是一个问题。一个常见的问题是,整个数据集的渲染要么非常慢,要么只分块进行,导致部分数据未被使用。
为了规避上述问题,一些 API 允许分页。最简单的形式是,您向 API 传达一个页码,API 然后计算数据集中偏移量。为了可靠的分页和最大的灵活性,分页机制实际上应该使用一个指针——一个标记最后发出数据项的标记。
要在 Wijmo 数据网格中包含分页 API,请使用 ICollectionView
实例。如果您的 API 支持 OData,那么您可以直接使用 ODataCollectionView 来完成这项任务。
例如,以下视图每页显示六项:
const view = new ODataCollectionView(url, 'Customers', {
pageSize: 6,
pageOnServer: true,
sortOnServer: true,
});
总的来说,标准的 CollectionView
也可以用于异步数据加载。
const [view, setView] = React.useState(() => new CollectionView());
React.useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(posts => setView(view => {
view.sourceCollection = data;
return view;
}));
}, []);
// render datagrid
上面的代码并不完美:异步操作应该用 disposer 正确清理。useEffect
的一个更好的版本是:
React.useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
fetch('https://jsonplaceholder.typicode.com/posts', { signal })
.then(res => res.json())
.then(/* ... */);
return () => controller.abort();
}, []);
除了直接调用 API,您可能还会关心 跨域资源共享 (CORS)。CORS 是一种浏览器中的安全机制,它会影响向非当前域执行的请求。
除了隐式的 CORS 请求和响应模式(包括所谓的预检请求)之外,一个关键方面是通过例如 cookie 来传递凭据。默认情况下,凭据仅发送到同源请求。
以下内容也将向其他服务发送凭据——如果服务正确响应了预检(OPTIONS)请求:
fetch('https://jsonplaceholder.typicode.com/posts', { credentials: 'include' })
到目前为止,数据调用是在组件挂载时完成的。这种方法并不理想。它不仅意味着总是需要等待数据,而且还使得取消和其他流程更难实现。
您想要的是某种全局数据状态,可以轻松地(并且独立于特定组件的生命周期)访问和更改。虽然像 Redux 这样的状态容器解决方案是最流行的选择,但也有更简单的替代方案。
一种可能性是使用 Zustand(德语中的“状态”)。您可以将所有与数据相关的活动建模为对全局定义的状态对象的操纵。对该对象的更改将通过 React hooks 报告。
// state.js
import create from 'zustand';
const [useStore] = create(set => ({
data: undefined,
load: () =>
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(posts => set({ data: posts })),
}));
export { useStore };
// datagrid.js
import { useStore } from './state';
// ...
export default function MyDataGridPage() {
const data = useStore(state => state.data);
const load = useStore(state => state.load);
const view = new CollectionView(data);
React.useEffect(() => {
if (!data) {
load();
}
}, [data]);
return (
<FlexGrid itemsSource={view} />
);
}
这里有一些关于 React 数据网格的额外信息:
后续步骤
数据网格已成为显示、组织甚至编辑各种应用程序中数据的非常受欢迎且灵活的工具。使用 FlexGrid,您可以在不到五分钟的时间内快速轻松地构建 Angular、React 和 Vue 数据网格。