加载中...

五讲干了一件事:把一个 109 行的画图程序,在五轮真实需求下推到 1513 行前端 + 725 行后端,
然后回头看——这些行数落在了哪里。

答案就是这套课想教的全部东西。


一 · 五轮需求,一张表

新需求 被逼出来的东西 代价落在哪
26 能画图 三层分工 · 图形自绘(多态)· 事件委托 · 状态机式 Controller
27 能选中 / 拖动 / 改样式 / 删除 hitTest bound move setProp deleteShape · Selection · 事件中转 Model +197
View +13
Creator 各 +1 行
28 关掉再打开还在 · 断网能编辑 对象 IDlocalID/displayID)· 分层存储 · 变更分级 Model +191
View +6
Controller 零改动
29 有个服务端 网络协议 · Mock 版服务端 · Multi-User Model 浏览器端零改动
新增 657 行 Go
30 两边真的接起来 版本号算变更 · /sync 协议 · QSerializer · onload Model +359
View +2

每一讲的推导都是同一个动作:

不是先想类图,是先问"这个需求要求程序回答什么它现在答不上来的问题",
再把这些问题变成接口,装到该装的那一层上。

27 讲最典型:想做"选中并拖动",就得回答五个新问题——

点了一下,点中了哪个?   → hitTest(pt)
选中框画在哪?          → bound()
怎么把它挪 (dx,dy)?    → move(dx, dy)
怎么改它的样式?        → setProp(key, val)
怎么从文档里删掉?      → deleteShape(shape)

二 · 全系列最硬的三个数字

文件 v26 v30 倍数
Model dom.js 109 856 ×7.9
ViewModel view.js 112 133 ×1.2
Controller 三个原有 Creator 合计 269 256 ×0.95

第三行最值得盯着看。

27 讲加选择工具、28 讲加离线存储、29 讲加服务端、30 讲加同步——
rect.jspath.jsfreepath.js 从头到尾几乎没被打扰过,还反而少了 13 行
normalizeRect 被搬去了 Model,因为它本来就是 Model 的活)。

"Model 越厚越好"不是审美偏好,是这组数字。
逻辑放进 Model,它的变更就波及不到上面两层;散出去,每一次需求都要惊动所有人。

而且必须内聚——原文那句:
“Model 层总体来说是最容易测试的,因为它的环境依赖最小。如果这些代码被分散到
View、Controller 层中,代码的阅读难度、维护难度、测试的难度都会大幅增加。”

856 行放一个文件里看着"胖",散到八个文件里看着"均衡"——但后者每一处都变得难测、难改。
「胖」不是缺点,「散」才是。

Model 层最终的五项职责(原文总结,全系列最该抄下来的一张表):

# 职责 哪一讲长出来的
1 业务逻辑,对外暴露业务接口(最本职) 26
2 实现 View 委托的 onpaint,完成绘制 26
3 实现 Controller 的 hitTest,支持 selection 27
4 与服务端通讯——View、Controller 都不需要感知服务端 29 / 30
5 离线编辑 localStorage 的存取 28

三 · 反复出现的六个手法

这套课真正的价值不在"画图程序怎么写",在于同一招被用了好几次。

① 让 X 自己干(多态)

for s in shapes: s.onpaint(ctx)      # 而不是 if 是矩形…elif 是圆…

26 讲一个决定,后面被原样复用了五次:

方法 哪一讲加的 不这么做的话
onpaint 26 一串 if/elif
hitTest bound move setProp 27 再来四串
toJSON 30 再来一串

好的设计决定会反复付利息。 要是 26 讲当初选了"画法写在外面",
27 讲要加的就不是四个方法,而是四串 if/elif,而且每加一种图形都要回去改四个地方。

② 口子先开好,实现先给最笨的

口子 哪一讲留的 当时的实现 兑现了吗
invalidateRect(rect) 26 参数收了不用,全屏重画 一直没做局部更新
hitCode 27 只有 0 和 1 留给"点在边框/控制点/旋转手柄"
localID / displayID 分家 28 只差一个 t 前缀 30 讲兑现
paintweb 的转译层 30 原封不动转发 留给多租户

信息在现场是免费的,事后重建是昂贵的。
接口里先留下你现在不用、但将来必然需要的信息。

③ 注册表 + 工厂

view.registerController("RectCreator", ()=> new QRectCreator(view, "rect"))   // 26 讲
qshapes.register("rect",               json => new QRect(json))               // 30 讲

同一个套路,一个注册"怎么造 Controller",一个注册"怎么从 JSON 造回图形"。
两者的收益也一样:加一个新的 = 写一个类 + 一行 register,别处一个字不改。

④ 中间加一层,两头互不认识

Creator ──喊──▶ View 的槽 ──▶ 菜单              27 讲:跨 Controller 通信
浏览器  ─────▶ paintweb ───▶ paintdom          30 讲:跨进程通信

同一个形状,只是一个隔的是两个模块,一个隔的是两个进程。

⑤ 只动变了的那部分

为了什么 怎么做
本地分层存储(28) 少写盘 改一个图形,只重写 10001:2 那一条
网络只发 changes(30) 少传数据 改一个图形,只把它放进 changes

先用在硬盘上,再用在网络上。28 讲那个变更分级,就是 30 讲同步的地基。

⑥ 事件:唯一一种「我通知你,但我不认识你」的通信方式

事件 谁发 谁听 哪一讲
onControllerReset Creator 菜单 27
onSelectionChanged View 的 setter 菜单 27
onload Model(加载完) View 30
DataChanged Model(数据变了) View 28 讲预告

直接调用 = 我认识你(依赖出现了);发事件 = 谁想听谁听(依赖消失了)。


四 · 值得背下来的判断

分层

#
1 数据的三个住处,三个一句话测试:换台机器该跟过去吗(Model)/ 开两个窗口该各一份吗(ViewModel)/ 松手该消失吗(Controller
2 半成品不进 Model——撤销栈、协同同步、Model 不变量、ESC 语义,四条理由
3 Model 按"存什么"分类,Controller 按"怎么操作"分类——两套分类轴,不该强行对齐(4 种图形 ↔ 6 种工具)
4 ViewModel 的厚度,正比于局部更新的精细度——不做局部更新,它就该是薄的
5 Model 的形状由使用场景决定:浏览器端三层、服务端四层;浏览器的 Model 自绘、服务端的 Model 一行绘制都没有

接口

#
6 依赖接口不随业务变的那一方——绘图动词四十年没变,DOM 字段这一版就多了个 fillColor
7 接口的严谨程度,取决于"一旦定错了,收回来有多贵"——localStorage 随意,网络协议严谨
8 门要开成业务动作的样子,不要开成数据结构的样子addShape vs _shapes.push
9 参数名划定了这个口子将来能长多大lineStyleshapeStyle
10 只要一个标识符可能被外部世界改掉,就别拿它当内部引用的 keylocalID / displayID
11 一个设计良好的 URL,读一眼就能画出对方的 Model 树

工程决策

#
12 不要烧脑——一个方案会逼出大量中间态时,先看能不能换个方案让这些情况根本不存在(一条条编辑操作 → 一次 /sync
13 判断最坏情况,不是平均情况——"记操作历史"平均很省,但最坏情况没有上界
14 及早推出 Mock——把协议这个最贵的错误,提前到最便宜的时候暴露。30 讲真的改了协议,代价很小
15 判断一个设计好不好,不是看"更灵活",是看"是否更省事、且顺手没把路堵死"
16 当某个模块必须绕过框架才能实现时,通常是框架少了一种语义MousePosTracker 缺"旁听")

五 · 作者自己没做到的地方(诚实清单)

读到能拿作者自己的原则量出他自己的偏差,才算读进去了。

# 问题 违反了哪条 后来修了吗
1 菜单按钮列表硬编码在 menu.js 22 讲"Controller 的辅助 View 必须归它自己" ✗ 一直没修。注释掉一个 creator,会留下会高亮的死按钮,而且点了会把当前工具弄丢
2 MousePosTracker 绕过委托机制,直接包 canvas 的 onmousemove 它不该知道 canvas ✗ 框架缺"旁听"语义
3 deleteShape 不清 localStorage 里那条 ✗ 小泄漏,等文档淘汰才清
4 v28 里 shape.type 被当两个东西用(父指针 + 类名) 可读性 v30 修了toJSON 出来后 hack 自然消失)
5 QEventManager 讨论了但从没实现 — 实际就是两个普通回调字段
6 _loadRemote 里"本地离线编辑的内容会被放弃" ✗ 很粗暴的冲突策略:远程赢。真正的协同要复杂得多

第 4 条特别值得看:重构的常见形态不是"把烂代码改漂亮",而是
“引入一个新概念(toJSON / QSerializer),原来的将就自然就没地方待了”。


六 · 这五讲到底是什么——一个定位说明

6.1 它不是「前端底层原理」

这是最容易误解的一点。 这五讲在这个位置:

   应用架构        ← 【这五讲在这里】MVC 分层、数据住哪、接口设计、同步策略
  ─────────────────
   前端工程        打包、模块、工程化              ← 完全没碰
   框架原理        React 的 Fiber、Vue 的响应式    ← 完全没碰
   浏览器 API      DOM、事件、canvas、localStorage ← 【用到了,但只讲"怎么用"】
  ─────────────────
   浏览器内核      渲染管线、JS 引擎、事件循环      ← 只擦了个边
   操作系统        进程、内存、网络栈              ← 那是 06~16 讲

作者自己在 26 讲就撇清过:

“这个程序没有依赖任何第三方库,是裸写的 JavaScript 代码。……
这并不是去鼓励裸写 JavaScript 代码,这只是为了消除不同人的喜好差异。

浏览器只是这个案例的运行环境,不是主题。

6.2 顺带学到的前端(都是用法层面)

学到的深度
网页的运行模型(<script> 是 main,之后进消息循环) ✓ 挺完整
document 是元素树、按坐标/焦点分派事件 ✓ 够用
事件 = 往字段里放函数 ✓ 但没讲冒泡/捕获/事件委托的完整机制
canvas 是一片格子、立即模式 vs 保留模式 ✓ 这条其实挺硬
localStorage、XMLHttpRequest、异步回调 ✓ 用法
JS 语法(class / 闭包 / 箭头函数 / this ✓ 读代码够用

没学到的:渲染管线(DOM→CSSOM→Layout→Paint→Composite)、JS 引擎与事件循环、
框架原理(虚拟 DOM diff / Fiber / 响应式)、工程化(模块、打包)、CSS、HTTP 缓存与 CDN。
那是另一条线,跟这门课基本不重叠。

6.3 但这里有个反转:这五讲教的东西更长命

保质期
React 的 Fiber 怎么实现的 React 换代就过时
浏览器渲染管线 换个平台(Android / Qt / Unity)不适用
MVC 分层 · 数据住哪层 · 事件解耦 · 增量同步 · 从需求推接口 换任何平台、任何年代都成立

把"画图程序"三个字划掉,剩下的全都成立:

这里叫 换个场景就是
Model / ViewModel / Controller 任何有界面的程序:Qt、Android、Unity、终端 TUI
事件盒子 / 上岗下岗 任何"控制反转"的框架:回调、监听器、中间件
localID / displayID 数据库自增主键 vs 业务单号;inode vs 文件名;commit SHA vs 分支名
分层存储 + 变更分级 任何"增量同步":Git、rsync、数据库 binlog
Mock 先行 任何跨团队协作:接口先定、并行开发
paintweb 转发层 网关 / BFF

你读的是 JavaScript,学的不是 JavaScript。

6.4 一句准确的定位

这五讲是「一个真实应用的架构演进案例」,不是前端教程。
它教的是"逻辑该放哪儿",不是"浏览器怎么跑起来的"。

顺带一提:这门课里更接近"图形界面底层原理"的是 21 讲
(消息循环、事件分派、焦点、GDI、控制反转)——
而且那一讲讲的也不是前端,是所有图形界面程序的共同底层。


七 · 如果只记三条

一 · 架构设计 = 先问"这个需求要求程序回答什么它现在答不上来的问题",
再把问题变成接口,装到该装的那一层上。

二 · 数据住哪层,问三句话就够了:换台机器该跟过去吗 / 开两个窗口该各一份吗 / 松手该消失吗。

三 · Model 越厚越好,而且必须内聚——×7.9 vs ×1.2 vs ×0.95 就是全部理由。


八 · 接下来去哪

这门课还会拿这个程序继续讲:

内容 跟这五讲的关系
31 辅助界面元素的架构设计 已带读带读-09——控件框架、包装第三方库、控件化拆单例
32 架构:系统的概要设计 已带读:[带读-10](…/32-架构:系统的概要设计/带读-10-32讲:系统的概要设计(附 AI coding 对照).md)——整套方法论的总纲、v01 非 MVC 版对照、以及 AI coding 之后这些边界怎么变
41~44 "画图"程序后端实战 把 29 讲那个 Mock 变成正式版(数据库、多租户、高可靠)
加餐 实战:"画图程序"的整体架构 全景复盘

带读系列自己的索引:

带读-地图              哪个文件在哪层、几条链路、谁认识谁     ← 迷路了看这个
带读-源码导航           每一讲的源码在哪、每个文件的结构地图
带读-00                前端底子(26~30 共用)
带读-01 ~ 03           26 讲:Model / 一次画矩形 / 收口
带读-04                27 讲:加选择工具
带读-05                28 讲:离线持久化与对象 ID
带读-06                29 讲:网络协议与服务端(含异步)
带读-07                30 讲:对接、同步、Model 层的厚度
带读-08                收尾(26~30)
带读-09                31 讲:辅助界面元素与控件化
带读-10                32 讲:系统的概要设计(附 AI coding 对照)

六个可跑的东西:

文件 演什么 在哪
qpaint-mini.html 真能画图,右侧面板实时显示三层各握着什么 26 讲
事件盒子.py 盒子 / 抢占 / 归还,不 stop() 的 bug 现场 26 讲
自己画自己.py 画法写在外面 vs 写在图形身上 26 讲
工厂和实例.py lambda、工厂 vs 实例、存实例为什么当场坏掉 26 讲
递一支笔.py 同一批图形递三支不同的笔(含 ASCII 笔真的画出来) 26 讲
两个ID.py localID / displayID,只有一个 ID 会怎样 28 讲
公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关