本篇要回答的问题:画图程序连服务端的第一步——网络协议该怎么设计(RESTful 范式、重试友好性、版本管理)?第一个服务端实现版本为什么要先做成 Mock 版?
上一讲(实战三)我们让画图程序支持了浏览器端的离线持久化。本讲正式迈向服务端,但先不写正式服务,而是聚焦两件事:定义网络协议 + 给出一个串联业务用的 Mock 服务端。暂不考虑多租户授权(留待服务端开发篇)。
服务端是多文档的(浏览器端是单文档 DOM,文档叫 drawing),核心功能:创建/获取/删除 drawing,在 drawing 中创建/取/修改/删除 shape,以及改 shape 的 zorder。
网络协议
遵循一套 RESTful 范式:
| 操作 | 协议 |
|---|---|
| 创建对象 | POST /objects |
| 修改对象 | POST /objects/<ObjectID> |
| 删除对象 | DELETE /objects/<ObjectID> |
| 查询对象 | GET /objects/<ObjectID> |
| 列出对象 | GET /objects(或 ?key=value,本例未用) |
重试友好性
关键判断:网络不稳定,一次请求失败时你不一定能确定真实状态——可能服务端已执行、只是返回时网络出问题。所谓重试友好性,是指同一操作执行两遍,结果与只执行一遍一致。
各类操作的重试友好性对比:
| 操作 | 是否重试友好 | 原因 / 改造 |
|---|---|---|
| 只读(查询/列出) | 是 | 天然幂等 |
创建 shape POST /drawings/<id>/shapes |
是 | 由客户端传 ShapeID,重复创建可返回 409 冲突 |
创建 drawing POST /drawings |
否 | 无参数,重复调用会建出两个 drawing |
| 改 zorder(front/back 相对值) | 否 | 执行两遍会移动 2 层 |
把"不友好"改造成"友好"的手段:
| 手段 | 说明 |
|---|---|
| 客户端传 id / name / uuid | 本质差别不大;name 若被后续引用就等同 id;uuid 是常规改造手法(理解为对象序列号或请求序列号) |
| 改用绝对值而非相对值 | zorder 用 Zorder = 5(绝对)替代 front/back(相对) |
| 用请求序列号 X-Req-Uuid | 通用方法;代价是服务端要记录最近成功的所有 RequestUUID,收到时检查是否已执行过 |
关键洞见:网络协议与 localStorage 存储的 Shape JSON 格式不同——网络协议里类型藏在 key(
"path": {...}),localStorage 里用"type": "path"平铺。原因:localStorage 只是本地缓存、影响小,怎么方便怎么来(无 Schema);而网络协议未来可能作为开放 API,需严谨对待。
版本升级
关键判断:网络协议是一组开放 API,一旦放出就难收回,需考虑兼容。常见做法是带版本号(
POST /v1/objects);发生不兼容变更时升版本(v2)。
带版本号的好处:
| 好处 |
|---|
| 可逐步下线旧版流量,一段时间内两版协议并存 |
| 新老版本业务服务器相互独立,前端由 nginx / 应用网关分派 |
第一个实现版本:为什么先做 Mock
第一个服务端版本怎么做,有两种选择:
| 方案 | 特点 |
|---|---|
| 憋大招 | 直接做业务架构设计→评审→编码→测试→上线 |
| Mock 版(作者选此) | 不依赖数据库,业务逻辑全基于内存数据结构,只串联业务 |
为什么 Mock?因为服务端有大量非业务的通用难题:
| 通用难题 | 含义 |
|---|---|
| 高可靠 | 数据不能丢,硬盘坏、甚至机房地震都不能丢 |
| 高可用 | 服务无单点,几台停机仍能访问;极端如支付宝要异地双活 |
关键判断:先做 Mock 的价值有二——其一,让团队并行:网络协议是协作基础,Mock 让前端不必等后端,后端可自主对协议做高覆盖单测;其二,让业务最快被串联,快速验证网络协议有效性,发现不满足需求可及时调整。
Mock 服务端(Go 实现,paintdom)分两层:
| 层 | 源码 | 说明 |
|---|---|---|
| Model 层 | shape.go / drawing.go | 与网络无关的纯业务核心,DOM 树比浏览器多一层:Document => Drawing => Shape => ShapeStyle(浏览器的 QPaintDoc 对应这里的 Drawing) |
| Controller 层 | service.go | 实现网络协议 |
关键洞见:为什么把网络协议层看作 Controller 层?因为服务端通常无显示模块,故无 View 层;网络协议层负责接受用户输入——只不过输入不是日常交互,而是来自自动化(Automation)程序的 API 请求。
总结
本讲给出网络协议的设计考量(RESTful 范式、重试友好性、版本管理),并选择先做一个不依赖数据库、只串联业务的 Mock 服务端——让前后端并行、让协议尽早被验证。网络协议是前后端耦合的"使用界面",是影响团队开发效率的关键。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| RESTful 范式 | POST/DELETE/GET 对应 创建/删除/查询,资源用 /objects/<id> |
| 重试友好性 | 同一操作执行两遍结果与一遍一致;靠客户端传 id/uuid、绝对值、X-Req-Uuid 实现 |
| 协议版本号 | /v1/、/v2/,便于并存、灰度下线、网关分派 |
| Mock 服务端 | 不依赖 DB、纯内存、只串联业务,让前后端并行、尽早验证协议 |
| 网络协议层=Controller | 服务端无 View;它接受来自 Automation 的 API 输入,本质是 Controller |
一句话速记
网络协议是前后端耦合的"使用界面":用 RESTful 范式 + 重试友好性(客户端传 id/uuid、绝对值替相对值)+ 版本号;第一个实现先做只串业务、不依赖 DB 的 Mock 版,让前后端并行、协议尽早被验证。
几条值得记住的判断
- 重试友好性 = 幂等:创建对象由客户端传 id/uuid,是把"不友好"改造成"友好"的关键。
- 网络协议是开放 API,要带版本号、要比本地缓存(localStorage)严谨得多。
- 先做 Mock 服务端:核心价值是让团队并行、让业务与协议最快被串联验证。
思考题
文中把"创建对象"从重试不友好改造为友好,靠的是让客户端预先分配 id/uuid。回到你的接口:哪些"创建类"接口在网络抖动重试后会产生重复数据?它们能否改成由客户端携带幂等键(uuid / X-Req-Uuid)来根治?

