为什么要有 Cumulo?

刷一下存在感, 感觉 Cumulo cljs 接近一个可 Demo 的状态了
2014 开始使用 React 之后, 就对 Restful 的套路感到有疑问
React 要的是声明式地描述结构, 然后用算法自动填充其中的逻辑
Restful API 的设计类似 DOM API 的设计, 跟 React 的思路完全不同
为了在网络层达高开发效率, 高层级的抽象是少不了的

我微博上留了点记录, 还有个很早的视频, 其实套路大概是一样的
http://www.tudou.com/programs/view/EoKUKOXe1eo/
两年中间我的做了不少的试验, 也从 js 迁移到了 cljs, 变化很大
后来其实为了简单, 我继续做了简化, 方便用很少的代码就能上手
从一开始我就知道性能不行, 而且我主要的目标就是为了编码方便
方案验证可行之后, 我才会去考虑性能的问题

如果你写过大一点的网页聊天室, 要同步各种状态, 问题就会遇到
后端开发也许旧的无所谓, 但在前端会觉得比较难受
当服务器有一条数据更新, 而前端有多个 model 依赖这条数据显示

  • 常用的办法是监听 server push, 本地 model 每个位置到需要更新一遍

  • 前后端存储的数据格式有差别时, 更新的代码也会更繁琐

  • 增加新代码时比较啰嗦, 而且可能漏掉已有代码中的一些关联操作

  • 本地和远端的操作需要做区分, 在某些逻辑上要特殊处理

从方案来说旧的 Restful 有着内在的不足, 对复杂场景的抽象偏低
Cumulo 并不是为了解决全部问题, 就确实可以在部分场景简化开发

和 GraphQL 或者 Falcor 方案的区别

先声明我并没有写过这两样代码, 前者因为官方技术, 比较熟悉
后者我从 Netflix 开发人员的演讲视频了解的

我的看法是 GraphQL 不够通用, 从 View 去声明 Store 对数据流有影响
这是个依赖关系的问题, 我认为是 View 依赖 Store, 而不是反过来
Cumulo 里 Store 的结构依赖一些状态值, 而不是 View 当中的数据声明
这也是 Web 路由的做法, 用片段的字符串来决定展开的结构是怎样
Cumulo 中用 state 中的一些参数来决定 Store 怎样展开
另外相比 GraphQL 对数据库封装, Cumulo 的 Diff 就太粗暴了, 伤害性能

对 Falcor 的细节了解很少, 官方文档很复杂, 至少对我来说...
整体思路阐述得很漂亮, 就是分析一个网页需要的数据, 只抓取缺少的数据
相比 GraphQL, JSON Graph 的概念更简单一些
简聊的代码里山寨了一部分, 但我实在不了解 Falcor, 只学到皮毛
对我来说我会觉得太复杂我就先不碰了, 我没有处理复杂性问题的天分
用 Promise 强行封装请求我觉得也只是权宜之计, 问题复杂性依然在

Cumulo 往简单了说就是把前端 DOM 更新方案原模原样搬到了后端
本来 DOM 更新慢, 现在是网络慢, 我消耗服务器性能来简化网络
多个 Restful 请求就合并在一个 Diff 里了, 处理返回结果的代码也省了

数据流设计

Cumulo 的核心思想大致用下面的代码就能表达完了:

db_0 = {states: {}, users: {}, messages: {}}
# get `action` from network
db_n+1 = updater(db_n, action)

scene = renderScene(db)
store = renderStore(scene, user_id)

changes = diff(store_n, store_n+1)
# send `changes` over network
clientStore_n+1 = patch(clientStore_n, changes)

DB 中的 states 涉及到一些用户行为, 会有一些特殊性
其余的只是前端 React Store 的更新代码而已, 纯函数的代码
其中的 scene 也许会有困惑, 但这主要是未来优化性能, 可以先略过
大致上就是对应的前端 React 架构, Store, updater, render, diff/patch

以前介绍 Data Diff 比较多一点, 现在看来用 Diff 来更新很普通
Diff 方案的话把性能做好最重要, 没必要深入介绍细节了

JavaScript 方案

其实算是单向数据流的一个延伸, 好多工具链复用就好了
主要是数据的 diff/patch 和 DOM 的 diff/patch
界面直接用 React, 数据需要借助 immutable 模块

http://facebook.github.io/immutable-js/docs/
https://github.com/intelie/immutable-js-diff/
https://github.com/intelie/immutable-js-patch/

具体代码我某抽象出满意的方案, 而且转向 Clojure 后没有再深入
但可以参考下面 Clojure 版数据流代码进行自行想象...
纯函数代码只要了解参数和返回数据类型即可掌握,
WebSocket 部分比较啰嗦, 需要借助具体实现才能知道细节

ClojureScript 方案

cljs 中其实也有可复用的代码, 但我还是试着弄了
cljs 原生就是不可变数据, 用的是 deep equal, diff 过程性能不佳
我写了个 shallow equal 做 Diff, 适用性和性能有问题, 只是基本可用
而 Respo 的大致也是一样, 性能和适用场景有不小的局限

https://github.com/Cumulo/shallow-diff
https://github.com/mvc-works/respo

也照着 js 方案一样尝试对共用逻辑做了一些优化, 发现更清晰一些
Clojure 的 Atom 类型是个自带 watcher 的引用, 很实用

https://github.com/Cumulo/cumulo-client
https://github.com/Cumulo/cumulo-server

前端模块提供两个有副作用的函数 setup-socket! send!
然后在前端我只要简单地初始化, 后边监听 store-ref 即可

(defonce store-ref (atom {}))
(defn dispatch [op op-data] (send! op op-data))

(defn configs {:url "ws://localhost:4010"})
(setup-socket! store-ref configs)

后端提供 setup-server! reload-renderer! 两个带副作用的函数
其他 updater render-scene render-view 是纯函数需要自己定义
reload-renderer! 是为热替换而写的,

(defonce db-ref (atom schema/database))

(defn -main []
  (setup-server! db-ref updater render-scene render-view {:port 4010}))

(defn on-jsload []
  (reload-renderer! @db-ref updater render-scene render-view))

updater 其实就是接受一些内部生成的参数, 纯函数而已, 用于更新 DB

(defn updater [db op op-data state-id op-id op-time]
  (case op
    :state/connect (state/connect db op-data state-id op-id op-time)
    db))

具体代码可以在项目文件当中找到 Demo, 这里不展开

状态

目前属于画大饼的状态. 我开发的劲头也不足, 确实是编写边玩
周末也就整理了一下 Respo 代码, 然后更新了一下相关依赖
topic-tag 是四月份写的, 我更新到了 Respo 和 Shallow Diff 上
基本能代表目前 Cumulo 方案实际开发中的状态

https://github.com/TopixIM/topic-tag
https://github.com/TopixIM/topic-tag-server

底层其实用了 Node.js 的 ws 模块, 虽然换成 Java 也不是不可以..
但是吧, 前后端同时做热替换这种噱头用 Java 还是做不到
我也就靠热替换噱头一下了... js 方面用 Webpack 直接就能做
cljs 里略麻烦, Figwheel 前后端都能做, 也比较成熟
我的代码里用的是 boot-reload, 更简单, 但不能做服务端
总之 cljs 就是人少, 就算效率高, 实际上比不上 js 的体量

走一步看一步吧. 对 cljs 和 Cumulo 有兴趣可以微博微信找我聊.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者


引用和评论

0 条评论