背景
画猜类业务更新得并不频繁,但实现过的人才知道其中的辛酸苦辣,从一开始的囫囵吞枣,快速业务开发,到现在的考虑画猜组件设计,已经实现了很大的跨越。也正是因为踩过坑,才知道架构和设计的重要性。
画猜组件 1.0
挂在玩吧你画我猜公众号里的3个页面:每日十佳画作、最新最热和单幅画作展示。
痛点:几乎每个页面都用到了画猜回放的功能,但每个页面都有重复代码,除此之外还包括获取 openid、微信分享等重复功能,而且由于层层迭代,画猜功能的增加,只能四处打补丁,修一处坏一处,坏一处补一处,现在也只是勉强使用。
画猜组件 2.0
广场投票 H5
广场投票画猜作品的活动 H5,总共3个页面:主页、我的作品页和分享页。改进点:在 1.0 的基础上画猜功能更完善更稳健,除此之外微信分享功能已抽取到 front-common。
痛点:几乎每个页面都用到了画猜回放的功能,但还是没有考虑把画猜回放功能抽取出去,虽然后续几乎无改动,但别的同事再次开发画猜类页面时,还得重复造轮子,除此之外,获取 openid 的逻辑 write again。
画猜组件 3.0
微信画猜分享迁移
工程迁移推动起来的项目迁移改造,其中微信画猜分享就是其中一个,这时候就要考虑组件、模块的复用性了。改进点:结合 1.0 和 2.0 的功能,顺带结合即将要开始的业务需求(你发自拍我来画活动 H5),考虑抽取画猜组件。
- 痛点:一旦开始了改革,改革的要求就变得比较严格了,虽然实现了组件化,但扩展性、可维护性、单一性还是有待提高。
总结
回过头看前3次的改进历程,每次都有进步,但这3次都有一个通病,都欠缺了一个特别重要的东西:动手之前先出方案,没有一个好的框架设计前提,写出来的代码可能处处留有雷区,不小心就爆炸,伤了自己,还伤及无辜。所以在画猜 4.0 出来之前,这篇设计文档一定要先出来。
目标
- 对画猜业务模块的熟悉
- 对业务模块的设计有自己的思考和积累
-
设计
既然画猜 3.0 已经实现了组件化,我们这次的设计差不多就是从这个组件分析入手的,下面再对画猜 3.0 更细致的分析一下它的痛点:
绘画和回放在同一个组件,有大量的 if、else 判断,扩展性较差,维护成本高;
- 某些函数违背了单一原则,耦合了太多其他逻辑,函数体积大,可读性较差,后期维护成本高;
- 所有功能集合在一个组件里,代码 600+,可读性较差,维护成本高。
分析
功能
小画板
回放
基础功能
普通画笔、橡皮擦、画布颜色、撤销、恢复和清除画布
如果不关注下面的扩展功能,其实最基本的小画板和回放就是由这 6 种功能组成的。扩展功能,只不过是在这些基础功能上增加的额外处理。扩展功能
小画板:
获取绘画数据、生成带背景颜色的图片
画笔协议
{"type": 1,"color": "#1F1F1F","linewidth": 5,"path": [[151.6667,191.6667],[131,205.6667]],"width": 750}
type:表示操作类型,1 为普通画笔,2 为橡皮擦,3 为画布颜色,4 为撤销,5 为恢复,6 为清除画布;
| 操作 | type |
|---|---|
| 普通笔画 | 1 |
| 橡皮擦 | 2 |
| 画布颜色 | 3 |
| 撤销 | 4 |
| 恢复 | 5 |
| 清除画布 | 6 |
color:type 为 1 时表示画笔颜色,type 为 3 时表示画布背景色;
linewidth:type 为 1 时表示画笔粗细,type 为 2 时表示橡皮擦大小;
path:type 为 1 和 2 时,path 里存储的坐标点,其他 type 时 为空;
width:画布宽度。
一组完整的数据就是由多个这样的协议对象随意组合而成,比如:
先画一个黑色的点,然后用橡皮擦擦一下,改变画布背景色为粉色,撤销上一笔,也就是将画布背景色改变为原来的淡黄色,恢复上一笔,将画布颜色恢复为粉色,最后清空画布。
[{"color": "#000000","linewidth": 1,"path": [[99.79106140136719,192.35011291503906]],"type": 1,"width": 375},{"color": "#000000","linewidth": 20,"path": [[118.82266998291016,189.593017578125],[121.47528839111328,196.3426513671875]],"type": 2,"width": 375},{"color": "pink","linewidth": 20,"path": [ ],"type": 3,"width": 375},{"color": "#000000","linewidth": 20,"path": [ ],"type": 4,"width": 375},{"color": "#000000","linewidth": 20,"path": [ ],"type": 5,"width": 375},{"color": "#000000","linewidth": 20,"path": [ ],"type": 6,"width": 375}]
这样的数据,有 2 个地方会用到:
1 个是小画板,执行各种操作后,会生成一份完整的数据,然后可以把数据传给服务端;
1 个是回放的时候,从服务端那里拿到数据,进行绘制。
难点
撤销、恢复
思路:
每100笔存储一张截图,其余存储具体的操作,等撤销的时候找到最近的截图加操作,绘制上去;等恢复的时候,在现有基础上,把最新一个操作绘制上去就可以了。
关键点:
存储另外一组数据,和传给服务端的数据不一样,这组数据只需要存储除去撤销、恢复以外的其他操作,当然,会比之前的又多存储一种数据,就是截图。
所以数据存储可以在一个单独的文件里进行处理,也就是 stack.js;
基础功能在一个文件里单独处理,也就是 board.js;
衍生的扩展功能和附加的逻辑处理,小画板和回放各自对应一个文件,也就是 draw.js 和 play.js;
其他比较通用的功能可以放到 utils 里面。
方案
其中依赖的底层模块如下:

这样设计的理由:
- 组件独立,便于维护和横向扩展;
模块拆分,函数功能单一化,分类管理,可以随意组合使用,也可以脱离组件直接使用,便于维护和扩展。
