前言
先不说别的,上两个 arco design table 的 bug。原本是写 react table 组件,而后看源码学习思路,后果看的我真的很想吐槽。(其余组件我在学习源码上受益匪浅,尤其是工程化 arco-cli 那局部,我本人尝试写的轮子也是受到很多启发,这个吐槽并不是真的有歹意,我对 arco 和腾讯的 tdeisgn 是有期待的,因为 ant 一家独大太久了,很期待陈腐的血液)
如果 arco deisgn 的团队看到这篇文章,请肯定让写 table 的同学看一下!!!把多级表头的筛选 + 排序 + 固定逻辑好好梳理一下,目前的写法隐患太多了,我前面会写为什么目前的写法隐患很多,非常容易出 bug!
1、这是在线 bug demo codesandbox.io/s/jovial-ka…
bug 显示
2、持续看, 我筛选 userInfo 上,工资大于 2000 的行,基本没成果
在线 bug 的 demo codesandbox.io/s/competent…
、
说实话,我轻易送给大家几个 table 的 bug,都能够去给官网提 pr 了。(这个写 table 的人肯定要好好的批评一下!!!!)
离谱的 filter 代码
filter 是啥呢,咱们看下图
这个表头的筛选咱们简称为 filter
首先官网把 columns 上所有的受控和非受控的 filter 收集起来,代码如下:
const {currentFilters, currentSorter} = getDefaultFiltersAndSorter(columns);
复制代码
columns 咱们假如长成这样:
const columns = [
{
title: "Name",
dataIndex: "name",
width: 140,
},
{
title: "User Info",
filters: [
{
text: "> 20000",
value: "20000",
},
{
text: "> 30000",
value: "30000",
},
],
onFilter: (value, row) => row.salary > value,
},
{
title: "Information",
children: [
{
title: "Email",
dataIndex: "email",
},
{
title: "Phone",
dataIndex: "phone",
},
],
},
]
复制代码
getDefaultFiltersAndSorter 的代码如下, 不想看细节的,我就说下结论,这个函数是把 filters 受控属性,filteredValue 和非受控属性 defaultFilters 放到 currentFilters 对象里,而后导出,其中 key 能够简略认为是每个 columns 上的 dataIndex,也就是每一列的惟一标识符。
currentSorter 咱们临时不看,也是为排序的 bug 埋下隐患,咱们这篇文章先不谈排序的 bug。
function getDefaultFiltersAndSorter(columns) {
const currentFilters = {} as Partial<Record<keyof T, string[]>>;
const currentSorter = {} as SorterResult;
function travel(columns) {if (columns && columns.length > 0) {columns.forEach((column, index) => {
const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex;
if (!column[childrenColumnName]) {if (column.defaultFilters) {currentFilters[innerDataIndex] = column.defaultFilters;
}
if (column.filteredValue) {currentFilters[innerDataIndex] = column.filteredValue;
}
if (column.defaultSortOrder) {
currentSorter.field = innerDataIndex;
currentSorter.direction = column.defaultSortOrder;
}
if (column.sortOrder) {
currentSorter.field = innerDataIndex;
currentSorter.direction = column.sortOrder;
}
} else {travel(column[childrenColumnName]);
}
});
}
}
travel(columns);
return {currentFilters, currentSorter};
}
复制代码
这里的曾经为出 bug 埋下隐患了,大家看啊,它是递归收集所有 columns 上的 filter 相干的受控和非受控的属性,而且受控的属性会笼罩非受控。
这里没有独自辨别受控的 filter 属性和非受控的属性就很奇怪。前面剖析,因为 arco deisgn 有个专门解决受控和非受控的 hooks,因为他当初不辨别,还用错这个 hooks,造成我看起来它的代码奇怪的要命!!
接着看!
而后,他用下面的 currentFilters 去
const [filters, setFilters] = useState<FilterType<T>>(currentFilters);
复制代码
接着看一下 useColunms,这个跟 filters 前面非亲非故,所以咱们必须要看下 useColumns 的实现
const [groupColumns, flattenColumns] = useColumns<T>(props);
复制代码
简略形容一下 useColumns 的返回值 groupColumns, flattenColumns 别离代表什么:
groupColumns,它将 columns 按行存储到数组外面,啥是按行呢,看下图
name、user info、Information、salary 是第一行
Birthday、address 是第二行,Email,phone 也是第二行
city、road、no 是第三行
flattenColumns 是啥意思呢?就是 columns 叶子节点组成的数组,叶子节点是指所有 columns 中没有 children 属性的节点。以下是具体代码,有趣味的能够看看,咱们接着看,马上很奇怪的代码就要来了!
function useColumns<T>(props: TableProps<T>): [InternalColumnProps[][], InternalColumnProps[]] {
const {
components, // 笼罩原生表格标签
rowSelection, // 设置表格行是否可选,选中事件等
expandedRowRender, // 点击开展额定的行,渲染函数。返回值为 null 时,不会渲染开展按钮
expandProps = {}, // 开展参数
columns = [], // 外界传入的 columns
childrenColumnName, // 默认是 children
} = props;
// 上面有 getFlattenColumns 办法
// getFlattenColumns 平铺 columns,因为可能有多级表头,所以须要平铺
// getFlattenColumns,留神这个平铺只会收集叶子节点!!!!
const rows: InternalColumnProps[] = useMemo(
() => getFlattenColumns(columns, childrenColumnName),
[columns, childrenColumnName]
);
// 是否是 checkbox
const isCheckbox =
(rowSelection && rowSelection.type === 'checkbox') ||
(rowSelection && !('type' in rowSelection));
// 是否是 radio
const isRadio = rowSelection && rowSelection.type === ‘radio’;
// 开展按钮列的宽度
const {width: expandColWidth} = expandProps;
// 是否有 expand—row
const shouldRenderExpandCol = !!expandedRowRender;
const shouldRenderSelectionCol = isCheckbox || isRadio;
// 获取到自定义的操作栏,默认是 selectNode 和 expandNode
const {getHeaderComponentOperations, getBodyComponentOperations} = useComponent(components);
const headerOperations = useMemo(
() =>
getHeaderComponentOperations({
selectionNode: shouldRenderSelectionCol ? 'holder_node' : '',
expandNode: shouldRenderExpandCol ? 'holder_node' : '',
}),
[shouldRenderSelectionCol, shouldRenderExpandCol, getHeaderComponentOperations]
);
const bodyOperations = useMemo(
() =>
getBodyComponentOperations({
selectionNode: shouldRenderSelectionCol ? 'holder_node' : '',
expandNode: shouldRenderExpandCol ? 'holder_node' : '',
}),
[shouldRenderSelectionCol, shouldRenderExpandCol, getBodyComponentOperations]
);
// rowSelection.fixed 示意 checkbox 是否固定在右边
const selectionFixedLeft = rowSelection && rowSelection.fixed;
// 抉择列的宽度
const selectionColumnWidth = rowSelection && rowSelection.columnWidth;
const getInternalColumns = useCallback(
(rows, operations, index?: number) => {const operationFixedProps: { fixed?: 'left' | 'right'} = {};
const _rows: InternalColumnProps[] = [];
rows.forEach((r, i) => {const _r = { ...r};
if (!('key' in r)) {_r.key = _r.dataIndex || i;}
if (i === 0) {
_r.$$isFirstColumn = true;
if (_r.fixed === 'left') {operationFixedProps.fixed = _r.fixed;}
} else {_r.$$isFirstColumn = false;}
_rows.push(_r);
});
const expandColumn = shouldRenderExpandCol && {
key: INTERNAL_EXPAND_KEY,
title: INTERNAL_EXPAND_KEY,
width: expandColWidth,
$$isOperation: true,
};
const selectionColumn = shouldRenderSelectionCol && {
key: INTERNAL_SELECTION_KEY,
title: INTERNAL_SELECTION_KEY,
width: selectionColumnWidth,
$$isOperation: true,
};
if (selectionFixedLeft) {operationFixedProps.fixed = 'left';}
if (typeof index !== 'number' || index === 0) {[...operations].reverse().forEach((operation) => {if (operation.node) {if (operation.name === 'expandNode') {_rows.unshift({ ...expandColumn, ...operationFixedProps});
} else if (operation.name === 'selectionNode') {_rows.unshift({ ...selectionColumn, ...operationFixedProps});
} else {
_rows.unshift({
...operation,
...operationFixedProps,
title: operation.name,
key: operation.name,
$$isOperation: true,
width: operation.width || 40,
});
}
}
});
}
return _rows;
},
[
expandColWidth,
shouldRenderExpandCol,
shouldRenderSelectionCol,
selectionColumnWidth,
selectionFixedLeft,
]
);
const flattenColumns = useMemo(
() => getInternalColumns(rows, bodyOperations),
[rows, getInternalColumns, bodyOperations]
);
// 把表头分组的 columns 分成 n 行,并且加上 colSpan 和 rowSpan,没有表头分组的话是 1 行。
// 获取 column 的深度
const rowCount = useMemo(
() => getAllHeaderRowsCount(columns, childrenColumnName),
[columns, childrenColumnName]
);
// 分行之后的 rows
const groupColumns = useMemo(() => {
if (rowCount === 1) {return [getInternalColumns(columns, headerOperations, 0)];
}
const rows: InternalColumnProps[][] = [];
const travel = (columns, current = 0) => {rows[current] = rows[current] || [];
columns.forEach((col) => {const column: InternalColumnProps = { ...col};
if (column[childrenColumnName]) {
// 求出叶子结点的个数就是 colSpan
column.colSpan = getFlattenColumns(col[childrenColumnName], childrenColumnName).length;
column.rowSpan = 1;
rows[current].push(column);
travel(column[childrenColumnName], current + 1);
} else {
column.colSpan = 1;
// 这是
column.rowSpan = rowCount - current;
rows[current].push(column);
}
});
rows[current] = getInternalColumns(rows[current], headerOperations, current);
};
travel(columns);
return rows;
}, [columns, childrenColumnName, rowCount, getInternalColumns, headerOperations]);
return [groupColumns, flattenColumns];
}
export default useColumns;
function getFlattenColumns(columns: InternalColumnProps[], childrenColumnName: string) {
const rows: InternalColumnProps[] = [];
function travel(columns) {
if (columns && columns.length > 0) {columns.forEach((column) => {if (!column[childrenColumnName]) {rows.push({ ...column, key: column.key || column.dataIndex});
} else {travel(column[childrenColumnName]);
}
});
}
}
travel(columns);
return rows;
}