大数据量的十八般武艺¶
这一章讲一件事:数据多到一屏放不下时,前端怎么摆、后端怎么切,两头怎么配合。 九种兵器逐个讲:列表、表格、看板、分页、三种编辑、删除(软删除)、排序、过滤、 搜索(基础与高级)。主线例子:项目列表页——从「一屏 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 → 拖票过状态机。
- 表示法三选一: 列表管看、表格管管、看板管改;从最简单的开始, 专业化↔可复用性成反比。
- 通用表格 = 泛型组件 + 数据源对象 + 单元格渲染器; 分页不属于表格, 独立做、到处用。
- 分页四概念: total / limit / offset / page;客户端可请求,服务器说了算。
- 排序必须先于切片——「只排那一页」没有意义。
- 排序字段走白名单映射,既是防泄密也是防注入;用户输入永不直接进 SQL。
- 软删除 = UPDATE 一个标记位; 「撤销」比「没发生过」有信息价值。
- 过滤
Condition::all/any可组合; 搜索从contains起步,别过度设计。 - 重型搜索两条路: Elasticsearch(独立引擎)或 Toql(查询语言); 记住锤子定律——先想清楚问题,再挑工具35。
12. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 三种表示法与选型规矩 | Managing Bigger Sets of Data | text/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」) |
-
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:77,搜「tabular data」;:104,搜「rule of thumb」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:130,搜「emphasize operations」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:150,搜「inversely proportional」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1692,搜「educational purposes」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:98,搜「material_yew」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:170,搜「iter()」)、:206(搜「None => html」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:240,搜「No projects yet」)、:292(搜「polite message」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:380,搜「ITableDataSource」)、:420(搜「Using generics is necessary」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:363,搜「Creating a data source」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:470,搜「cellrenderer」)、:552(搜「data and the configuration」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:350,搜「separate the concern of pagination」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:571,搜「not the way a table should be implemented」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:615,搜「DragEvent」)、:583(搜「overhead」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:589,搜「dragstart」)、:592(搜「drop event」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:691,搜「DragStart」)、:696(搜「Drop」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:709,搜「set_transfer_data」)、:733(搜「text/plain」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:745,搜「kill the server」)、:748(搜「final word should always come from the server」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:758,搜「Total」)、:1679(搜「Page: A wrapper」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:805,搜「get_projects」)、:913(搜「conditional application」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:916,搜「ordering has to precede slicing」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:936,搜「tradeoff」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:939,搜「benefit of using the same language」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:945,搜「Calculate the steps」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:971,搜「hybrid solution」)、:980(搜「full focus」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1000,搜「not be POST but PUT」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1061,搜「almost the same」)、:1077(搜「original values」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1087,搜「No Content」)、:1090(搜「Not Found」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1147,搜「soft delete」)、:1147(搜「information value」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1208,搜「sort_to_column」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1193,搜「fictive name」)、:1238(搜「full control」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1740,搜「Never blindly convert」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1281,搜「Condition::all」)、:1321(搜「Condition::any」)、:1328(搜「come from a query parameter」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1383,搜「contains」)、:1425(搜「only use pagination to limit」)。 ↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1449,搜「Elasticsearch」)、:1464(搜「Toql」)、:1486(搜「maintenance overhead」)、:1496(搜「tested and working solutions」)。 ↩↩ -
出处:「Managing Bigger Sets of Data」(
text/42-fm-introduction.txt:1527,搜「law of the instrument」)。 ↩↩