背景

画猜类业务更新得并不频繁,但实现过的人才知道其中的辛酸苦辣,从一开始的囫囵吞枣,快速业务开发,到现在的考虑画猜组件设计,已经实现了很大的跨越。也正是因为踩过坑,才知道架构和设计的重要性。

画猜组件 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 出来之前,这篇设计文档一定要先出来。

    目标

  1. 对画猜业务模块的熟悉
  2. 对业务模块的设计有自己的思考和积累
  3. 未来要沉淀出玩吧特色的业务设计结构

    设计

    既然画猜 3.0 已经实现了组件化,我们这次的设计差不多就是从这个组件分析入手的,下面再对画猜 3.0 更细致的分析一下它的痛点:

  4. 绘画和回放在同一个组件,有大量的 if、else 判断,扩展性较差,维护成本高;

  5. 某些函数违背了单一原则,耦合了太多其他逻辑,函数体积大,可读性较差,后期维护成本高;
  6. 所有功能集合在一个组件里,代码 600+,可读性较差,维护成本高。

    分析

    功能

    小画板
    image.png
    回放
    image.png

    基础功能

    普通画笔、橡皮擦、画布颜色、撤销、恢复和清除画布
    如果不关注下面的扩展功能,其实最基本的小画板和回放就是由这 6 种功能组成的。扩展功能,只不过是在这些基础功能上增加的额外处理。

    扩展功能

    小画板:
    获取绘画数据、生成带背景颜色的图片

回放:
播放/暂停、继续播放、进度条、改变播放速度

画笔协议

  1. {
  2. "type": 1,
  3. "color": "#1F1F1F",
  4. "linewidth": 5,
  5. "path": [
  6. [151.6667,191.6667],
  7. [131,205.6667]
  8. ],
  9. "width": 750
  10. }

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:画布宽度。

一组完整的数据就是由多个这样的协议对象随意组合而成,比如:
先画一个黑色的点,然后用橡皮擦擦一下,改变画布背景色为粉色,撤销上一笔,也就是将画布背景色改变为原来的淡黄色,恢复上一笔,将画布颜色恢复为粉色,最后清空画布。

  1. [
  2. {
  3. "color": "#000000",
  4. "linewidth": 1,
  5. "path": [
  6. [
  7. 99.79106140136719,
  8. 192.35011291503906
  9. ]
  10. ],
  11. "type": 1,
  12. "width": 375
  13. },
  14. {
  15. "color": "#000000",
  16. "linewidth": 20,
  17. "path": [
  18. [
  19. 118.82266998291016,
  20. 189.593017578125
  21. ],
  22. [
  23. 121.47528839111328,
  24. 196.3426513671875
  25. ]
  26. ],
  27. "type": 2,
  28. "width": 375
  29. },
  30. {
  31. "color": "pink",
  32. "linewidth": 20,
  33. "path": [ ],
  34. "type": 3,
  35. "width": 375
  36. },
  37. {
  38. "color": "#000000",
  39. "linewidth": 20,
  40. "path": [ ],
  41. "type": 4,
  42. "width": 375
  43. },
  44. {
  45. "color": "#000000",
  46. "linewidth": 20,
  47. "path": [ ],
  48. "type": 5,
  49. "width": 375
  50. },
  51. {
  52. "color": "#000000",
  53. "linewidth": 20,
  54. "path": [ ],
  55. "type": 6,
  56. "width": 375
  57. }
  58. ]

这样的数据,有 2 个地方会用到:
1 个是小画板,执行各种操作后,会生成一份完整的数据,然后可以把数据传给服务端;
1 个是回放的时候,从服务端那里拿到数据,进行绘制。

难点

撤销、恢复

思路:
每100笔存储一张截图,其余存储具体的操作,等撤销的时候找到最近的截图加操作,绘制上去;等恢复的时候,在现有基础上,把最新一个操作绘制上去就可以了。

关键点:
存储另外一组数据,和传给服务端的数据不一样,这组数据只需要存储除去撤销、恢复以外的其他操作,当然,会比之前的又多存储一种数据,就是截图。

所以数据存储可以在一个单独的文件里进行处理,也就是 stack.js;
基础功能在一个文件里单独处理,也就是 board.js;

衍生的扩展功能和附加的逻辑处理,小画板和回放各自对应一个文件,也就是 draw.js 和 play.js;
其他比较通用的功能可以放到 utils 里面。

方案

其中依赖的底层模块如下:
image.png
image.png

这样设计的理由:

  1. 组件独立,便于维护和横向扩展;
  2. 模块拆分,函数功能单一化,分类管理,可以随意组合使用,也可以脱离组件直接使用,便于维护和扩展。