Clojure China

"Virtual DOM 展开的一些有意思的探索"内容整理

#1

八月中旬我在广州举办的 React 开发者大会上做了异常分享, 幻灯片在


大致的演讲内容, 用文字再做一些回顾以及补充.

关于我

我叫题叶, 在 2014 年开始尝试使用 React, 工作一直在单页面应用方向,
2015 年年底开始尝试 ClojureScript(简写, cljs) 做个人项目的一些应用.
目前已经积累了一些 cljs 的工具链和可能用到的小工具.
主要放在 Respo, mvc-works, Memkits, Cumulo, Topix 这几个 GitHub 账号维护.

ClojureScript 开发存在哪些痛点

React 的很多特性来源于函数式编程, 比如一切皆是表达式, 比如不可变数据,
但是 JavaScript 当中并没有很好地围绕这些供特性进行设计,
所以用 js 做 Virtual DOM 的开发好优化, 某些方面就存在一些难以逾越的障碍,
一种解决方案就是切换到一种基于函数式编程特性设计的语言, 比如 OCaml, Clojure,
那么我选择了 cljs, 这个语言动态类型对于开发工具链来说比较方便一些.
http://clojure-script.org

首先 cljs 作为函数式语言存在一些学成成本, 比如 Lisp 语法, 比如不可变数据,
括号太多对写惯了大括号的开发者来说不那么方便,
社区主要是基于 Emacs, LightTable, Cursive, 以及其他一些 IDE 工具来开发的,
我的话, 开发了一套自己的方案, 用网页编辑器来帮忙生成括号:


一旦跨过了文本语法的难处, 后面就能体会到灵活的语法带来的很多方便了.

由于 cljs 语言本身内置了 immutable data, 整个开发编写逻辑的思路就很不一样,
这点需要时间适应, 我没有展开, 但是可以从教程学习. 这里放点例子.
https://clojuredocs.org/clojure.core/recur
https://clojuredocs.org/clojure.core/update-in

由于 Clojure 的主要社区是 JVM 生态上的, 在工具链上对 js 开发者相对不那么友好.
比如此前的 Lein, Boot 等等工具, 需要相当的 Java 开发背景才能很快上手,
现在这一点已经好很多, 社区有了新的编译工具 http://shadow-cljs.org
shadow-cljs 跟 Webpack 的体验相像, 具体参考我以前的文章:
https://segmentfault.com/a/1190000011499210
另一方面, Figwheel 之类的工具也在变得越来越好用:
https://github.com/bhauman/figwheel-main

Respo: Virtual DOM 类库

Respo http://respo.site 是我基于 ClojureScript 开发的 Virtual DOM 实现,
其中包含的功能相对较少, 但是比较有意思, 相对 React 有一些定制的功能,
用 Clojure 语法写界面还比较轻松, 语言本身和适合做 DSL,
这中间我加了一些自己的想法, 就是对 Dispatch 和 Mutate 做了处理.
这两个修改全局状态和局部状态的函数, 在事件发生时暴露出来, 逻辑上更加清晰:

Respo 因为只做了 DOM Diff/Patching, 就有很清晰的 diff 结果,
这个结果以 EDN 的格式展示. 这样, 也提供了一种可能性, 就是 diff 结果可以在网络传输,
这就带来一种有意思的可能, 可以再 Worker 甚至服务端做计算, 然后在浏览器主线程显示.
当然, 这个方案本身并没有什么优势, 只是说展开了一些新方案的可能性.

Respo 的状态树设计

Respo 有一个精心处理过的特性, 就是对局部状态的形态进行了设计.
React 当中局部状态指示存储在组件级别, 这个在热替换当中就需要特殊处理,
不然, 热替换发生时, 组件状态就会丢失, 开发过程调试界面就麻烦多了.
另一方面, React 社区从 cljs 社区吸收的单一全局状态的概念, 在状态这里不满足,
我认为, 我们需要的一个方案, 能同时满足全局存储状态/局部编写状态的需求.

(defonce store (atom {:states {}}))

我想到了一个方案, 就是在状态上构造跟组件层级类似的一棵树,
这样, 组件状态就能被存储到 Global Store 当中去, 热替换就好解决了,
大致给人的感觉是这样:

另一方面, 我用 Clojure 的 macro 特性, 添加了一些语法糖,
这些语法糖的作用就是把状态从根组件一层层传递下来,
以及一个 mutate! 函数的实现, 从布局发起全局状态的修改:

(cursor-> :sub-cursor comp-demo states p1 p2)
(defn on-click [state]
  (fn [e dispatch! mutate!]
    (mutate! (update state :count inc))))

实际上语法还是相对 React 啰嗦不少的, 还好实际开发当中还能接受,
关于实现的细节, 我在文档上做了一些整理:

但是总之基于这套方案, 热替换和全局状态是得到保证的.
而这些对于开发体验来说, 有着不小的提升.

实时同步的方案 Cumulo

另外我还介绍了一下我的数据同步方案, Cumulo,
这是一个借鉴了 Virtual DOM 设计的方案, 用来处理数据同步的问题,
我在服务端对数据做 Diff, 然后分发到浏览器, 因为发送的是 Diff, 数据就是很精准的.
这样可以绕过 Restful API 开发过程当中调用多个接口手动拼凑数据的问题.

而且, 在目前的单页面实时协作应用的开发当中, 数据状态之间的关系相对复杂,
我期望能像 React 一样, 梳理出一个清晰的数据流, 然后用 Diff/Patch 来填补中间的实现.

我用 cljs 基本实现了这样的方案, 目前整理在下面这些地址:


http://topix.im/

目前还有一些难以克服的问题, 主要是性能问题, 我需要做 Diff, 这对数据库不友好.
导致目前不能靠数据库有优化性能很做存储, 难以用线上, 只是个玩具项目.

但这个方案同时很适合用来做一些小的工具, 实时地分发一些数据.
它的开发体验, 在后端对数据也能做到类似 React 热替换的效果, 对调试很有帮助.
我已经用这套方案做了不少有意思的 demo, 后续也将会做更多的改进和尝试.

小结

我觉得 two-day binding 和 Virtual DOM 的方案给前端的界面开发带来了很多的启迪.
特别是 Virtual DOM, 让以往函数式编程的研究的成果在前端有了更多运用的场合.
而这些, 特别能够在压缩开发成本改善开发体验方面带来帮助.
Respo 和 Cumulo 是我在学习和使用 ClojureScript 当中积累的思考和整理的经验,
这些也将能够在 React 开发当中, 出于技术栈的相似, 带来一些思考和帮助.