为什么要有 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 有兴趣可以微博微信找我聊.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。