React/HTML中如何更优地以表格形式展示嵌套JSON对象?
嵌套JSON转相对尺寸表格的优化实现方案
问题背景
给定嵌套JSON对象:
{ "A":{ "a": [1,2], "b": [3] }, "B":{ "c":[4] } }
需要将其转换为相对尺寸的规整表格(一级键跨行对应下属子项,二级键跨行对应数组元素),但现有实现存在痛点:
- 用div布局:无绝对尺寸时,单元格行列对齐难以保证
- 原生table实现:需手动解构对象+编写复杂的rowspan计算逻辑,代码可读性极差
优化方案
方案1:用React声明式库简化跨行处理
推荐使用@tanstack/react-table(v8+),它支持通过配置实现跨行合并,无需手动计算rowspan,代码更具声明式特点,同时天然支持相对尺寸。
实现步骤:
- 安装依赖:
npm install @tanstack/react-table
- 组件代码:
import { useMemo } from 'react'; import { useTable, useRowSpan } from '@tanstack/react-table'; export default function NestedJsonTable() { // 扁平化嵌套JSON,标记层级 const data = useMemo(() => { const flattened = []; const source = { "A":{ "a": [1,2], "b": [3] }, "B":{ "c":[4] } }; Object.entries(source).forEach(([parentKey, children]) => { Object.entries(children).forEach(([childKey, values]) => { values.forEach((val, idx) => { flattened.push({ parent: idx === 0 ? parentKey : '', child: idx === 0 ? childKey : '', value: val }); }); }); }); return flattened; }, []); // 配置列规则,包含跨行合并逻辑 const columns = useMemo(() => [ { accessorKey: 'parent', header: '一级键', rowSpan: (row) => { const parentVal = row.original.parent; if (!parentVal) return 0; return data.filter(item => item.parent === parentVal).length; } }, { accessorKey: 'child', header: '二级键', rowSpan: (row) => { const childVal = row.original.child; if (!childVal) return 0; return data.filter(item => item.child === childVal).length; } }, { accessorKey: 'value', header: '值' } ], [data]); const table = useTable({ columns, data }, useRowSpan); const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = table; return ( <table {...getTableProps()} style={{ width: '100%', borderCollapse: 'collapse' }}> <thead> {headerGroups.map(headerGroup => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map(column => ( <th {...column.getHeaderProps()} style={{ border: '1px solid #ccc', padding: '8px' }}> {column.render('Header')} </th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map(row => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.getVisibleCells().map(cell => { const props = cell.getCellProps(); const span = cell.column.rowSpan?.(row, cell.column); if (span === 0) return null; if (span) props.rowSpan = span; return ( <td {...props} style={{ border: '1px solid #ccc', padding: '8px' }}> {cell.render('Cell')} </td> ); })} </tr> ); })} </tbody> </table> ); }
方案2:CSS Grid实现无table对齐布局
如果不想依赖第三方库,CSS Grid可以实现天然的行列对齐,同时支持相对尺寸,代码逻辑更直观。
组件代码:
import { useMemo } from 'react'; export default function NestedJsonGrid() { const gridData = useMemo(() => { const result = []; let rowIndex = 0; const source = { "A":{ "a": [1,2], "b": [3] }, "B":{ "c":[4] } }; // 预处理跨行数 const parentSpanMap = {}; const childSpanMap = {}; Object.entries(source).forEach(([parent, children]) => { let total = 0; Object.entries(children).forEach(([child, values]) => { total += values.length; childSpanMap[child] = values.length; }); parentSpanMap[parent] = total; }); // 生成网格数据 Object.entries(source).forEach(([parent, children]) => { Object.entries(children).forEach(([child, values]) => { values.forEach(val => { result.push({ parent, parentSpan: parentSpanMap[parent], child, childSpan: childSpanMap[child], value: val, row: rowIndex++ }); }); }); }); // 标记是否需要渲染单元格(避免重复) let prevParent = ''; let prevChild = ''; result.forEach(item => { item.showParent = item.parent !== prevParent; item.showChild = item.child !== prevChild; prevParent = item.parent; prevChild = item.child; }); return result; }, []); return ( <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', // 三列等宽,自适应容器 width: '100%', border: '1px solid #ccc' }}> {/* 表头 */} {['一级键', '二级键', '值'].map(title => ( <div key={title} style={{ border: '1px solid #ccc', padding: '8px', fontWeight: 'bold', textAlign: 'center' }}> {title} </div> ))} {/* 内容单元格 */} {gridData.map((item, idx) => ( <> {item.showParent && ( <div style={{ border: '1px solid #ccc', padding: '8px', gridRow: `${item.row + 2} / span ${item.parentSpan}` // 表头占1行,从第row+2行开始跨行 }}> {item.parent} </div> )} {item.showChild && ( <div style={{ border: '1px solid #ccc', padding: '8px', gridRow: `${item.row + 2} / span ${item.childSpan}` }}> {item.child} </div> )} <div style={{ border: '1px solid #ccc', padding: '8px' }}> {item.value} </div> </> ))} </div> ); }
方案3:原生table的简化计算方式
如果坚持使用原生table,可以通过预处理跨行映射表,拆分计算逻辑,让代码更易读:
组件代码:
import { useMemo } from 'react'; export default function NestedJsonNativeTable() { const tableRows = useMemo(() => { const rows = []; const source = { "A":{ "a": [1,2], "b": [3] }, "B":{ "c":[4] } }; // 第一步:计算所有层级的跨行数 const parentSpanMap = {}; const childSpanMap = {}; Object.entries(source).forEach(([parent, children]) => { let totalRows = 0; Object.entries(children).forEach(([child, values]) => { totalRows += values.length; childSpanMap[child] = values.length; }); parentSpanMap[parent] = totalRows; }); // 第二步:生成表格行,标记是否显示单元格 let currentParent = ''; let currentChild = ''; Object.entries(source).forEach(([parent, children]) => { Object.entries(children).forEach(([child, values]) => { values.forEach(val => { rows.push({ showParent: parent !== currentParent, parent, parentSpan: parentSpanMap[parent], showChild: child !== currentChild, child, childSpan: childSpanMap[child], value: val }); currentParent = parent; currentChild = child; }); }); }); return rows; }, []); return ( <table style={{ width: '100%', borderCollapse: 'collapse' }}> <thead> <tr> <th style={{ border: '1px solid #ccc', padding: '8px' }}>一级键</th> <th style={{ border: '1px solid #ccc', padding: '8px' }}>二级键</th> <th style={{ border: '1px solid #ccc', padding: '8px' }}>值</th> </tr> </thead> <tbody> {tableRows.map(row => ( <tr key={row.value}> {row.showParent && ( <td style={{ border: '1px solid #ccc', padding: '8px' }} rowSpan={row.parentSpan}> {row.parent} </td> )} {row.showChild && ( <td style={{ border: '1px solid #ccc', padding: '8px' }} rowSpan={row.childSpan}> {row.child} </td> )} <td style={{ border: '1px solid #ccc', padding: '8px' }}>{row.value}</td> </tr> ))} </tbody> </table> ); }
内容的提问来源于stack exchange,提问作者JH Lee
相关产品推荐
相关产品推荐

