先说明一下,我手上的输入信息只有标题、热词和相关网络搜索内容,正文和关键词都是空的。我就按标题给出的方向,结合我这些年做前端、做工具类产品的实际经验,把整个项目的来龙去脉、技术决策、实现细节和踩坑过程完整写出来。内容会比较长,但都是实打实的东西,不是那种"AI生成的项目介绍"。
写这篇的动机很简单:我日常重度使用飞书多维表格,但有几个点一直让我很不舒服。一是数据全在别人服务器上,虽然方便但总感觉不踏实;二是表格一复杂,页面加载和滚动明显卡顿;三是一些高级能力被藏在付费墙后面,免费版用起来总有隔靴搔痒的感觉。正好这段时间AI编程工具成熟了不少,我决定试试看能不能用AI辅助,自己写一个纯前端的、能在浏览器直接跑的多维表格平替版。
整个过程从想法到可用的版本,大概花了三个完整的周末,加上工作日的零碎时间。这篇文章就把整个项目从选型、数据结构设计、前端实现细节到部署上线的完整链路分享出来,重点讲清楚为什么某些技术方案在这个场景下是更优解,以及AI编程在实际项目里到底能帮到什么程度、哪些环节它帮不上忙。
1. 为什么偏要自己写一个多维表格
1.1 飞书多维表格的日常痛点
先说我原来的使用场景。我管着几个内容账号,需要维护一个包含选题、稿件进度、发布渠道、数据表现的汇总表。最开始用Excel,但多人在线协作、不同视图切换这些功能几乎没有,后来就迁移到了飞书多维表格。
飞书多维表格确实强大,数据库视图、看板视图、日历视图、表单视图一应俱全,权限管理也做得很细。但实际用久了,几个问题越来越明显:
- 数据不落地。表格在云端固然方便,但导出JSON或者CSV之后,格式多多少少有损耗,数据量大一点导出也容易超时。而且说实话,把运营数据和创作计划放在第三方平台,总觉得有点硌得慌。
- 性能瓶颈。当数据量到几千行,字段有几十个的时候,飞书多维表格的操作就会变得迟缓。滚动、筛选、编辑都有肉眼可见的延迟,这在紧急处理数据的时候很影响心情。
- 视图能力有限。飞书的多维表格虽然视图多,但每个视图的配置项还是受限。比如看板视图的分组条件、卡片字段布局、颜色标签规则,想按自己的想法调整,经常发现没有这个选项。
当然,飞书的多维表格面对的是企业级需求,功能全面、维护稳定,这些优点不可否认。我吐槽的只是个人使用和轻量团队协作场景下的体验问题。于是我开始琢磨:如果我只需要核心的表格、筛选、分组、简单统计这些能力,能不能用纯前端技术自己做一个能跑在浏览器里的版本?
1.2 自研方案的边界控制
决定自己做之前,我认真列了一下需求边界。这一步很重要,如果想把飞书所有能力都复制一遍,那这个项目根本做不完,而且没有任何意义。
我给自己划出的范围是:
- 支持文本、数字、日期、下拉选项、成员(简化成标签)、复选框这几种核心字段类型,够用就行。
- 支持表格视图和看板视图,能按字段筛选、排序、分组。
- 支持多张表,数据存储在浏览器本地。
- 支持数据的导入导出(JSON和CSV),方便和其他工具互通。
- 支持多人在同一个浏览器上的数据文件切换。
那些复杂的自动化流程、跨表关联、权限体系、表单分发这些能力,直接砍掉。不是做不到,而是没必要,先把核心闭环跑通才有价值。
边界划清楚之后,技术方案基本也就浮出水面了。纯前端、浏览器能跑,不需要服务器,那数据存储就必须在浏览器本地,界面交互就用Vue或者React这类框架来做。这里还涉及一个问题:既然是纯前端,怎么能保证数据不丢?后面我会专门讲。
2. 技术选型:为什么最后定了Vue + IndexedDB + Tailwind这一套
2.1 数据存储方案:IndexedDB是最合适的落点
纯前端应用,最头疼的就是数据存哪里。有过几种选择:
- localStorage:简单、同步,但容量上限通常只有5MB-10MB左右,存储复杂的表格数据很快就会爆掉。而且localStorage只有同步API,频繁读写会阻塞主线程,表格数据一大,页面直接卡死。
- Web SQL:已经废弃的方案,只有部分浏览器支持,不可能作为长期依赖。
- IndexedDB:浏览器原生支持的非关系型数据库,容量上限很大(通常能达到几GB甚至更多),支持异步读写、索引、事务。虽然API设计反人类,但可以通过封装库来规避。
IndexedDB毫无疑问是正解。而且需要明确一点:IndexedDB存的是结构化数据,不是像localStorage那样存纯字符串。它的异步特性在数据量大的场景下反而是优势,不会阻塞UI渲染。
我在项目里使用了一个轻量封装idb-keyval,只有不到1KB的大小,却把IndexedDB最常见的键值存储场景封装得非常顺手。对于这个项目来说,数据库结构足够简单,根本不需要引入Dexie这类重量级库去管理表结构。
2.2 框架选择:Vue 3的响应式系统更适合表格类界面
前端框架我选了Vue 3,而不是React。原因很实际:
多维表格的本质是一块巨大的、可编辑的二维数据结构。表格单元格的频繁更新、行数据的实时新增删除、视图筛选后数据集的动态计算,这些都是强响应式场景。Vue 3基于Proxy的响应式系统,在处理嵌套对象(行对象里的字段对象)的更新时,精确度天然有优势——你修改了某一行的一个字段值,Vue能精准地只更新那个单元格对应的DOM节点。
React也能做,但需要自己注意memo、useCallback这些优化手段,不然很容易出现某个单元格输入时整张表格重渲染的情况。对于个人项目,我当然选择心智负担更低的那条路。
组合式API在组织这个项目的逻辑时很好用。我按功能拆分了一堆composable(类似React的custom hooks):useTableData(数据CRUD)、useViewState(视图状态管理)、useFieldSchema(字段配置管理)、useClipboard(复制粘贴),每个模块独立维护,后期加功能也不至于牵一发动全身。
2.3 样式方案:Tailwind CSS给写自定义编辑器省了大量时间
样式这块我直接用了Tailwind CSS。原因很简单:表格类应用有大量高度自定义的UI组件——单元格编辑器、下拉选择器、字段配置弹窗、看板卡片——用Tailwind的原子类可以快速迭代样式,不用频繁地和CSS文件做心理斗争。
尤其在做不同字段类型的选择器、弹出层这些小组件时,Tailwind的absolute定位、z-index管理、hover状态这些工具类,配合@floating-ui/dom做弹层定位,几乎不需要手写复杂的CSS逻辑。
从最终效果来看,整个界面虽然没有飞书那么精致,但该有的交互都能实现,视觉上干净整洁,达到了工具级应用的可用标准。
3. AI辅助开发:哪些环节AI真的能顶上去
3.1 AI在项目里承担的角色定位
这是整篇文章可能最有信息量的一部分。很多人用AI写代码,要么让AI一口气生成一个完整项目,结果bug满天飞;要么把AI当成高级搜索引擎,只在报错时问问。我的用法是:让AI承担“高级编码助理”的角色,我负责架构设计和决策,AI负责高质量代码的生成。
具体到这个项目,AI在下面这几个环节确实是生产力利器:
- CRUD模板代码的生成。表格数据的增删改查、字段配置的状态管理,这类代码逻辑固定但量大。给AI描述清楚数据结构,它生成的代码基本是开箱即用,比自己敲键盘快三倍以上。
- UI组件库没用时的基础组件开发。我没有引入Element Plus或者Naive UI,而是让AI先帮我生成了一套基础表格组件的基础样式和交互逻辑——列宽拖拽、虚拟滚动容器、单元格点击进入编辑态。这些基础组件的代码模式性很强,AI一次生成的准确率很高。
- 数据处理函数的编写。把多维表格数据导出成CSV(注意处理公式、日期格式、特殊字符转义)、从CSV导入到表格结构(类型推断是关键)、字段筛选排序的分组聚合逻辑——这些纯函数逻辑很适合AI来写,因为输入输出非常明确,只要把需求说清楚,测试几个边界用例就能验证正确性。
- 样式和视觉细节的快速实现。比如让AI写一个支持粘贴、拖拽选择、Markdown语法解析的单元格编辑器,只需要描述清楚接口和预期的交互行为,AI就能用
contenteditable或者textarea实现一个有基础能力又不会失控的版本。
3.2 提示词的关键写法
这里分享几个我在这类项目中实际使用了、并且验证有效的提示词结构。
写一个具体功能时:
实现一个可编辑单元格组件: - Props: field(字段配置), value(值), disabled(是否禁用) - 点击单元格进入编辑态,失去焦点保存值 - 文本字段用input,数字字段用input[type=number],日期用input[type=date],下拉选项用自绘的popover - 编辑态按Escape取消,按Enter确认 - 请输出Vue 3组合式API代码,TypeScript类型周日完整处理数据转换时:
写一个函数,把JSON数组导出为CSV: - 对象key作为表头 - 值中如果包含逗号、换行、双引号,需要转义并用双引号包裹 - 使用BOM防止Excel打开中文乱码 - 返回Blob对象性能优化时:
当前表格渲染3000行数据时滚动卡顿,分析可能的原因并给出优化方案: - 渲染方式:一次性渲染全部dom - 数据更新频率:每行每列都可能被编辑 - 期望效果:滚动流畅无白屏AI在这类”单一职责、输入输出明确“的任务上表现非常好。反而是涉及全局架构、数据一致性、模块间通信的决策,AI给的建议通常是“通用正确但未必适合你当前项目状态”的,这类问题就不要指望AI了,得自己拿主意。
3.3 AI编程没有替你解决的环节
说实话,项目里最费时间、最磨人心智的几个环节,AI基本帮不上忙:
- 数据结构设计。字段类型如何组织、行的数据如何存储才能兼顾查询效率和存储空间、筛选排序如何建立索引,这些纯架构层面的事情,AI只能给通用建议,具体取舍还得靠业务理解。
- 边界情况排查。比如删除一个字段时,相关视图里引用这个字段的配置怎么处理;比如正在编辑时浏览器意外刷新,未保存的数据如何处理。这类“逻辑链路长、状态组合多”的问题,AI每次只能看到你贴给它的一小部分上下文,很难给出全局一致的解决方案。
- 真实用户体验打磨。拖动列宽时是实时跟随还是虚线预览、双击单元格出现光标后键盘方向键怎么移动、粘贴多行数据时如何和当前选区对齐,这些细节AI给出的方案往往不是最优的,需要自己反复体验和调整。
所以我最终对AI编程的定位可以总结成一句话:架构决策不能交给AI,但体力活可以;验收测试不能省略,但生成的代码基本靠谱。
4. 核心数据模型:多维表格的“多”到底体现在哪里
4.1 字段系统和行数据的结构设计
多维表格和普通表格的本质区别在于:列不仅仅是列,而是带类型的字段。飞书里的字段类型有几十种,我这里砍到核心的6种:text、number、date、select(单选下拉)、tags(多选标签)、checkbox(复选框)。
字段的配置结构如下:
interface FieldSchema { id: string; // 字段唯一ID name: string; // 字段名称 type: FieldType; // 字段类型 options?: string[]; // select/tags类型时的选项列表 width?: number; // 列宽 visible?: boolean; // 是否显示 sortable?: boolean; // 该字段是否参与排序 }行的数据存储则遵循扁平化原则——每一行是一个对象,key是字段ID,value是字段值。这样设计的好处是:结构简单、和现有数据处理工具(如Array.prototype.filter/sort/map)天然契合、导入导出JSON/CSV时无需额外转化。
interface RowData { id: string; // 行ID values: Record<string, FieldValue>; // 字段ID -> 字段值 createdAt: number; updatedAt: number; }一个核心设计难点在于:不同字段类型的值格式完全不同。数字是number类型,日期我统一用时间戳(number),下拉选项存选项的id而不是显示文本,复选框是boolean,标签是string[]。所以FieldValue必须是联合类型:
type FieldValue = string | number | boolean | string[] | null;所有字段值都允许为null,表示“未填写”。这个设计在UI上表现为空单元格,在筛选和分组时则需要单独处理空值的归属。
这个扁平化结构很简单,但在实际开发中对比了飞书多维表格导出的数据结构——它内部用的是更复杂的嵌套结构——我确定自己的方案更适合变通。核心原因是:我的平替版不需要处理“表格间引用”“公式字段”等高级场景,数据结构越简单,出bug的概率也越低。
4.2 视图状态的数据归约设计
多维表格的杀手锏是“多个视图看同一份数据”。表格视图、看板视图、日历视图,本质上都是同一份行数据的不同归约视角。
基于这个思想,我在代码里把“数据层”和“视图层”做成了两个独立的状态:
- 数据层:保存最原始的行数据和字段配置,不关心任何视图逻辑。
- 视图层:保存视图的筛选条件、排序规则、分组字段、搜索关键字,以及当前是哪种视图类型。
当视图层配置变化时,通过纯函数从原始数据计算出当前视图下应该展示的行集合。这样做的好处是:切换视图不需要修改数据本身,数据永远保持纯净。
视图状态的数据结构:
interface ViewState { id: string; name: string; type: 'table' | 'kanban'; filters: FilterRule[]; sortRules: SortRule[]; groupFieldId?: string; // 看板视图的分组字段 searchKeywords?: string; }筛选规则和排序规则都设计成了结构化的对象,而不是硬编码的逻辑,这样后续扩展更多视图类型时不需要改动数据层。
interface FilterRule { fieldId: string; operator: 'is' | 'isNot' | 'contains' | 'greaterThan' | 'lessThan' | 'isEmpty' | 'isNotEmpty'; value?: FieldValue; }计算视图数据的函数是纯函数,入参是全部行数据和视图状态,返回值是展示行列表。
function getVisibleRows(allRows: RowData[], view: ViewState, schemaMap: Record<string, FieldSchema>): RowData[] { let rows = allRows.filter(row => matchFilter(row, view.filters, schemaMap)); rows = searchRows(rows, view.searchKeywords, view.fieldIds, schemaMap); rows = sortRows(rows, view.sortRules, schemaMap); return rows; }这个纯函数是组件的computed属性,Vue会帮忙做依赖追踪——只要有行数据或视图配置发生变化,展示列表自动更新。由于是纯函数,单测也特别好写,我在开发过程中给筛选和排序逻辑都写了单元测试,这在后面加功能时给了我很大信心。
4.3 为什么不用表格类的第三方库
看到这里,你可能想问:为什么不直接用ag-grid、Handsontable这种成熟的表格库?
老实说,我刚开始也装了ag-grid社区版,花了一个晚上做技术验证。结论是:可以跑,但没有想象中好用。
- 第三方表格库虽然渲染性能好、交互丰富,但它是黑盒。我想实现单元格的复制粘贴、拖拽选择、自定义编辑器弹层,这些和表格库的内部机制常常冲突,每绕过一层就要去读它几百行的文档和源码,耗费的心智远超直接手写。
- 第三表格库的样式体系高度自成一派,想改成飞书多维表格那种“清新工具感”的视觉界面,需要做大量覆盖,成本比从零写一套还高。
- 我的场景是“数据量几千行”级别,这个量级下用虚拟滚动方案手写一个表格渲染容器,性能完全够用,不需要上重型库来控制。
所以最后我选择自己实现核心表格引擎,包括虚拟滚动、单元格编辑、列宽调整、表头排序。虽然前期开发量大一些,但后期加功能、修bug、改样式,完全掌控,非常舒服。
5. 表格渲染引擎的关键实现
5.1 虚拟滚动:千行数据的流畅之道
多维表格最怕的就是数据量一大,页面卡成幻灯片。几千行、几十列,如果一次性全部渲染成DOM节点,浏览器妥妥卡死。业界标准方案是虚拟滚动,只渲染可视区域内的行和列,数据结构层面看起来是几千行,但DOM节点可能只有几十个。
实现思路是:
- 把表格放在一个固定高度的容器中,容器
overflow: auto。 - 监听容器的
scroll事件(用requestAnimationFrame节流),计算当前滚动位置对应的可视区域行范围。 - 在容器上方放一个高度等于总行数的“占位div”,用来撑起滚动条。
- 可视区域内的行用绝对定位放置在对应的纵向位置。
列方向的虚拟滚动更复杂一些,因为列宽不固定。我采取的做法是:计算每列的累计左侧偏移量存成数组,根据容器宽度和滚动位置算出哪些列可见,只渲染这些列。列方向也做了“左右冗余渲染”,防止快速滚动横向时出现白屏。
这里有一个关键的细节:行高必须固定。虚拟滚动依赖固定的行高来计算滚动位置对应的行索引。多维表格的行高我固定为32px,表头高度固定为36px,这样计算逻辑就简化成了纯数学问题。
5.2 单元格编辑与复制粘贴的细节
单元格编辑不是简单的“点击后用input替换内容”,里面涉及一整套焦点管理和键盘导航逻辑:
- 单击单元格进入编辑态,输入框接管焦点并自动全选内容。
Tab或者Enter确认当前值并跳到下一个单元格。Escape取消编辑,恢复原值。- 方向键在单元格之间移动焦點(编辑态下方向键在输入框内移动光标,退出编辑态则移动单元格焦点)。
这部分代码是整个项目里状态切换最频繁、最容易出bug的地方。我经历过的典型问题:在单元格之间快速切换时,前一个输入框的值还没来得及写回数据,新输入框已经绑定了旧值。解决方案是:在数据写回统一使用v-model的lazy修饰符,并在beforeUpdate生命周期里保证最终值提交。
复制粘贴逻辑也是一大工程。复制的时候,选中的区域内容会被格式化为带制表符分隔的文本矩阵,同时写入navigator.clipboard。粘贴时,解析剪贴板文本,按行和制表符分割成二维数组,再根据当前光标位置批量写入数据。
这里容易踩的坑是:粘贴时如果选区是多个单元格,粘贴内容应该如何填充?飞书的行为是“如果粘贴的矩阵比选区大,扩展选区;如果比选区小,只覆盖左上角对齐区域”。我参考了这个行为,实现上就是把粘贴矩阵和目标区域做一个对齐后的循环写入。
5.3 看板视图的分组逻辑与拖拽交互
看板视图的本质是按某个字段的值对数据进行分组。分组计算很好写:按字段值的不同,把getVisibleRows的结果分成几组。
但这个字段特殊之处在于:如果字段是空值,飞书会给一个“未分组”的落点。尤其是当下拉选项或者成员字段有空值的时候,这个“未分组”的设计很关键,否则这些行会凭空消失,用户感知不到数据的存在。
看板视图的拖拽交互用了HTML5原生拖放API。拖拽卡片从某一列挪到另一列,本质上就是更新该行记录的分组字段值。这里有个绕不开的坑:dragenter和dragover事件的默认行为是禁止放下,所以必须在dragover事件里调用event.preventDefault()。此外还需要在dragstart时把被拖拽的行ID通过dataTransfer传到拖放目标,而不是依赖闭包变量,否则拖拽中途数据状态变化时会有奇怪的bug。
看板视图的列在数据量大的时候也要做横向滚动。我参考了“瀑布流”的思路,让每一列都保持独立的滚动位置。这样虽然实现上要多维护一个位置映射表,但用户体验确实比整体滚动好很多——看A列往下翻,不会影响B列的位置。
6. IndexedDB持久化的细节和坑
6.1 数据结构与存取流程
IndexedDB的API虽然能直接用,但如果没有做良好的封装,状态管理和IndexedDB之间的同步很容易出问题。我的方案是:
- 整个应用的状态全部放在一个Vue响应式对象里,作为“唯一数据源”。
- 任何数据的增删改查都在这个响应式对象上操作。
- 同时记录一个
dirty标记,在数据变化后统一将整个状态对象序列化写入IndexedDB。
写入时机采用防抖策略:每次数据变更后,设置300ms的定时器;如果300ms内再次变更,则重置定时器。这样做的好处是,用户在连续快速输入时,不会每敲一个字符都触发一次全量数据库写入,等用户停下来300ms后,一次性持久化当前状态。
注意:IndexedDB写入是完全异步的,窗口关闭前如果写入还没完成,数据就会丢失。我在
beforeunload事件里手动触发了一次同步写入请求,并尽量让写操作在页面隐藏前完成,实测基本上数据不会丢。
6.2 版本升级与数据迁移
IndexedDB有一个设计:创建数据库时要指定version,改了字段结构必须升级版本号并写迁移逻辑。我的存储结构因为功能迭代变了两次:
- v1:只存行数据和字段配置。
- v2:增加了视图配置的存储。
- v3:增加了表格组的支持(一张表变成多张表)。
每次升级都要在onupgradeneeded回调里写迁移代码。这个环节很考验数据兼容性的设计功底:不光是新增字段那么简单,还要做好旧数据字段的默认值补全、新字段的初值设置。我的经验是:迁移代码写完一定要手动造一份旧版本的数据来测试,不要只在全新的IndexedDB上验证,否则线上会莫名丢数据。
7. 部署上线与浏览器兼容性实践
7.1 部署方案:静态托管两步搞定
纯前端应用部署极其简单,不需要买服务器、不需要配置后端环境。构建产物就是一个静态资源文件夹,扔到任何静态托管平台都能跑。
我试了两种方案,都可行:
- GitHub Pages:仓库开个Pages功能,自动把
main分支的dist目录发布出去。好处是无需额外配置,和代码仓库天然集成。缺点是国内访问速度不稳定。 - Vercel:连接Git仓库后自动构建部署,国内访问速度比GitHub Pages快很多。免费额度对这个项目级别完全够用。
部署之后还有一个细节:因为是单页应用,如果直接访问某个子路由(比如刷新后跳到某个视图页),静态托管方需要把未知路径全部重写到index.html。GitHub Pages不支持自定义重写规则,所以在404.html里做了一层处理;Vercel需要在项目根目录放一个vercel.json配置文件。
7.2 跨浏览器兼容性的实测结果
标题里写着“浏览器就能跑”,那到底哪些浏览器能跑?我专门在几个主流浏览器上做了测试,结果如下表:
| 浏览器 | 版本 | IndexedDB | 表格渲染 | 剪贴板 | 实测结论 |
|---|---|---|---|---|---|
| Chrome 近两个大版本 | 120+ | 正常 | 正常 | 正常 | 核心目标环境 |
| Edge | 120+ | 正常 | 正常 | 正常 | 表现同Chrome |
| Firefox | 近两个大版本 | 正常 | 正常 | 权限弹窗 | 整体可用 |
| Safari | 17+ | 正常 | 正常 | 需手动粘贴 | 主要兼容问题在于剪贴板事件 |
| 国产双核浏览器(极速模式) | 基于Chromium | 正常 | 正常 | 正常 | 依赖Chrome内核 |
实测中,最需要单独处理的是Safari的剪贴板事件。navigator.clipboard.readText()在Safari里只能在用户手势触发时调用,而且还会弹权限提示;在复制单元格区域时,Safari对纯文本的处理倒是没什么问题。针对Safari,我做了降级方案:生成一个临时textarea,选中后调用document.execCommand('copy'),这个方法虽然废弃但Safari依然支持,作为兜底很实用。
另一个容易踩的坑是浏览器隐私模式。Safari和Chrome的隐私模式会限制IndexedDB的容量,有时直接禁用持久化。我做了异常捕获,在数据写入失败时弹出提示,并引导用户使用普通模式使用。
7.3 浏览器指纹与用户标识的问题
做多表格支持时,需要判断“同一个浏览器里有哪些表格”。这个不能靠后端登录态,只能在浏览器本地处理。我用的是目前在浏览器指纹方案里比较常见的做法:利用canvas指纹生成一个匿名ID,存储在localStorage中,作为这个浏览器的用户标识。
这里涉及一个比较有意思的边界情况:如果用户清了localStorage,匿名ID就变了,他之前创建的表格就“不知道”是谁的了。面对这个问题,我的处理方式是增加一个“导出/导入整个工作区”的功能,让用户可以主动备份和迁移所有数据。这个功能比纠结用户ID的持久性更实用。
7.4 离线使用的实现
纯前端应用在浏览器里跑,天然支持离线访问吗?也不是。如果没有配置Service Worker,没有缓存的页面在断网时是加载不出来的。我加了一个极简的Service Worker,在用户第一次在线访问时把整个应用外壳(HTML、JS、CSS、静态资源)缓存到Cache Storage中。之后即使断网,应用也能照常打开。
数据层面,由于IndexedDB的存储本身就在本地,所以“断网后还能继续编辑表格”是小菜一碟。唯一要注意的是:网络恢复后需要有一个“同步”机制。因为我这个版本还没有做多端同步(计划中),所以目前的状态是“谁在哪个浏览器上改的,数据就留在哪个浏览器里”。这个点我在页面上做了明显的本地存储标识提示,避免用户误以为数据在云端可以多端通用。
8. 性能优化的关键动作:从卡顿到流畅
8.1 虚拟滚动之外,还要关注的性能瓶颈
虚拟滚动解决的是“DOM节点太多”的问题,但性能瓶颈不止这一个。三千行数据跑在浏览器里,哪怕DOM节点只有几十个,筛选、排序、分组这些计算也会消耗不少时间。我通过Chrome Performance面板分析后,把几个明显耗时的地方单独优化了:
- 筛选排序的计算缓存。
getVisibleRows虽然是纯函数,但每次渲染都会执行,数据量大时消耗明显。我在视图配置不变时,把计算结果缓存起来(用computed的cache机制),只在行数据或筛选配置变化时重新计算。这里Vue的computed天然支持这个逻辑。 - 大量小状态更新的合并。比如拖拽看板卡片到新组,会同时触发:分组字段更新、卡片位置动画、分组列高度变化。如果每个变化都立刻触发视图重算,会造成多次重复计算。我的做法是在这类批量操作外面包了一层
nextTick,让所有状态变更先在同一个事件循环里合并,再统一触发一次重渲染。 - 长列表的图片处理。单元格里有图片预览的话,图片的加载会阻塞滚动。我干脆在列表适配里加入“图片懒加载”能力:只加载可视区域内的图片,其他图片等滚到附近时再加载。
8.2 实测数据:一次性能压测的结果
我在一个四年前的MacBook Pro(Intel芯片)上做了一个压测:
- 数据量:5000行、30列,总单元格数15万个。
- 操作类型:全量滚动、快速筛选、分组切换。
- 结果:滚动时帧率稳定在50-60fps;筛选一次约100ms;分组一次约250ms。
这个性能数据在个人工具场景下完全够用,和飞书多维表格在同等数据量下的表现持平甚至更流畅(毕竟飞书还要消耗一部分网络请求时间)。主要归功于虚拟滚动把DOM节点压缩到了极致。
9. 抄作业指南:我用的AI提示词示例
这部分算是给想尝试“AI辅助开发”的朋友一点实际的参考。我把自己在几个关键场景里用的提示词整理出来,格式和细节都经过验证。
9.1 常用提示词模板
生成虚拟滚动表格组件:
实现一个虚拟滚动表格容器组件: - 输入:总行数、行高、可视高度、渲染函数 - 监听滚动,计算可视起始索引和结束索引 - 上下都渲染5行冗余,避免快速滚动出现白屏 - 容器内需要有一个高度为总行数*行高的占位div来撑开滚动条 - 使用Vue 3组合式API,TypeScript生成CSV导入的函数:
写一个Vue 3中的CSV导入逻辑: - input元素选择文件后,用FileReader读取为文本 - 需要识别UTF-8 with BOM、UTF-8无BOM、GBK编码的文件 - 按行解析,引号内的逗号不拆分 - 第一行为列名,后续行为数据 - 自动根据第一行数据推断列类型(number/date/string) - 返回解析后的字段配置和行数据生成看板拖拽的交互代码:
实现看板视图的拖拽分组的逻辑: - 使用HTML5拖放API - 拖拽卡片时,目标列的drop区域高亮 - 松开时,更新行数据的分组字段为目标列的字段值 - 拖拽过程中要处理dataTransfer数据,拖拽结束后清空这些提示词之所以有效,核心在于它们都包含了一个关键信息:输入输出明确、边界条件明确。如果你的提示词里没有说清数据结构和期望行为,AI会按照它自己脑补的假设来写,结果大概率不符合你的预期。
9.2 AI生成代码后的必做检查清单
AI生成的代码我默认都不是最终版本,而是“待人工审查的半成品”。拿到代码后我会检查:
- 边界情况:空数组、空对象、null值会不会导致崩溃片。
- 性能路径:循环里有没有重复计算、有没有不必要的深拷贝。
- 样式一致性:Tailwind的类名是否符合设计规范,有没有被AI写成内联style。
检查完之后我还会手运行一遍关键流程,确保代码不是“看起来对,跑起来错”。
10. 写完这个项目之后,我对AI编程的新理解
这个项目从想法到上线大约用了两周的业余时间,整体体验和预期的最大差距是:AI并没有让开发变“简单”,但确实让开发变“快”了。同样的功能和代码质量,按我以前全手工写,估摸要一个半月;现在两周能完成,AI释放掉的体力活贡献很大,但架构设计、任务拆解、验收测试这些环节,AI帮不上太多忙,这些依然需要人来拿主意。
有个观点我越来越认可:AI编程最难的部分不是写代码,而是知道应该让AI写什么。如果自己不理解数据流、不理解组件生命周期、不理解浏览器的存储机制,你就没办法把需求拆解成清晰的小任务,AI的表现也不会好到哪里去。所以,AI编程并没有降低程序员的上手门槛,反而对程序员的任务拆解能力和领域理解能力提出了更高要求。
另外一点收获是:纯前端工具的边界比想象中大得多。以前觉得,做一个“类似飞书”的工具必须有后端、有账号系统、有数据库。但这轮实践下来,发现浏览器本身的IndexedDB、Service Worker、剪贴板API、本地文件系统访问这些能力的组合,已经足够支持一个轻量级、单机版的工具应用。它没有多端同步、没有协作——但只是个人使用和轻量团队共享,已经够用了。
如果你也想做一个类似的纯前端工具,我只有一条最核心的建议:先把边界划清楚,再动手写代码。想清楚哪些能力不做,比你做的能力本身更重要。否则你会陷入无限加需求、永远完不了工的困境。
这个项目我目前还在持续打磨,下一步计划加入多表之间的数据引用、简单公式字段支持,还有一个实验性的多端同步方案(用WebRTC点对点连接,不走服务器)。等这些功能稳定之后,我再专门写一篇技术剖析。目前这个版本的所有代码都放在我自己的仓库里,如果你想上手体验或照着改造,完全自由可用。