@are-visual/virtual-table v0.11.1
Virtualize
VirtualTable
@are-visual/virtual-table 是一个基于插件机制实现的虚拟表格组件,仅支持 React。
Install
npm install @are-visual/virtual-table
yarn add @are-visual/virtual-table
pnpm add @are-visual/virtual-tableUsage
import type { ColumnType } from '@are-visual/virtual-table'
import { VirtualTable } from '@are-visual/virtual-table'
interface User {
id: number
name: string
age: number
}
const dataSource: User[] = [
{ id: 1, name: 'Allen', age: 26 },
{ id: 2, name: 'Andrew', age: 43 },
{ id: 3, name: 'Max', age: 12 },
]
const columns: ColumnType<User>[] = [
{
dataIndex: 'id',
title: 'UID',
width: 100,
render(value) {
return `#${value}`
},
},
{
dataIndex: 'name',
title: 'Username',
width: 100,
},
{
dataIndex: 'age',
title: 'Age',
align: 'right',
width: 100,
},
{
key: 'actions',
title: 'Action',
width: 100,
render(value, record, index) {
return (
<button type="button">View details</button>
)
},
},
]
function App() {
return (
<VirtualTable
rowKey="id"
dataSource={dataSource}
columns={columns}
estimatedRowHeight={37}
/>
)
}Columns 定义
| Prop Name | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| key | React 需要的 key 属性,如果已经指定了唯一的 dataIndex,可忽略此属性 | Key | ||
| dataIndex | 指定 dataSource 中的 key 用于单元格内容展示 | string | ||
| className | 每列样式名称 | string | ||
| colSpan | 表头列合并,设置为 0 时,不渲染 | number | ||
| title | 表头内容 | ReactNode | ||
| align | 单元格对齐方式 | left |right |center | ||
v0.11废弃 | ||||
| width | 列宽度 | number | | v0.11废弃 string类型 | |
| fixed | 固定列 | left | right | ||
| render | 自定义单元格渲染内容 | (value, record, index) => ReactNode | ||
| onHeaderCell | 设置表头单元格属性 | (column, index) => TdHTMLAttributes | ||
| onCell | 设置单元格属性 | (column, index) => TdHTMLAttributes |
Table Props
| Prop Name | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| ref | 设置最外层 div ref | Ref\<HTMLDivElement> | ||
| tableBodyRef | 设置 body 部分 table ref | Ref\<HTMLTableElement> | ||
| className | 样式类名 | string | ||
| style | 样式 | CSSProperties | ||
| tableBodyClassName | body 样式类名 | string | ||
| tableBodyStyle | body 样式 | CSSProperties | ||
| columns | 表格列配置 | ColumnType[] | ||
| dataSource | 表格数据源 | object[] | ||
| rowKey | 表格行 key 的取值 | string | (data) => React.Key | key | >=0.5.0 |
| estimatedRowHeight | 预计每行高度 | number | 46 | |
| estimatedColumnWidth | 预计每列宽度设置后将会开启横向虚拟化 | number | ||
| overscanRows | 额外在首尾渲染数据条数 | number | 5 | |
| overscanColumns | 横向虚拟化时,在头和尾额外渲染多少列 | number | 3 | |
| stickyHeader | 表头吸顶为 true 时 top 为 0,为 number 则是偏移量 | number | boolean | ||
| defaultColumnWidth | 缺失宽度设置时的默认值(与虚拟化无关) | number | 100 | >=0.2 |
| pipeline | 插件实例 | TablePipeline | ||
| rowClassName | 表格行样式类名 | (record, index) => string | ||
| onRow | 设置行属性 | (record, index) => TdHTMLAttributes | ||
| getOffsetTop | 计算顶部偏移量 | () => number | 使用最外层 div 计算 offsetTop | |
| virtualHeader | 表头虚拟化 | boolean | true | >0.1.0 |
getOffsetTop
例如上图所示,业务开发中的常见布局形式,绿色部分即为 Table 组件之前的额外区域,若这一部分的 DOM 高度较高,滚动会导致可视区域内容计算出错,导致 Table 存在空白部分。
在虚拟列表的实现中,当滚动事件触发时,需要使用 scrollTop 与最接近滚动容器顶部的元素(锚点元素)位置进行比较,再得出最新的数据可视范围。
如上图所示,当 Table 与滚动容器的上边缘相交时,数据可视范围计算才可以开始计算。而正是因为额外区域的存在,导致 Table 与滚动容器上边缘相交前,可视数据范围的计算便已经触发了,造成 Table 中存在空白行。
所以,@are-visual/virtual-table 提供了 getOffsetTop 属性,用于得知额外区域的具体高度,这样在数据可视范围计算时才能避免这个问题。
一般来说,你不太需要关注 getOffsetTop,因为它有一个默认实现:使用 table 的 DOM 节点计算与滚动容器之间的距离作为偏移量。
getOffsetTop 总是会在滚动事件中反复调用。
关于
getOffsetTop的默认实现是否会造成额外重排/性能影响,还有待验证。若你实在担心,可以设置 getOffsetTop 以覆盖默认实现。
useTableInstance
一个提供编程式操作 VirtualTable 的 hook。
版本要求:>=v0.8.0
scrollToRow 滚动到指定行
const instance = useTableInstance()
const onScroll = () => {
// 通过索引值,滚动到第 10 行
instance.scrollToRow(10)
instance.scrollToRow(10, 'smooth') // 指定 behavior
}
<VirtualTable instance={instance} />注意:由于是虚拟列表,行高是不确定的,若
estimatedRowHeight不准确,scrollToRow 则无法准确滚动到对应的行。
scrollToColumn 滚动到指定列
const instance = useTableInstance()
const onScroll = () => {
// 通过 key,滚动到指定列
instance.scrollToColumn('columnKey')
instance.scrollToColumn('columnKey', 'smooth') // 指定 behavior
}
<VirtualTable instance={instance} />注意:由于是虚拟列表,列宽是不确定的,若
estimatedColumnWidth不准确,scrollToColumn 则无法准确滚动到对应的列。
scrollTo 手动滚动
const instance = useTableInstance()
const onScroll = () => {
// 使用像素值进行滚动,背后其实是对原生 DOM 节点的 scrollTo 进行的封装
instance.scrollTo({ top: 0, left: 0 })
}
<VirtualTable instance={instance} />getScrollValueByRowIndex
const instance = useTableInstance()
const onScroll = () => {
// 通过索引值,滚动到第 10 行所对应的数值
console.log(instance.getScrollValueByRowIndex(10))
}getScrollValueByColumnKey
const instance = useTableInstance()
const onScroll = () => {
// 通过 key,获取滚动到指定列所对应的数值
console.log(instance.getScrollValueByColumnKey('columnKey'))
}getDOM
有些时候,你不得不获取 DOM 节点来进行一些操作。
const instance = useTableInstance()
const doSomething = () => {
const { root, headerWrapper, bodyWrapper, bodyRoot, body } = instance.getDOM()
}
<VirtualTable instance={instance} />getCurrentProps
有些时候,你可能不太方便获取到传递给 VirtualTable 的 props,那么你可以通过 instance.getCurrentProps 获取。
const instance = useTableInstance()
const doSomething = () => {
console.log(instance.getCurrentProps().stickyHeader)
}
<VirtualTable stickyHeader={120} instance={instance} />getRowVirtualizeState 获取行虚拟化用到的数据
const { startIndex, endIndex, overscan, estimateSize } = instance.getRowVirtualizeState()getRowHeightMap 获取所有的行高信息
const heightMap = instance.getRowHeightMap()
heightMap.get(rowKey)getColumnByKey 通过 columnKey 获取 column 定义
const column = instance.getColumnByKey(columnKey)getColumnByIndex 通过索引获取 column 定义
const column = instance.getColumnByIndex(index)getColumnKeyByIndex 通过索引获取 columnKey
const columnKey = instance.getColumnKeyByIndex(index)getColumnWidths 获取所有的列宽
所有的列宽通过 columnKey 进行记录(无序)。
const widths = instance.getColumnWidths()
widths.get(columnKey)getColumnWidthByKey 通过 columnKey 获取列宽
const width = instance.getColumnWidthByKey(columnKey)getColumns 获取 middleware 处理过的 columns
const pipelineColumns = instance.getColumns()getDataSource 获取 middleware 处理过的 dataSource
const pipelineDataSource = instance.getDataSource()extend 自定义扩展函数
当你编写插件时想要提供一些函数用于手动调用,extend 能够帮你方便设置。
import type { TableInstance } from '@are-visual/virtual-table'
declare module '@are-visual/virtual-table' {
interface TableInstance {
foo: () => void
}
}
instance.extend({
foo() {
// do something...
},
} satisfies Partial<TableInstance>)通过以上的函数,你可以很方便的获取一些 VirtualTable 内部的数据,但是这些函数不能在 render 阶段中直接调用,否则会抛出 has not been implemented yet 错误。因为这些函数都在 VirtualTable 内部渲染阶段初始化,它们初始化完成后便能使用。建议在事件处理函数中调用。
你也许会发现有些函数能够在渲染阶段直接使用,但是并不能保证在未来的版本中均是如此。
插件
@are-visual/virtual-table 提供一个 useTablePipeline hook 用于组合各种插件,为 Table 增加各式各样的功能。
目前插件列表:
- columnResize 列宽调整
- tableEmpty 空提示
- tableExpandable 行展开
- horizontalScrollBar 水平滚动条
- tableLoading 加载状态
- tableSelection 单选/多选
- tableSummary 总结栏
import '@are-visual/virtual-table/middleware/selection/styles.scss'
import { tableSelection } from '@are-visual/virtual-table/middleware/selection'
import '@are-visual/virtual-table/middleware/loading/styles.scss'
import { tableLoading } from '@are-visual/virtual-table/middleware/loading'
import { useState, type Key } from 'react'
import { useTablePipeline, VirtualTable } from '@are-visual/virtual-table'
function App() {
const [selectedRowKeys, setSelectedRowKeys] = useState<Key[]>([])
const pipeline = useTablePipeline({
use: [
// 单选/多选插件
tableSelection({
selectedRowKeys,
onChange(selectedRowKeys, selectedRows, info) {
setSelectedRowKeys(selectedRowKeys)
},
}),
// loading 插件
tableLoading({ loading: false })
],
})
return (
<VirtualTable
pipeline={pipeline}
rowKey="id"
dataSource={dataSource}
columns={columns}
estimatedRowHeight={37}
/>
)
}插件顺序
你可以指定 priority 来编排插件的顺序,数字越大越靠后。
例如下面的 columnResize,其他插件修改 columns 后,才会轮到 columnResize 执行,这样它才能获取到最新最完整的 columns.
const pipeline = useTablePipeline({
use: [
tableLoading({ loading: true }),
{ priority: 100, hook: columnResize()},
],
})合并
你可以使用 pipeline 属性合并外层传进来的插件,当你基于 VirtualTable 封装顶层组件又希望提供插件能力时,它会很有用。
const another = useTablePipeline({
use: [
tableLoading({ loading: true }),
{ priority: 100, hook: columnResize()},
],
})
const mergedPipeline = useTablePipeline({
// 与 another 合并
pipeline: another,
use: [
// 一些其他插件
],
})自定义插件
插件本身就是一个 react hook,它接受 @are-visual/virtual-table 传递的数据,处理再返回。
遵循下面这样的公式。当多个插件一起使用时,前一个插件返回的 context 会成为下一个插件所接收到的 context,所以这就是 pipeline。
插件 context 定义
| key | 说明 | 类型 | 版本 |
|---|---|---|---|
| dataSource | 表格数据源 | object[] | |
| columns | 表格列配置 | ColumnType[] | |
| rowKey | 表格行 key 的取值 | string | |
| estimatedRowHeight | 预计每行高度 | number | |
| rootRef | 最外层 div 元素 | RefObject\<HTMLDivElement> | |
| headerWrapperRef | header 外层 div 元素 | RefObject\<HTMLDivElement> | |
| bodyWrapperRef | body 外层 div 元素 | RefObject\<HTMLDivElement> | |
| bodyRootRef | body 外层 table 节点 | RefObject\<HTMLTableElement> | |
| bodyRef | tbody 元素 | RefObject\<HTMLTableSectionElement> | |
| getScroller | 获取滚动容器 | () => ScrollElement | undefined | |
| getOffsetTop | 计算顶部偏移量 | () => number | |
| instance | 与 useTableInstance 一致 | TableInstance | >=0.8.0 |
插件返回值定义
| key | 说明 | 类型 | 版本 |
|---|---|---|---|
| dataSource | 表格数据源 | object[] | |
| columns | 表格列配置 | ColumnType[] | |
| rowKey | 表格行 key 的取值 | string | |
| visibleRowSize | 当前虚拟化下所显示的行数 | number | |
| rowClassName | 自定义表格行 class | (record, index) => string | |
| onRow | 设置行属性 | (record, index) => TdHTMLAttributes | |
| render | 自定义 Table 外层渲染 | MiddlewareRender | |
| renderRoot | 自定义 div.virtual-table 渲染 | MiddlewareRenderRoot | |
| renderContent | MiddlewareRenderContent | ||
| renderHeaderWrapper | MiddlewareRenderHeaderWrapper | ||
| renderHeaderRoot | MiddlewareRenderHeaderRoot | ||
| renderHeader | Header 自定义渲染 | MiddlewareRenderHeader | |
| renderHeaderRow | 表头行自定义渲染 | MiddlewareRenderHeaderRow | |
| renderHeaderCell | 表头单元格自定义渲染 | MiddlewareRenderHeaderCell | |
| renderBodyWrapper | MiddlewareRenderBodyWrapper | ||
| renderBodyRoot | MiddlewareRenderBodyRoot | ||
| renderBody | 自定义 tbody 渲染 | MiddlewareRenderBody | |
| renderBodyContent | 表格 body 内容自定义 | MiddlewareRenderBodyContent | >=0.4.0 |
| renderRow | 表格行自定义渲染 | MiddlewareRenderRow | |
| renderCell | 单元格自定义渲染 | MiddlewareRenderCell |
出于性能考虑,请自行 memo render 函数
Render 结构
Context
└── render(<TableRoot />)
│
└── renderRoot(div.virtual-table)
│
└── renderContent(<><TableHeader/><TableBody/></>)
│
├── renderHeaderWrapper(<TableHeader />) div.virtual-table-header
│ │
│ └── renderHeaderRoot(<table />)
│ ├── colgroup
│ │
│ └── renderHeader(<thead />)
│ └── renderHeaderRow(<tr />)
│ └── renderHeaderCell(<th />)
│
└── renderBodyWrapper(<TableBody />) div.virtual-table-body-wrapper
│
└── renderBodyRoot(table.virtual-table-body)
├── colgroup
│
└── renderBody(<tbody />)
└── renderBodyContent(Row[])
└── renderRow(<tr />)
└── renderCell(<td />)Render 类型签名
interface RenderOptions<T = any> {
column: ColumnType<T>
columnWidths: Map<Key, number>
rowIndex: number
columns: ColumnType<T>[]
rowData: T
startRowIndex: number
columnDescriptor: ColumnDescriptor<T>[]
}
type MiddlewareRender<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderRoot<T = any> = (
children: ReactNode,
options: Omit<RenderOptions<T>, keyof RenderOptions<T>>
) => ReactNode
type MiddlewareRenderContent<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderHeaderWrapper<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderHeaderRoot<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderHeader<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderHeaderRow<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor'>
) => ReactNode
type MiddlewareRenderHeaderCell<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'column' | 'columnWidths'>
) => ReactNode
type MiddlewareRenderBodyWrapper<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'startRowIndex'>
) => ReactNode
type MiddlewareRenderBodyRoot<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'startRowIndex'>
) => ReactNode
type MiddlewareRenderBody<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'startRowIndex'>
) => ReactNode
type MiddlewareRenderBodyContent<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'startRowIndex'>
) => ReactNode
type MiddlewareRenderRow<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'columns' | 'columnDescriptor' | 'rowIndex' | 'rowData'>
) => ReactNode
type MiddlewareRenderCell<T = any> = (
children: ReactNode,
options: Pick<RenderOptions<T>, 'column'>
) => ReactNode插件编写
由于插件只是一个 react hook,可以直接这样写
import type { MiddlewareContext, MiddlewareResult } from '@are-visual/virtual-table'
function useLog<T = any>(ctx: MiddlewareContext<T>): MiddlewareResult<T> {
console.log('useLog 中间件被调用')
return ctx
}
// 使用插件
const pipeline = useTablePipeline({
use: [
useLog,
],
})携带参数
// 方式 1
const withLog = (options?: { prefix: string }) => {
function useLog<T = any>(ctx: MiddlewareContext<T>): MiddlewareResult<T> {
console.log(options?.prefix, 'useLog 中间件被调用')
return ctx
}
return useLog
}
// 使用插件
const pipeline = useTablePipeline({
use: [
withLog({ prefix: '🎯' }),
],
})注意上述 withLog 的实现方式,一些与表格无关的渲染被触发时,withLog 依然会返回一个新的函数,这对于 diff 是有害的,总是会导致 Table 的额外渲染,如果你的插件不需要参数,那就没有影响,否则请使用 createMiddleware 创建插件。
使用 createMiddleware 创建插件
import { createMiddleware } from '@are-visual/virtual-table'
function useLog<T = any>(ctx: MiddlewareContext<T>, options?: { prefix: string }): MiddlewareResult<T> {
console.log(options?.prefix, 'useLog 中间件被调用')
return ctx
}
const withLog = createMiddleware(useLog)
// 使用插件
const pipeline = useTablePipeline({
use: [
withLog({ prefix: '🎯' }),
],
})createMiddleware 会缓存插件的 options 参数,并在每一次渲染阶段进行一次比较,options 不同时才会返回新的函数,这样有利于避免 Table 进行一些额外的渲染。
插件注意事项
由于插件是一个 react hook,所以也需要遵守 react hooks 规则,不能在循环、判断条件中使用。
下面这种方式便是错误的,它违反了 hooks 规则,hook 不能位于判断条件中使用。
const pipeline = useTablePipeline({
use: [
enableSelection ? tableSelection({}) : null,
loading ? tableLoading({}) : null,
],
})Context Hooks
以下所列出的 hook 均与 Table 内部的 Context 有关,无法脱离 Provider 使用。
useContainerSize
读取 Context 传递的 Table 尺寸信息。
import { useContainerSize } from '@are-visual/virtual-table'
const {
scrollContainerHeight,
scrollContainerWidth,
tableHeight,
tableWidth,
} = useContainerSize()useHorizontalScrollContext
当你自定义的插件需要同步水平滚动时,可以使用这个 hook。使用 listen 进行滚动同步。
import { useHorizontalScrollContext } from '@are-visual/virtual-table'
const { listen, notify } = useHorizontalScrollContext()
const element = useRef()
useEffect(() => {
const node = element.current
if(node == null) return
// listen 会返回一个清除函数
return listen('union-key', (scrollLeft, targetNode) => {
node.scrollLeft = scrollLeft
})
}, [listen])
<div
ref={element}
onScroll={() => {
// element 滚动时,调用 notify 函数,同步其他容器
notify('union-key', {
scrollLeft: () => element.current.scrollLeft,
node
})
}}
/>useScrollSynchronize
基于 useHorizontalScrollContext 的封装。
import { useScrollSynchronize } from '@are-visual/virtual-table'
const nodeRef = useScrollSynchronize<HTMLDivElement>('union-key')
<div ref={nodeRef}></div>useTableRowManager
此 hook 可以获取当前 Table 行的高度、更新行高。
可参考 tableExpandable 行展开的实现。
import { useTableRowManager } from '@are-visual/virtual-table'
const { getRowHeightList, updateRowHeight } = useTableRowManager()类型签名:
interface TableRowManagerContextType {
getRowHeightList: () => number[]
/**
* @param index rowIndex
* @param key 唯一的 key,用于去重
* @param height 行高
*/
updateRowHeight: (index: number, key: Key, height: number) => void
}useColumnSizes
此 hook 可以获取当前 Table 每一列的宽度、更新列宽。
类型签名:
interface TableColumnsContextType {
widthList: Map<Key, number>
}useTableSticky
此 hook 可以获取每列 fixed 的值、sticky 位置。
interface StickyContextState {
size: Map<Key, number>
fixed: { key: Key, fixed: FixedType | undefined }[]
}参考
6 months ago
7 months ago
7 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago