加载中...

本篇要回答的问题:怎么把前后端两个软件(paintdom、paintweb)真正对接起来,并在对接的同时保留离线编辑能力?为此网络协议要怎么调整、文档怎么加载、Model 层为什么应该"越厚越好"?

这是"画图"程序五讲实战的收官篇。上一讲我们定义了 paintdom(服务端)与 paintweb(前端)之间的网络协议并各自实现了第一版,但两者还没真正对接。本讲就完成对接——目标不是加编辑功能,而是让文档能存到服务端、随处可打开,同时断网能继续编辑、联网能自动同步

宏观的系统架构:paintweb 服务端的"平庸"

paintweb(:8888)和 paintdom(:9999)在 DEMO 里同进程跑(paintdom 作为 goroutine),只为方便调试一起起停;现实中它们是不同软件,通过反向代理进程间协作。

paintweb 自身的服务端是业务无关、"平庸"的,它只做三件事:

paintweb 服务端职责 说明
托管 Web 前端文件 一个普通静态文件下载服务器,吐 HTML+CSS+JS
支持帐号服务 对接 Account Service,实现 Web 用户登录/登出(基础架构类服务,须全公司共享)
业务协议转译 把 Session-based API 请求转为 Multi-User API 请求;本例不支持多租户,转译退化为简单转发

关键判断:业务逻辑的串联,靠的是 www 里的 js 文件 + paintdom 的 API,而不是 paintweb 的后端。后端越"平庸"、越业务无关越好。

把它对应到 24 讲讲过的分层模型:

本例角色 对应层 含义
paintdom Multi-User Model 多租户业务服务器,实现 Automation 所需 API
paintweb 服务端 Session-based Model 负责 Session-based → Multi-User 的转译
www 里的 HTML+JS+CSS Session-based ViewModel 真正的 Web 业务入口,遥控浏览器侧渲染

胖前端 vs 胖后端

模式 业务逻辑在哪 优点 缺点
胖前端(本例) JavaScript 可离线 Web 业务代码暴露
胖后端 如 PHP 跑在后端 IT 资产保全更安全(别人看不到完整逻辑) 无法离线,断网即不可用

计算变更:怎么知道离线改了什么?

对接后要做到:断网照常离线编辑保存;一联网,离线改动自动同步到 paintdom。难点是如何识别断网期间改过的内容。三种思路:

思路 做法 问题
一:全量保存 每次完整保存整篇文档 太浪费(平时每次编辑都自动保存)
二:记录操作历史 每个编辑操作写入 localStorage 同一对象多次编辑指令爆炸;断网久了甚至超过文档大小,缺鲁棒性
三:对象版本号 给对象加 ver,与文档基版本 baseVer 对比,ver > baseVer 即变更 选用此方案

prepareSync(baseVer) 遍历所有 shape,挑出 ver > baseVer 的放入 changes,返回 {shapes(全量ID列表), changes(变更), ver},并自增文档 ver。

同步变更:从"编辑操作"到"同步协议"

有了变更信息,怎么发给服务端?若把变更还原成一条条编辑操作发送,会遇到部分成功的中间态——最烧脑、最考验编程水平。

架构准则:不要烧脑。对大部分非性能敏感的业务代码,简单易实施是第一原则。

于是选择修改网络协议,新增一个同步接口,整体把变更 diff 推过去(由 QSynchronizer 类完成)。

新增的文档同步接口示意

复盘的洞见:最初按业务理解定义的"一系列编辑操作"协议没有错,只是没预见离线编辑需求。需求的预见性很重要;及早推出 Mock 让前端快速迭代,才能及早暴露协议不足——越晚调整协议,代价越大。

加载文档:统一图形表示 + 注册式可扩展

难点是根据服务端返回的 JSON 重建文档。原先网络协议的图形格式和 localStorage 里的不同,要写两套加载逻辑——没必要。结合"图形种类会越来越多"这个可预期的变化点,做了一次重构:

  • 统一 localStorage 与网络协议中的图形表示;
  • 新增图形种类要容易、代码内聚。

手段是引入 qshapes: QSerializer 全局变量,各图形类型把自己的 creator 注册进去(qshapes.register("rect", json => new QRect(json))),每个图形实现 constructor(json)toJSON(),之后统一用 qshapes.create(json) 创建实例。

加载文档分三类场景:

场景 含义 联网时 非联网时
_loadBlank 加载新文档 服务端创建新 drawing 本地建临时文档(displayID 以 t 开头)
_loadTempDoc 加载一直离线编辑的临时文档 服务端创建 drawing 并同步离线数据 加载离线数据,继续离线编辑
_loadRemote 加载远程文档 先加载本地离线缓存,再异步拉远程,成功后放弃本地离线内容 加载本地缓存

加载完成后 QPaintDoc 发出 onload 消息,由 QPaintView 响应刷新界面。原因是 ajax 完成时机不可预期;用事件解耦,避免 Model 层去理解 View 层的业务逻辑

Model 层的厚度:越厚越好

秉承"Model 层越厚越好"的理念(22 讲提出:Model 层最与框架无关、最易测试、最易跨平台)。两组观测数据印证:

观测 数据
dom.js 行数 MVP(v26)约 120 行 → 最新(v30)约 860 行,翻约 7.x 倍
各版本变更 v27/v28/v30 主要改 Model;v29 看似没动 dom.js,其实整个变更都是服务端 Model(Multi-User Model)

v27 版本变更历史
v28 版本变更历史
v29 版本变更历史
v30 版本变更历史

推论:若 Model 代码不内聚、散落各处,变更质量会非常不受控。Model 层环境依赖最小、最容易测试,一旦被打散到 View/Controller,阅读/维护/测试难度都会大增。

迭代后总结出的 Model 层职责(自然会"胖"):

Model 层职责 说明
业务逻辑、对外暴露业务接口 最本职的工作
实现 View 委托的 onpaint 完成绘制
实现 Controller 的 hitTest 支持 selection
与服务端 Multi-User Model 通讯 View/Controller 都无需感知服务端
离线编辑 localStorage 存取 持久化

总结

本讲完成了画图程序前后端的对接,复杂度主要来自"支持离线编辑"。核心是:用对象版本号识别离线变更、用同步协议而非编辑操作流来回避部分成功的中间态、用注册式序列化统一图形表示并预留扩展、用 onload 事件解耦异步加载、最后用厚 Model 层承接绝大部分变化。

先把"是什么"回答清楚

概念 一句话说明
Multi-User Model 多租户业务服务器(paintdom),提供 Automation API
Session-based Model Session→多租户的转译层(paintweb 服务端),本例退化为转发
胖前端 / 胖后端 业务逻辑放前端 JS(可离线)/ 放后端如 PHP(更安全但不可离线)
baseVer 版本对比 文档基版本,对象 ver > baseVer 即为离线期间发生过变更
同步协议 整体推送变更 diff 的接口,回避逐操作发送的部分成功中间态
QSerializer 图形类型注册 creator 的全局序列化器,统一图形表示、易扩展

一句话速记

离线编辑靠"版本号识别变更 + 同步协议整体推送",把烧脑的部分成功问题挡在门外;图形用注册式序列化统一表示;一切复杂度尽量压进最易测试、最该变厚的 Model 层

几条值得记住的判断

  • 不要烧脑:非性能敏感的业务代码,简单易实施优先于"看似省事"的复杂方案。
  • 需求预见性 + 及早 Mock:协议越晚改越贵,Mock 能让前端先跑、尽早暴露协议缺陷。
  • Model 层越厚越好:它最易测试、最跨平台,是内聚的天然落点。

思考题

回到你自己的项目:你的客户端与服务端协议,是按"业务操作"定义的,还是能从容应对"断网/重试/部分成功"这类工程现实?如果现在要支持离线编辑,你会发现哪些原先"正确"的协议设计其实是缺乏预见性的?

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