编程语言研究书库

第 5 章拆解 — 大数据量的十八般武艺:列表、表格、看板、分页、编辑、删除、排序、过滤、搜索

大数据量的十八般武艺

这一章讲一件事:数据多到一屏放不下时,前端怎么摆、后端怎么切,两头怎么配合。 九种兵器逐个讲:列表、表格、看板、分页、三种编辑、删除(软删除)、排序、过滤、 搜索(基础与高级)。主线例子:项目列表页——从「一屏 20 行」到 「按截止日期倒序、翻到第 3 页、把某行就地改掉、再搜个人名」,每个兵器在这趟旅程上走一步。

1. 顶层全景

  表示法选型:列表 < 表格 < 看板(专业化↑ 可复用性↓)
    ↓
  通用表格组件:泛型三参数 + 数据源对象 + 单元格渲染器
    ↓
  看板拖拽:web-sys 直调浏览器 DragEvent API
    ↓
  分页:total/limit/offset/page ←→ 共享 Page 结构体(同语言的第一个红利)
    ↓
  编辑:聚焦(表单+PUT)/ 就地 / 混合;删除:确认 + 软删除
    ↓
  排序(白名单映射)→ 过滤(Condition)→ 搜索(contains → Elasticsearch/Toql)

  图说:前半章是「看」,后半章是「改」,中间的分页是分水岭。

2. 表示法选型:列表、表格、看板

解决什么问题: 同一批数据,摆法不同,用户读到的东西完全不同。 规矩(书里的话):永远从最简单的开始,复杂表示法是需求逼出来的; 三种都是「表格数据(tabular data)」——记录成行、字段成列,连数据库控制台 都长这个样,用户天生熟悉1。

表示法干什么用本章的例子
列表(list)只展示、不操作:着陆页小部件「最近的项目」一列链接,按创建日期倒序
表格(table)管理与浏览:可排序、可分页的结构化数据项目列表页
看板(board)强调「操作数据」而非「看数据」:列=状态,拖=改状态工单看板

看板那句描述值得记住:它「呈现的信息刚好够看出当前状态之外的选择, 并鼓励改变状态的操作」——焦点不在数据,在状态的迁移2。

一条设计原理(书里给了公式): 组件的专业化程度与可复用性成反比。 多用通用组件,少用复杂组件,高度专业化的只留几个; 「如果你发现自己造的组件一个都复用不了,八成是设计错了」3。

还有一句坦白的自我定位: 书里的 Bulma 组件是「为本书实现的教学件, 别拿去生产——缺专业库的精心设计和充分测试」4。 「别重复造轮子」是原则,但现实是 Rust-Wasm 的组件库选项当时非常有限 (material_yew 一家独大,还没有表格组件)5。

3. 列表与表格组件

列表是例行公事: 拿到 Vec<ProjectDto>,用迭代器 map 成一行行 <tr>, None 分支返回空标签兜底(没有 id 链接无从指起,只能优雅地忽略)6; 空数据要给礼貌提示(「还没有项目」),这个好习惯全章保持7。

表格是本章第一个硬骨头。 难在约束:Rust 是静态类型,表格想「什么数据都能画」, 得逼数据的每个字段实现 Display trait——不现实。书里的架构解法是把表格做成 泛型组件,三个类型参数:列枚举(如 ProjectField)、数据接口(如 IProjectDto)、 值类型(如 ProjectValue)8。

用法三步:造数据源 → 传排序回调(可选)→ 嵌进 html!9。 数据源是个对象,装着数据和全部配置:空态文案、行列表头开关、 以及重头——单元格渲染器(cell renderer):一个回调函数,拿到 「当前列 + 当前行的数据」,决定这一格画什么10:

  match 列 {
      Id       => 纯文本 id
      Summary  => 链到 /project-board/{id} 的链接
      Deadline => 格式化日期(没有就 "-")
      Active   => 一个 CheckTag 勾选图标
  }
  guard:id 是 None 就整格跳过(没有 id,链接也造不出来)

  图说:同一个表,四格四种画法——渲染器就是「表格与业务之间的翻译官」。

把分页从表格里摘出去(关注点分离):表格的唯一职责是表示结构化数据, 分页独立实现、到处复用11。书里还自我纠偏:「这不是表格的「标准做法」, 只是一个够用解;开发本来就是从能跑的解迭代到干净的解」12。

4. 看板拖拽:Rust 里调浏览器 API

解决什么问题: 拖放(drag-and-drop)不在 Yew 的领地里,要直接调浏览器 API ——通过 web-sys(第 07 篇那個「不够 rusty 但别无选择」的封装)。 书里形容这是「像用 JavaScript 一样用 JavaScript 的 API,只是从 Rust 调」; 代价是 Rust 更严,得为类型安全多写检查13。

功能最小集只有两个事件: dragstart(被拖的票上)和 drop(目标列上)14。

  <div draggable="true" {ondragstart}>        ← 票:标记可拖,挂上 dragstart 回调
      <a draggable="true" ondragstart={|e| e.prevent_default()}>  ← 链接默认可拖,禁掉防串戏
          {title}
      </a>
  </div>

  <div {ondrop} ondragover={|e| e.prevent_default()}>  ← 列:ondrop 收票;
                                                           ondragover 只是占位(可用来做高亮)

  图说:dragover 必须 prevent_default,否则 drop 事件根本不会触发(浏览器的怪规矩)。

两条消息完成闭环:DragStart(把票 id 塞进transfer data——拖放专用的数据暂存区) 和 Drop(读出 id → 跟目标列的状态比较 → 有合法变化才发更新请求)15。 对浏览器 API 的调用就一行:data_transfer.set_data("text", data)—— "text" 是 text/plain 的简写;读侧对称,get_data 返回 String16。

5. 分页:四个概念与一条铁律

解决什么问题: 两个方向都要护——客户端内存有限,不能一口吞全量; 服务器更不能被「查全站所有帖子」这种请求打死——分页也是一种安全手段。 客户端可以请求块大小,但最终裁决权永远在服务器(防恶意大 chunk 攻击)17。

四个概念(Key terms 里正式定义了前三个)18:

概念含义
Total全部可用条目数
Limit服务器返回/客户端请求的最大条目数
Offset游标位置:跳过多少条
Page包住这一块的容器,带 total/offset/limit 元数据(还有 list)

后端四步改造(项目资源为例): 接 Query<Pagination> + Query<Ordering> (Axum 支持多个查询参数;offset/limit 可空但要有默认值); 先 count 出 total;排序;用 apply_if(pagination.limit, QuerySelect::limit) 条件应用 limit(SeaORM 的条件应用特性),再 .offset();最后包进 Page19。

那条铁律(书里加粗强调的道理):排序必须先于切片。 「只给那一小块排序毫无意义——客户端自己也能排; 排全集再切,这件事只能服务器干」20。

前端侧两个改变: 请求时带上 limit/offset;接受 Page 而不是 Vec。 小部件(最近项目)其实不要元数据,只发 limit=3 然后丢掉元数据只用 list—— 「专用端点 vs 泛型端点 + 一点赘肉」的权衡,书里选了后者21。 而 Page 结构体本身放进 shared 成员——后端写、前端读,同一份定义, 「这就是两端同语言的红利」22。

分页组件只干一件事: 从 total/limit/offset 推导出「当前在第几页、 有没有前后页、翻页后 limit/offset 变成多少」——能推导的绝不在网络间传输23。

6. 编辑与删除

三种编辑形态(书里按场景分)24:

形态长什么样本质
聚焦编辑(focused)独立页面/弹窗里的传统表单,字段带标签最常见,也「有点老派」
就地编辑(in-place)值嵌在别的视图里,点一下变成输入框用户可能意识不到在编辑(拖票换列就是)
混合(hybrid)弹窗先看,点值即进入编辑前两种的合体

聚焦编辑和建单几乎一样(第 07 篇的表单),差别只有两个: 记录已有 id(藏在表单里、也出现在 URL 里)和 HTTP 方法用 PUT 而非 POST25。 混合/就地编辑的幕后跟聚焦编辑发的是同一个 PUT 请求; 没改的字段由表单负责填回原值26。

删除是编辑的特例: 方法换成 DELETE、不带请求体; 响应体也不用解析——结果全写在状态码里: 204 No Content(删了)、404(没这条,也许早删了)、500(边缘错误)27。

软删除(soft delete)——两层守卫:

  • 第一层在客户端: 删之前弹确认;
  • 第二层在服务器: 很多时候根本不该真删——交易记录要留全程历史 (撤销也比「没发生过」有信息价值);用户删号了,项目还得有人挂, 那就抹掉个人数据、保住关系。做法不是 DELETE 而是 UPDATE: 打个删除标记,列表里不再出现,库里的外键关系还活着28。

7. 排序、过滤、搜索

排序的完整链路: 前端发 ?sort=id&order=desc&limit=3; 后端有一个 sort_to_column 白名单映射:"id" => Column::Id、"summary" => …, 匹配不上返回 None 就用默认排序29。

这个映射是安全件,不是便利件: 「sort 参数可以是虚构名—— 我们不必泄露内部数据结构」;白名单外的名字落不到 SQL 里, 等于给排序字段上了一道栏杆30。(书里脚注还补了一条通用红线: 永远不要把用户输入直接拼成 SQL;要白名单、消毒、绑定参数、校验31。)

过滤:Condition::all 与 Condition::any。 例子是指派工单对话框: 列出「还没指派项目」的票(ProjectId.is_null()),约束是一张票不能同时进多个项目。 Condition::all 加的过滤是 AND 关系,Condition::any 是 OR,可以组合; 过滤条件也不必写死——它可以从查询参数来(下一步就是搜索)32。

搜索,基础版: 给人派活时总能假设用户记得对方名字的几个字—— ?q=ali,后端 filter(Column::Name.contains(q))。 q 可空:有就加过滤,没有就原样放行。可以和分页、排序叠加; 这一版故意只分页不包 Page——「只拿分页当限流用」33。

搜索,高级版(两条路):

方案是什么代价
搜索引擎Elasticsearch(基于 Apache Lucene),独立部署,REST API + 官方 Rust 客户端;客户端发平台无关的 JSON 查询多养一个服务;响应格式归它自己
客户端查询语言Toql:让终端用户自己写查询串,如 id, book_published eq 1, .age gt 50,后端安全翻译成一条 SQL(带 JOIN)已经是 ORM 之上的额外维护负担;当时只支持 MySQL;还得教用户这门语言

书里对两条路的共同态度:「该从军火库里拿高成本方案时, 就用生态里测过的工作方案,别自己造」34。

8. 主线例子走一遍:项目列表页的一天

  用户打开 /projects
      │  ① 前端请求 GET /projects?limit=20&offset=0
      ▼
  后端:count → 默认排序 → limit/offset 切片 → Page{total=86, offset=0, limit=20, list:[...]}
      │  ② total=86 → 分页组件推导出「共 5 页,当前第 1 页,有下页无下页」
      ▼
  用户点表头「截止日期」→ 请求带 sort=deadline&order=desc
      │  ③ sort_to_column 白名单映射 → 先全集倒序再切片(铁律!)
      ▼
  用户翻到第 3 页:offset=40;点某行标题就地改成「新版发布」:PUT /projects/41
      │  ④ 204/200 回来,行内刷新
      ▼
  用户打开「指派工单」对话框:GET /tickets/unassigned
      │  ⑤ ProjectId.is_null() 过滤;搜索框敲 "ali":q=ali → Name.contains
      ▼
  找到人,拖票到「TESTING」列:dragstart 塞 id → drop 读 id → PUT 改状态
  ——全程 9 种兵器,一个页面走完。

  图说:每种兵器都在这条链上恰好出现一次。
  (total=86、offset=40 是为演示编的数;状态名与端点名出自原文。)

9. 作者的判断与证据(分开标)

有出处的: 四概念定义与「先排序后切片」是书中明写1820; 软删除的交易记录论证、白名单映射的安全理由、Toql 的三条缺点,均为原文283034。

作者自己的评估:

  • 「专业化↔可复用性成反比」被作者称为 design principle,其实是经验法则;
  • 教学组件「别用于生产」的免责声明4;
  • 对 Elasticsearch/Toql「够用就别上」的克制,与第 2 章 YAGNI 一脉相承;
  • 章末引「锤子定律」(law of the instrument):工具多了才不容易把一切当钉子35。

10. 边界与局限

  • 表格组件是「够用解」: 移动优先、列宽、合并单元格这些深水区没进本书 (书外的生产选择:当时直接上成熟 JS 表格更稳)。
  • 搜索只到 contains: 全文检索的排序质量(相关性)、中文分词, Elasticsearch 那节完全没碰——第 13 章会回来补全文搜索。
  • unwrap() 依旧出没在前端 service 示例里(教学简化,生产要 Result 到底)。
  • 拖拽无障碍(a11y)没提: 键盘操作、屏幕阅读器的替代方案缺失。

11. 可带走的

主线例子一行走完: GET /projects?limit=20&offset=0 → Page{total,list} → 点表头排序(先全集后切片)→ 翻页(offset=40)→ 就地 PUT 改标题 → unassigned 过滤 + q=ali contains → 拖票过状态机。

  1. 表示法三选一: 列表管看、表格管管、看板管改;从最简单的开始, 专业化↔可复用性成反比。
  2. 通用表格 = 泛型组件 + 数据源对象 + 单元格渲染器; 分页不属于表格, 独立做、到处用。
  3. 分页四概念: total / limit / offset / page;客户端可请求,服务器说了算。
  4. 排序必须先于切片——「只排那一页」没有意义。
  5. 排序字段走白名单映射,既是防泄密也是防注入;用户输入永不直接进 SQL。
  6. 软删除 = UPDATE 一个标记位; 「撤销」比「没发生过」有信息价值。
  7. 过滤 Condition::all/any 可组合; 搜索从 contains 起步,别过度设计。
  8. 重型搜索两条路: Elasticsearch(独立引擎)或 Toql(查询语言); 记住锤子定律——先想清楚问题,再挑工具35。

12. 原文地图

主题原书章原文位置
三种表示法与选型规矩Managing Bigger Sets of Datatext/42-fm-introduction.txt:104(搜「rule of thumb」)、:130(搜「emphasize operations」)
专业化↔可复用性同上text/42-fm-introduction.txt:150(搜「inversely proportional」)
「别用于生产」免责声明同上text/42-fm-introduction.txt:1692(搜「educational purposes」)
列表 map 与空态同上text/42-fm-introduction.txt:170(搜「iter()」)、:234(搜「No projects yet」)
表格泛型三参数同上text/42-fm-introduction.txt:380(搜「ITableDataSource」)、:420(搜「generics」)
单元格渲染器同上text/42-fm-introduction.txt:470(搜「cellrenderer」)、:558(搜「gracefully skip」)
分页摘出表格同上text/42-fm-introduction.txt:350(搜「separate the concern」)
「够用解」自评同上text/42-fm-introduction.txt:571(搜「not the way a table should be implemented」)
web-sys 拖拽同上text/42-fm-introduction.txt:615(搜「DragEvent」)、:596(搜「bare minimum」)
dragover 怪规矩与同名简写同上text/42-fm-introduction.txt:659(搜「ondragover」)、:684(搜「spare」)
transfer data 读写同上text/42-fm-introduction.txt:709(搜「set_transfer_data」)、:733(搜「text/plain」)
分页的安全理由与裁决权同上text/42-fm-introduction.txt:745(搜「kill the server」)、:748(搜「final word」)
四概念同上text/42-fm-introduction.txt:758(搜「Total」)、:1679(搜「Page: A wrapper」)
后端四步与 apply_if同上text/42-fm-introduction.txt:805(搜「get_projects」)、:913(搜「conditional application」)
先排序后切片同上text/42-fm-introduction.txt:916(搜「ordering has to precede slicing」)
共享 Page 红利同上text/42-fm-introduction.txt:939(搜「benefit of using the same language」)
分页组件只推导同上text/42-fm-introduction.txt:945(搜「Calculate the steps」)
三种编辑形态同上text/42-fm-introduction.txt:971(搜「hybrid solution」)、:980(搜「old-fashioned」)
PUT 与 id 两差异同上text/42-fm-introduction.txt:1000(搜「not be POST but PUT」)
删除的状态码语义同上text/42-fm-introduction.txt:1087(搜「No Content」)
软删除两层守卫同上text/42-fm-introduction.txt:1147(搜「soft delete」)、:1144(搜「confirmation」)
排序参数与白名单同上text/42-fm-introduction.txt:1193(搜「fictive name」)、:1208(搜「sort_to_column」)
永不直拼 SQL同上text/42-fm-introduction.txt:1740(搜「Never blindly convert」)
Condition all/any同上text/42-fm-introduction.txt:1281(搜「Condition::all」)、:1321(搜「Condition::any」)
contains 搜索同上text/42-fm-introduction.txt:1383(搜「contains」)、:1425(搜「only use pagination to limit」)
Elasticsearch 路线同上text/42-fm-introduction.txt:1449(搜「Elasticsearch」)
Toql 示例与三缺点同上text/42-fm-introduction.txt:1470(搜「book_published eq 1」)、:1486(搜「maintenance overhead」)
锤子定律收尾同上text/42-fm-introduction.txt:1527(搜「law of the instrument」)
Points to remember同上text/42-fm-introduction.txt:1538(搜「Points to remember」)
Key terms同上text/42-fm-introduction.txt:1685(搜「Search engine」)

Footnotes

  1. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:77,搜「tabular data」;:104,搜「rule of thumb」)。 ↩

  2. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:130,搜「emphasize operations」)。 ↩

  3. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:150,搜「inversely proportional」)。 ↩

  4. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1692,搜「educational purposes」)。 ↩ ↩2

  5. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:98,搜「material_yew」)。 ↩

  6. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:170,搜「iter()」)、:206(搜「None => html」)。 ↩

  7. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:240,搜「No projects yet」)、:292(搜「polite message」)。 ↩

  8. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:380,搜「ITableDataSource」)、:420(搜「Using generics is necessary」)。 ↩

  9. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:363,搜「Creating a data source」)。 ↩

  10. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:470,搜「cellrenderer」)、:552(搜「data and the configuration」)。 ↩

  11. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:350,搜「separate the concern of pagination」)。 ↩

  12. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:571,搜「not the way a table should be implemented」)。 ↩

  13. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:615,搜「DragEvent」)、:583(搜「overhead」)。 ↩

  14. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:589,搜「dragstart」)、:592(搜「drop event」)。 ↩

  15. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:691,搜「DragStart」)、:696(搜「Drop」)。 ↩

  16. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:709,搜「set_transfer_data」)、:733(搜「text/plain」)。 ↩

  17. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:745,搜「kill the server」)、:748(搜「final word should always come from the server」)。 ↩

  18. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:758,搜「Total」)、:1679(搜「Page: A wrapper」)。 ↩ ↩2

  19. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:805,搜「get_projects」)、:913(搜「conditional application」)。 ↩

  20. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:916,搜「ordering has to precede slicing」)。 ↩ ↩2

  21. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:936,搜「tradeoff」)。 ↩

  22. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:939,搜「benefit of using the same language」)。 ↩

  23. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:945,搜「Calculate the steps」)。 ↩

  24. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:971,搜「hybrid solution」)、:980(搜「full focus」)。 ↩

  25. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1000,搜「not be POST but PUT」)。 ↩

  26. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1061,搜「almost the same」)、:1077(搜「original values」)。 ↩

  27. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1087,搜「No Content」)、:1090(搜「Not Found」)。 ↩

  28. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1147,搜「soft delete」)、:1147(搜「information value」)。 ↩ ↩2

  29. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1208,搜「sort_to_column」)。 ↩

  30. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1193,搜「fictive name」)、:1238(搜「full control」)。 ↩ ↩2

  31. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1740,搜「Never blindly convert」)。 ↩

  32. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1281,搜「Condition::all」)、:1321(搜「Condition::any」)、:1328(搜「come from a query parameter」)。 ↩

  33. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1383,搜「contains」)、:1425(搜「only use pagination to limit」)。 ↩

  34. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1449,搜「Elasticsearch」)、:1464(搜「Toql」)、:1486(搜「maintenance overhead」)、:1496(搜「tested and working solutions」)。 ↩ ↩2

  35. 出处:「Managing Bigger Sets of Data」(text/42-fm-introduction.txt:1527,搜「law of the instrument」)。 ↩ ↩2

On this page