加载中...

本篇要回答的问题:把前几讲抽象的 MVC/MVMP 理念落到一个真实案例——浏览器端的"画图"程序,它的 Model、ViewModel、Controller 三层各自该怎么写、怎么解耦?

这是"画图"程序实战四讲的第一讲。前面 22~25 讲讲的都是普适设计理念,显得抽象。本讲选一个需求好懂的"画图"程序(源码 github.com/qiniu/qpaint),把 B/S 程序在浏览器端的三层结构走一遍——本版本暂未连服务端,仍是单机版。

带读(26~30 讲连续实战的预备课):这五讲全部代码是 JavaScript + 浏览器。
不熟前端就先过 带读-00-读懂qpaint需要的前端底子.md
里面有 26~30 的完整路线。

展开精读:原文只给接口规格、不给一行实现,最关键的三个方法(Controller.onpaintstop()
invalidateRect())读起来全是黑盒。把 qpaint 源码拉下来对着读了一遍,逐条回答"为什么是这样、
以及作者自己做到了没有"——见同目录 精读-26-把MVC按在代码上验一遍.md
配套可跑代码 代码/qpaint-mini.html

B/S Web 程序的三层回顾:

本质
Model 层 多用户 Model + 单租户 Session-based Model;浏览器端是完整的单租户 DOM 模型
View 层 实为 ViewModel 层,真正 View 被浏览器实现;只有数据和可委托事件
Controller 层 多个相互解耦的 Controller;切忌让 Controller 互知,更别让 View 知道具体 Controller

Model 层

代码即 dom.js,是一棵以 QPaintDoc 为根的 DOM 树。各类规格:

类 / 接口 关键内容
QLineStyle width、color
QLine pt1/pt2、lineStyle,onpaint
QRect x/y/width/height、lineStyle,onpaint
QEllipse x/y/radiusX/radiusY、lineStyle,onpaint
QPath points[]、close、lineStyle,onpaint
interface Shape onpaint
QPaintDoc(根) addShape、onpaint

Model 层主要支持两种能力:添加图形(addShape)绘制(onpaint)

关键判断:绘制有两种做法——让 View 来画(需 Model 把数据暴露给 View),或让 Model 层自己画(避免暴露 DOM 实现细节)。作者选后者:虽然 Model 因此和 GDI 耦合、有点"不纯粹",但耦合 GDI 比暴露 DOM 数据细节更好,因为 GDI 接口更稳定

依赖选择原则:考虑耦合时,更倾向依赖接口更稳定的组件——因为这意味着你自己的接口也更稳定。

ViewModel 层

代码是 index.htm(总控:界面布局 Layout + 应用初始化 InitApplication)与 view.js(核心 QPaintView 类)。QPaintView 内容归类:

归属 成员 作用
Model 相关 doc: QPaintDoc 唯一与 Model 的连接,操作 Model
ViewModel 自身 properties(lineWidth/lineColor) 当前用户选择的样式,典型 ViewModel 数据
drawing(DOMElement) 操作 HTML DOM 的抽象
绘制 onpaintinvalidateRect 重绘;当前为简单起见总是整个 View 全部重绘
Controller 相关 registerController / invokeController / stopController 登记 / 激活 / 停止当前 Controller
事件委托(onmousedown/move/up、ondblclick、onkeydown) 让 Controller 选择感兴趣的事件响应
getMousePos 辅助方法,取鼠标位置

关键洞见:Web 开发本不用自己处理局部更新,为何这里又要管?因为我们没有用浏览器的 Virtual View,整个 DOM 数据组织完全自己管理(画在 canvas 上),于是面临的问题与传统桌面开发完全一致。

View 层即便把绘制交给 Model 后只剩"胶水层",仍承担两个重要责任:

责任 说明
屏蔽平台差异 Model 易做平台无关,Controller 大部分是事件响应也跨平台,差异收敛到 View
定义界面布局 不同尺寸设备的整体布局在 View 层控制最妥当

Controller 层

文件较多(相近的合并):menu.js(Menu/PropSelectors/MousePosTracker)、path.js、freepath.js、rect.js。两点要注意:

# 注意点 说明
1 菜单不直接和创建图形的 Controller 打交道 而是调 qview.invokeController 激活对应 Controller,避免两类 Controller 互相耦合
2 Model 4 种图形 ↔ 界面 6 种表现 Model 只有 QLine/QRect/QEllipse/QPath,界面有 Line/Rect/Ellipse/Circle/Path/FreePath——同一 DOM API 在 Controller 层常有多条实现路径

几个特殊点:选择 lineWidth/lineColor 操作的是 ViewModel 数据而非 Model(可看作某种 Selection);MousePosTracker 极简但特殊——它不操作 Model/ViewModel,而是操作输入事件

QRectCreator 为例看创建型 Controller 的工作机理:

环节 动作
构造 传入 shapeType——它复用于 Line/Rect/Ellipse/Circle(凡用两个 point 构建的图形)
mousedown 记录第一个 point,开启数据收集
mouseup 收集第二个 point,创建 Shape 并加入 DOM
keydown 支持按 ESC 放弃创建

架构思维上我们学到什么

关键判断:这个程序裸写 JavaScript、不依赖第三方库——不是鼓励裸写,而是为消除框架喜好差异。更要强调:别被框架绑架,框架不应增加代码耦合,否则就该丢掉;很可能你用的是好框架,但用没用好取决于你自己的思维。

需求分析之后进入架构第二步:概要设计(系统设计),核心是分解子系统,关心三个问题:

问题
每个子系统负责什么?
它依赖哪些子系统?能否少知道一些子系统的存在?
它们靠什么接口耦合?接口是否自然体现业务关系、是否足够稳定?

桌面程序一致的套路:

套路要点
Model 接口自然体现业务逻辑
View 连接 Model 与 Controller,提供事件委托,但不知道任何具体 Controller
Controller 每个彼此独立,职责就是响应事件→调 Model/ViewModel 改数据

总结

本讲把 MVC 落到画图程序:Model 自己画(耦合更稳的 GDI、不暴露 DOM 细节)、ViewModel 当胶水并屏蔽平台/布局差异、Controller 间靠 invokeController 解耦。当前仍是单机版,没连服务端。

先把"是什么"回答清楚

概念 一句话说明
QPaintDoc Model 层 DOM 树的根,支持 addShape 与 onpaint
Model 自绘 让 Model 画图,耦合更稳定的 GDI,胜过暴露 DOM 数据细节
依赖选择原则 优先依赖接口更稳定的组件,使自身接口也更稳定
QPaintView ViewModel 核心,含 doc/properties/drawing + Controller 管理 + 事件委托
invokeController 菜单通过它激活 Controller,避免 Controller 间耦合

一句话速记

Model 自己画图(耦合更稳的 GDI、不暴露 DOM)、ViewModel 当胶水并管布局、Controller 间只通过 invokeController 解耦——抽象的 MVC 在画图程序里就长这样。

几条值得记住的判断

  • 依赖更稳定的组件:耦合 GDI 好过暴露 DOM 数据细节。
  • 同一 DOM API 在 Controller 层常有多条实现路径(4 种 Model 图形 → 6 种界面表现)。
  • 别被框架绑架——框架不该增加耦合,用没用好取决于你的思维。

思考题

作者让 Model 自己绘制(耦合 GDI),理由是"GDI 接口更稳定"。回到你的代码:你在做依赖选择时,是按"接口稳定度"权衡,还是仅凭"哪个库顺手"?有没有因为依赖了一个高频变动的组件,把不稳定传染给了自己的接口?

公告栏
这是我的个人知识库。
记录技术,也记录生活 —— 读过的、试过的、想明白的,都堆在这儿。
最新文章
网站资讯
文章数目 :
5
已运行时间 :
本站总字数 :
15.7k
本站访客数 :
本站总访问量 :
最后更新时间 :
全局知识图谱
当前页面 已访问 文章 标签
ESC 关闭 · 滚轮缩放 · 拖拽移动 · Ctrl+G 开关