一 · 八个文件,各是什么
| 文件 | 层 | 干什么 |
|---|---|---|
index.htm |
组装根 | 决定加载哪些文件 + 页面布局(一个 menu div + 一个 canvas) |
dom.js |
Model | 图形的数据 + 自己画自己 |
view.js |
ViewModel | 五个事件盒子、花名册、selection、当前样式、重画 |
creator/rect.js |
Controller | 画 Line / Rect / Ellipse / Circle |
creator/path.js |
Controller | 画折线 |
creator/freepath.js |
Controller | 自由笔 |
accel/select.js |
Controller | 选择工具(27 讲新增) |
accel/menu.js |
Controller | 菜单按钮 + 属性面板 + 鼠标位置显示 |
最容易忘的一条:
menu.js也是 Controller。
它不是"框架",它跟那些画图工具是平级的——只不过它的辅助 View 是按钮,别人的是橡皮筋。
二 · 三条链路
链路一 · 画一个矩形
你在画布上按下鼠标
↓ 浏览器:查 document 树 → 命中 <canvas>
↓ 【view.js】 canvas.onmousedown 的转交函数
↓ 【view.js】 view.onmousedown 盒子
↓ 【rect.js】 QRectCreator.onmousedown → 记起点(Model 里什么都没发生)
拖动…松手
↓ 【rect.js】 onmouseup → buildShape()
↓ 【dom.js】 doc.addShape(矩形) ★ 图形诞生
↓ 【view.js】 invalidateRect → 取笔、擦干净
↓ 【dom.js】 doc.onpaint → 每个图形自己画
链路二 · 换工具
你点菜单上的「Rect」按钮
↓ 浏览器:查 document 树 → 命中 <button id="RectCreator">
↓ 【menu.js】 按钮的 onclick → onClickCtrl("RectCreator")
↓ 【view.js】 invokeController("RectCreator")
├─ stopController() → 上一个把盒子还回来
└─ 查花名册 → 工厂() → new QRectCreator(...)
↓ 【rect.js】 构造函数 → 抢走四个盒子
链路三 · 画完自动切回选择工具(27 讲新增)
(接链路一,松手之后)
↓ 【rect.js】 reset() → qview.fireControllerReset() 「我画完了」
↓ 【view.js】 看 onControllerReset 槽 → 取出函数调用
↓ 【menu.js】 启动时挂的那个函数 → qview.invokeController("ShapeSelector")
↓ 【view.js】 走一遍链路二的后半段
↓ 【select.js】 QShapeSelector 上岗,抢走盒子
链路四 · 选中一个图形并拖动(27 讲新增)
你在画布上点一下(当前是选择工具)
↓ 【view.js】 转交 → view.onmousedown 盒子
↓ 【select.js】 QShapeSelector.onmousedown
↓ 【dom.js】 doc.hitTest(pt) → 倒着遍历,问每个图形"这点在你身上吗"
↓ 【view.js】 view.selection = 命中的那个 → setter 发 onSelectionChanged
↓ 【menu.js】 听到了,把调色板更新成这个图形的颜色
拖动…松手
↓ 【select.js】 onmouseup → selection.move(dx, dy) ★ 这一刻 Model 才真的变
三 · 谁认识谁
menu.js ──▶ view.js ──▶ dom.js
creator/*.js ──▶ view.js ──▶ dom.js
creator/*.js ──▶ dom.js (直接调 addShape)
select.js ──▶ view.js ──▶ dom.js
✗ menu.js 不认识任何 creator (只念字符串 "RectCreator")
✗ creator 之间 互不认识
✗ view.js 不认识任何具体 Controller(只认识字符串 + 花名册)
✗ dom.js 谁都不认识
箭头全都朝下,没有回头的,没有横着的。
四 · View 上一共有几个「槽」
这是最容易混的地方。它们机制一样(都是装函数的字段),但用法完全不同:
| 槽 | 谁放进去 | 什么时候放 | 会换人吗 | 谁取出来调 |
|---|---|---|---|---|
onmousedown onmousemoveonmouseup ondblclick onkeydown |
当前 Controller(构造时抢) | 每次换工具换一次 | 会,一直在换 | View(浏览器送事件来时) |
onControllerReset |
菜单 | 启动时挂一次 | 不会 | View(Creator 调 fire... 时) |
onSelectionChanged |
菜单 | 启动时挂一次 | 不会 | View(selection setter 里) |
方向一:外部世界 → Controller
浏览器 ─▶ canvas 盒子 ─▶ View 的五个盒子 ─▶ 当前 Controller
方向二:Controller 之间横向,View 当中转站
Creator ─喊 fire─▶ View 的 onControllerReset 槽 ─▶ 菜单
View 自己 ───────▶ View 的 onSelectionChanged 槽 ─▶ 菜单
View 在所有方向上干的都是同一件事:端着盒子,按盒子转交,不关心内容。
五 · 两个入口
事件其实从两个地方进来,在 View 汇合:
canvas.onmousedown ┐
canvas.onmousemove │
canvas.onmouseup ├──▶ View 的五个盒子 ──▶ 当前 Controller
canvas.ondblclick │
document.onkeydown ┘
↑ 只有这一个挂在 document 上(键盘没有坐标,canvas 又拿不到焦点)
六 · 相关篇目
| 想细看什么 | 去哪 |
|---|---|
| 完整源码 + 每个文件的结构地图(标行号) | 带读-源码导航 |
| 前端底子(盒子、lambda、canvas、document) | 带读-00 |
dom.js 逐类读 |
带读-01 |
| 一次画矩形的完整链条(含浏览器一侧) | 带读-02 |
| ViewModel 的责任 · 菜单与组装 · 架构思维 | 带读-03 |
| 27 讲:加选择工具 | 带读-04 |
| 28 讲:离线持久化与对象 ID | 带读-05 |
| 29 讲:网络协议与服务端(含异步) | 带读-06 |
| 30 讲:对接、同步、Model 层的厚度 | 带读-07 |
| 31 讲:辅助界面元素与控件化 | 带读-09 |
| 32 讲:系统的概要设计(附 AI coding 对照) | [带读-10](…/32-架构:系统的概要设计/带读-10-32讲:系统的概要设计(附 AI coding 对照).md) |
| 收尾:五讲带走了什么(六个手法 · 16 条判断 · 诚实清单) | 带读-08 |
七 · 服务端开发篇的带读(11~19)
这一页(一~六节)讲的是浏览器端的 qpaint(v26~v31)。
服务端那条线(34~46 讲)的带读接在下面,四讲实战 41~44 用的是同一个 qpaint,只是换到了paintdom/。
7.1 基础篇:存储与业务架构
| 想细看什么 | 去哪 |
|---|---|
| 37 讲:事务、主从与分布式(KV 是一切的基础) | 带读-11 |
| 38 讲:文件系统与对象存储(去目录 / 去关系 / 去可变那三刀) | 带读-12 |
39 讲:存储与缓存(缓存就是 @lru_cache · 那条永久不一致) |
带读-13 |
| 40 讲:服务端的业务架构建议(四条建议背后的机制) | 带读-14 |
7.2 实战四讲:把 mock 服务端做成产品级
| 想细看什么 | 去哪 |
|---|---|
| 41 讲:RPC 框架、分层与单元测试(★ 业务层零改动的那份 diff) | 带读-15 |
| 42 讲:使用界面、数据结构与算法(★ 最厚的一讲:多租户 · mongodb · 索引 · 顺序与事务) | 带读-16 |
| 43 讲:帐号与授权(★ OAuth 的零点 · 双 token · 授权码模式为什么那么绕) | 带读-17 |
| 44 讲:dex、ID Token 与「不造轮子」(★ 网关翻译授权 · JWT 不是加密的) | 带读-18 |
| 收尾:后端四讲带走了什么(六个手法 · 16 条判断 · 诚实清单 · 与前端五讲的对照) | 带读-19 |
7.3 服务端那边的源码在哪
前端那条线的源码在 26-…/代码/qpaint源码/v26 ~ 31-…/v31;服务端这条线是:
| 分支 | 是什么 | 放在哪 |
|---|---|---|
v29-服务端 · v30-服务端 |
第一版 mock 服务端(全内存) | 29 讲 · 30 讲 |
v31-服务端 |
mock 版定稿(41 讲的起点,286 行 service.go) |
41 讲 |
v41-服务端 |
换成 restrpc(169 行;业务层与 v31 字节相同) | 41 讲 |
v42-服务端 |
换成 mongodb + 多租户(含那份 README_IMPL.md) |
42 讲 |
v44-bear-网关 |
帐号授权(paintdom 零改动,全部在网关) |
44 讲 |
⚠ 一个必须记下来的事实:原文 44 讲让你看的
compare/v42...v44是空的(0 commit,v44=v42)。
接 dex 的代码不存在;真正提交了代码的是v44-bear。详见 带读-19 · 6.1。
7.4 服务端这条线的一句话
前端五讲问:什么该放进 Model? → 需求在长,Model 就该跟着长(dom.js ×7.9)
后端四讲问:什么不该自己写? → 环境在换,业务层就一行都不该动(×1.09)
★ 两个答案是同一件事:把业务从它所处的环境里剥出来。
