前言
在移動端開發中,表格組件是一個常見但復雜的需求。相比PC端,移動端表格面臨著屏幕空間有限、交互方式不同、性能要求更高等挑戰。本文將詳細介紹如何從零開始構建一個功能完整的移動端React表格組件,包含固定列、智能單元格合并、排序等高級功能。
項目背景
在實際項目中,我們經常遇到以下痛點:
- 現有表格組件在移動端體驗不佳
- 復雜的單元格合并需求難以實現
- 固定列在不同屏幕尺寸下對齊問題
- 大數據量下的性能優化
基于這些需求,我們開發了 @wtechtec/mobile-table
組件庫。
技術棧選擇
- React 18 - 主框架
- TypeScript - 類型安全
- NutUI React - 基礎UI組件庫
- Rollup - 構建工具
- PostCSS - 樣式處理
核心功能設計
1. 基礎表格結構
首先定義表格的基礎類型:
export interface BasicTableProps extends BasicComponent {columns: Array<TableColumnProps>data: Array<any>bordered: booleansummary?: React.ReactNodestriped?: booleannoData?: React.ReactNodesorterIcon?: React.ReactNodeonSort?: (column: TableColumnProps, sortedData: Array<any>) => voidshowHeader?: boolean
}export interface TableColumnProps {key: stringtitle?: stringalign?: stringsorter?: ((a: any, b: any) => number) | boolean | stringrender?: (rowData: any, rowIndex: number) => string | React.ReactNodefixed?: 'left' | 'right'width?: numberonCell?: (rowData: any, rowIndex: number) => CellConfig
}
2. 固定列實現原理
固定列是移動端表格的核心功能,實現思路如下:
// 計算固定列寬度
const useTableSticky = (columns: TableColumnProps[], rtl: boolean) => {const [stickyLeftWidth, setStickyLeftWidth] = useState(0)const [stickyRightWidth, setStickyRightWidth] = useState(0)useEffect(() => {// 計算左固定列總寬度let leftWidth = 0let rightWidth = 0columns.forEach(col => {if (col.fixed === 'left' && col.width) {leftWidth += col.width}if (col.fixed === 'right' && col.width) {rightWidth += col.width}})setStickyLeftWidth(leftWidth)setStickyRightWidth(rightWidth)}, [columns])return { stickyLeftWidth, stickyRightWidth }
}
關鍵點:以實際渲染寬度為準
在實際開發中,我們發現設置的 width
和渲染出來的寬度可能不一致,因此采用動態獲取DOM寬度的方案:
useEffect(() => {// 獲取所有 fixed: 'left' 列的實際寬度let width = 0columns.forEach(col => {if (col.fixed === 'left' && thRefs.current[col.key]) {width += thRefs.current[col.key]!.offsetWidth}})setStickyLeftWidth(width)
}, [columns, data])
3. 智能單元格合并算法
這是本組件的亮點功能,能夠自動識別相同值并進行最優的矩形區域合并:
// 創建多行多列合并配置
export const createMultiRowColumnMergeCellConfig = (data: any[], columns: string[]) => {const mergeCellMap = new Map<string, { rowSpan: number; colSpan: number; isMainCell: boolean;value: any;mergeType: 'row' | 'column' | 'block';}>()// 創建值到位置的映射const valueToPositions = new Map<any, Array<{row: number, col: number, colKey: string}>>()// 收集所有相同值的位置data.forEach((item, rowIndex) => {columns.forEach((colKey, colIndex) => {const value = item[colKey]if (value !== null && value !== undefined && value !== '') {if (!valueToPositions.has(value)) {valueToPositions.set(value, [])}valueToPositions.get(value)!.push({row: rowIndex,col: colIndex,colKey})}})})// 處理每個相同值的合并valueToPositions.forEach((positions, value) => {if (positions.length <= 1) returnconst mergeAreas = findMaxRectangleAreas(positions)mergeAreas.forEach(area => {if (area.positions.length > 1) {createMergeArea(area, value, mergeCellMap)}})})return mergeCellMap
}
矩形區域識別算法:
// 查找最大矩形合并區域
const findMaxRectangleAreas = (positions: Array<{row: number, col: number, colKey: string}>) => {const areas = []const usedPositions = new Set<string>()const sortedPositions = [...positions].sort((a, b) => {if (a.row !== b.row) return a.row - b.rowreturn a.col - b.col})for (const startPos of sortedPositions) {const startKey = `${startPos.row}-${startPos.col}`if (usedPositions.has(startKey)) continue// 嘗試找到以當前位置為起點的最大矩形const maxRect = findLargestRectangleFromPosition(startPos, positions, usedPositions)if (maxRect.positions.length > 1) {areas.push(maxRect)maxRect.positions.forEach(pos => {usedPositions.add(`${pos.row}-${pos.col}`)})}}return areas
}
4. 排序功能實現
const handleSorterClick = (item: TableColumnProps) => {if (item.sorter && !sortedMapping.current[item.key]) {const copied = [...innerValue]if (typeof item.sorter === 'function') {copied.sort(item.sorter as (a: any, b: any) => number)} else if (item.sorter === 'default') {copied.sort()}sortedMapping.current[item.key] = truesetValue(copied, true)onSort && onSort(item, copied)} else {sortedMapping.current[item.key] = falsesetValue(data)}
}
樣式設計與優化
1. 移動端適配
.nut-table {overflow: hidden;position: relative;word-wrap: break-word;word-break: break-all;
}.nut-table-wrapper {display: flex;width: 100%;flex-direction: column;font-size: 14px;color: #1a1a1a;overflow-y: auto;overflow-x: hidden;position: relative;border: 1px solid #f0f0f0;
}.nut-table-wrapper-sticky {overflow-x: auto;
}
2. 固定列樣式
.nut-table-fixed-left,
.nut-table-fixed-right {position: sticky;z-index: 2;
}.nut-table-sticky-left {left: 1px;box-shadow: 6px 0 6px -4px rgba(0, 0, 0, 0.15);
}.nut-table-sticky-right {right: 1px;box-shadow: -6px 0 6px -4px rgba(0, 0, 0, 0.15);
}
構建配置優化
Rollup 配置
export default {input: 'src/index.ts',output: [{file: pkg.main,format: 'cjs',sourcemap: true,exports: 'named'},{file: pkg.module,format: 'esm',sourcemap: true,exports: 'named'},{file: pkg.unpkg,format: 'umd',name: 'MobileTable'}],external: ['react', 'react-dom', '@nutui/nutui-react'],plugins: [resolve({extensions: ['.ts', '.tsx', '.js', '.jsx'],preferBuiltins: false,dedupe: ['react', 'react-dom']}),postcss({inject: true,extract: false,modules: false // 關鍵:禁用CSS模塊化}),typescript({tsconfig: './tsconfig.json',declaration: true,declarationDir: 'dist',rootDir: 'src'})]
}
使用示例
基礎用法
import { Table } from '@wtechtec/mobile-table'const columns = [{ key: 'name', title: '姓名', width: 100, fixed: 'left' },{ key: 'age', title: '年齡', width: 80 },{ key: 'address', title: '地址', width: 200 }
]const data = [{ name: '張三', age: 25, address: '北京市朝陽區' },{ name: '李四', age: 30, address: '上海市浦東新區' }
]<Table columns={columns} data={data} />
智能合并用法
import { Table, createMultiMergeOnCellFunction, createMultiRowColumnMergeCellConfig
} from '@wtechtec/mobile-table'const mergeColumns = ['gender', 'age', 'class']
const multiMergeCellMap = createMultiRowColumnMergeCellConfig(data, mergeColumns)const columns = [{key: 'gender',title: '性別',onCell: createMultiMergeOnCellFunction(multiMergeCellMap, 'gender')}// ...
]
性能優化策略
1. 虛擬滾動(大數據量)
const VirtualTable = ({ data, height = 400 }) => {const [startIndex, setStartIndex] = useState(0)const [endIndex, setEndIndex] = useState(20)const visibleData = useMemo(() => {return data.slice(startIndex, endIndex)}, [data, startIndex, endIndex])return <Table data={visibleData} />
}
2. 合并計算緩存
const useMergeCellMap = (data: any[], columns: string[]) => {return useMemo(() => {return createMultiRowColumnMergeCellConfig(data, columns)}, [data, columns])
}
遇到的技術難點與解決方案
1. CSS樣式無效問題
問題:npm包引用后樣式無效
原因:Rollup配置中開啟了CSS模塊化,導致類名被哈希化
解決方案:
postcss({inject: true,extract: false,modules: false // 禁用CSS模塊化
})
2. 固定列對齊問題
問題:設置的width與實際渲染寬度不一致
解決方案:以實際DOM寬度為準,動態計算sticky區域寬度
3. 單元格合并復雜度
問題:如何實現智能的多行多列合并
解決方案:設計矩形區域識別算法,自動找到最優合并方案
測試與發布
單元測試
describe('Table Component', () => {test('renders basic table', () => {render(<Table columns={columns} data={data} />)expect(screen.getByText('姓名')).toBeInTheDocument()})test('merge cells correctly', () => {const mergeCellMap = createMultiRowColumnMergeCellConfig(data, ['gender'])expect(mergeCellMap.size).toBeGreaterThan(0)})
})
發布流程
# 構建
pnpm run build# 發布到npm
pnpm publish --access public
總結與展望
通過本次開發,我們成功構建了一個功能完整的移動端表格組件,主要收獲:
- 架構設計:合理的類型定義和組件拆分
- 算法優化:智能合并算法的設計與實現
- 性能優化:虛擬滾動、計算緩存等策略
- 工程化:完整的構建、測試、發布流程
未來規劃
- 支持表格編輯功能
- 增加更多主題樣式
- 優化大數據量性能
- 支持表格導出功能
參考資料
- React官方文檔
- NutUI React
- Rollup官方文檔
項目地址:GitHub - @wtechtec/mobile-table
NPM包:@wtechtec/mobile-table