从React到Domcom: 一个提供DOM部件的web框架
设计背景
ReactJS给前端Web应用开发的思路带来巨大转变,颠覆了很多以前的观念。我有多次机会接触和学习ReactJS,在理解它先进的理念的同时也发现它存在一些问题,最初主要集中在性能方面,比如重复生成部件的虚拟dom,整体性计算Diff和进行patch,更新检测机制不够完善等。我的思路是用一种方法标识所有Dom节点特性以及其它所有部件特性的有效性和可变性。首先想到的用普通的值和特殊的对象来加以区别,后来发现用响应函数是最合适的手段。响应函数的采用使我得以进一步改善部件的更新检测机制。后来我发现,相比于性能,不断演进后得到的这些设计决策给数据管理和应用设计带来的提升和便利更为明显和重要。最终Domcom超出预期地实现了我所有的设计目标,不但避免了影响ReactJS性能的基本因素,也同时弥补了ReactJS整体架构影响应用设计和实现复杂性的一些明显缺陷。
Domcom整体特性
Domcom是DOM和Component合并后的缩写,宗旨是为开发Web应用提供DOM部件。通过Domcom,可以整体上改善数据管理,尽可能减少不必要的DOM操作,提升程序运行效率。Domcom提供的部件是声明式和响应式的,充分运用函数范式和对象范式两种程序设计风格,提高代码复用,简化设计。以下是本框架主要特性的一个列表:
-
基于响应函数的声明式部件
任何dom特性以及部件的其它合适属性都可以是响应函数。
-
最小化Dom访问和更新
Domcom预先通过部件声明和描述了整个Dom,web应用绝大部分时间不需要访问Dom特性和状态。更新的时候应用能跳过有效的部件,只处理当前失效的部件和特性。
-
便于组合扩展的部件
Domcom可以利用函数范式组合生成部件和设置传递参数,也可以利用对象范式通过继承机制定义新部件。这能提高web应用的代码复用,降低复杂度,令设计更为简单清晰。
-
最大化解耦模型和控制器
Domcom作为Dom部件的提供者,,在MVC或者MVV*模式专注于解决视图的问题。对模型和控制器完全保持中立和开放的视角。普通的值、变量和响应函数成为通往数据的桥梁和管道。借助这种方法,domcom最大化解耦视图于模型和控制器,给实现带来便利,简化设计。利用Domcom,很多时候我们甚至不需要为Model或者Controller作特意的设计。根据应用的复杂度和相关需求,Domcom当然也可以与POJO, 事件、observable, 基于类的扩展、Flux, immutable等不同解决方案联合使用,甚至可能借用backbone.js, ember.js, react.js等现有框架或库中的相关组件。
-
不向Dom附加任何框架性元素和特性
Domcom没有针对框架目的在Dom中设置元素id,class或其它辅助特性;也不需要为组合部件添加父元素或者任何其它辅助元素。利用Domcom可以得到一个最小最清洁的Dom。
-
简单强大的路由部件
Domcom自带路由部件,实现非常简单,然而提供了强大的功能,支持正则表达式、通配符,允许多层次、多点嵌入的参数化路由。
-
方便的Promise支持
除了通常的Promise使用方式以外,Domcom的特性值和子部件可以直接是Promise,还另外提供了便利的Defer部件,更方便于使用Promise。
-
兼容各种浏览器,甚至IE 6, 7, 8
框架的设计特点使得自然而然具有良好的兼容性。虽然浏览器兼容性不是Domcom最初的设计目标,但是因为它管理Dom的模式使得它只需用到很少的浏览器及Dom相关的API,刚实现完毕框架即已经自然支持IE9及其它各大主流浏览器,继而用了很少时间就通过修改实现将支持扩展到了IE 6, 7, 8。
-
无需依赖,无需不可变数据结构,无需浏览器或语言补丁,无需搭配程序库,无需补充解决方案
Domcom自身具有比较适度的代码规模,当前最小化大约60K+。因为框架在更新检测以及数据传递上采用的机制,使得它可以更灵活地使用数据,没有使用不可变数据结构的硬性需求。框架的实现代码不涉及任何非主流的、尖端的浏览器或javascript语言特性。这些方面使得Domcom无需依赖,无需浏览器或语言补丁,无需搭配程序库,无需补充解决方案即可以解决应用程序的通常需求。因为Domcom已经全面彻底地代理Dom,连angular那种通过指令直接操作Dom的需求都已经消失,因此即使是jQuery这样的库都变得不再必要。、
-
不需要模板语言
Domcom设计的中尽量方便直接使用Coffee-script语言和javascript语言。用Javascript语言的代码已经非常简练可读,还可以用Coffee-script达到更好的效果,基本上能媲美Jade模板语言而灵活性更强。因此Domcom不存在多强的模板语言需求。当然,要为之加上适当的模板语言也是很可行的。如果有人能完成这项工作,我非常欢迎并期待合作。我个人更倾向于缩进风格而不是类似JSX那种XML风格的模板语言。考虑到不同用户的习惯,如果能够两种并存就更好了。
链接、文档和下载
可以用npm下载安装
npm install --save domcom
或者使用cdn
http://cdn.jsdelivr.net/domcom/0.1.1/domcom.min.js
文档
Domcom已经提供全面的文档(特别是中文文档),都在doc/文件夹。中文文档集中在doc/Chinese文件夹:
介绍和辅导教程: Domcom的基本介绍和入门的辅导教程。
概念和原理:了解Domcom有关的基本概念和原理。
API参考:关于Domcom所有公开的API的正式而详细的参考资料。
速查表:熟悉Domcom的API,常用技巧和惯用法。
常问问题:大家经常想了解的一些关于Domcom的问题。
doc/文件夹还有更多的文档内容。
社区
QQ群:DomcomJS社区 371409830
Google groups: Domcom
说明
本框架从2015年1月产生思路,4月份开始开发,其间经历了多次迭代,核心代码进行了多次很大的重构,当前实现合理,简明、优化,具备丰富的测试。今年上半年我的一个项目已经彻底弃用jQuery和Angular转向Domcom,有良好的体验。欢迎参与Domcom项目,共建社区,让Web开发更轻松,开发出更多改变世界的应用。
挺好,就是状态组装部分没有太多说明
有没有全局数据的概念, 也大致是单向数据流的概念?
关于单向数据流,Domcom并不特别强调。跟Immutable Data一样,其实并不是大多数应用的普遍需求。但是这个问题在ReactJS特别明显。因为ReactJS的更新依赖于props的变化,即如果上层传入本部件的props如果有改变,那么ReactJS据此认为本部件需要重新render,所以Immutable Data 某种程度上成为框架的强制需求,从而导致产生了某种必须的配套解决方案,如flux,reflux, redux, immutable.js。当然,我并不否认,immutable data还是有某些好处的。但是,我认为把它变成一种强制的需求,然后将它包装一种普遍化的解决方案,作为绝对化的优点来宣传,我是不认同的。而React社区关于单向数据流的论断,以及那些针对angular和双向绑定的那些拓扑图,我是存疑问的,有必要从不同角度不同思路对这些东西多加审视。同样,我也并不绝对否认这种解决方案有其适用的场景,也不完全排斥flux,reflux,redux等这些方案的作用。比如redux,如果应用确实有需求,Domcom是可以合它配合得很好的。
模块私有状态的数据存放在哪里?
可以封装在函数里面或者扩展部件里面,完全对外界隐藏。Domcom的更新检测是从下往上传递,和React正好相反。因此,Domcom也下层部件部件状态是可以自由改变的,鼓励这种局部的改变。
DOM 内部实现是 Virtual DOM 还是 Angular 之类的数据绑定?
更多是受virtual dom启发,是对virtual dom这种方案的发展。只有指令的实现可以认为是直接借鉴了angularJS,但指令实质也就是个简单的部件变换函数。
按照我写 React 的经验, JSX 语法小问题, Virtual DOM 性能也有优化的余地,然而 Immutable Data 确实是对于响应式编程来说非常重要的工具
重复一下,我认为某种意义上现在很多基于immutable data的解决方案只是围绕以前的virual dom的补充解决,与其更新检测,diff,patch过程密切相关。Domcom不排斥,但是也绝不鼓励immutable js,主张对此应该从问题需求而非框架需求进行考量。
再加一个问题, 是 Dao 语言的作者吗?
谢谢。我就是。你很细致哦。欢迎多交流。
@i5ting, 状态组装很简单,不变的状态用非函数的普通值传递,动态可变的就用函数。如果希望避免不必要的部件更新(不等于Dom刷新),就将函数转化为响应函数,尽量具体化其响应依赖(利用flow 这个模块中的工具)。Dom node的特性,If,Case, Cond部件的test域,Each的items等等都可以是响应函数(只要它们适合成为响应函数就可以是响应函数)。响应依赖主要是个优化工具。可以说,如果把所有与响应函数有关的代码拿掉,Domcom也是一样工作的,只是部件更新时的计算量会大一些,甚至Dom Node刷新也不见得会增加很多。
状态、数据传递通过普通的函数参数,类成员传递就可以。除了响应函数配合更新检测辅助优化以外,没有其它特别的机制。不需要重新把数据和状态嵌入到scope层次(angular), props层次以及states(reactJS),也不需要使用特定的类继承(Ember, Backbone等)。
用心了, 第一次听到 响应函数
和 强制响应函数
的概念.
mark
mark
看到源码的那一刻,就认定这个只能拿来当玩具而已
@joney-pinkman: 谢谢你的关注。很高兴第一次听到负面的评价。如果你能给出一些有说服力的论证和具体的比较,我会更加高兴。历经长时间开发,我感觉我已经基本在我的思路上做到位了,甚至超出了我最初开始这个项目的预期。如果有人能够以不同的视角和思路来审视它,提出问题,发现bug,这对domcom这个产品来说是最大的促进。欢迎在github上提出issues,提出pull request。 https://github.com/taijiweb/domcom https://github.com/taijiweb/domcom/issues
C.A.R. Hoare说,软件设计有两种方式:一种方式是,使软件很简单,明显没有缺陷;另一种方式是,使软件很复杂,没有明显的缺陷。 我想重构一下这句话:软件设计有两种,一种是简单到很难出现缺陷,一种是复杂到很难发现缺陷。 和angularjs和reactjs这样的框架相比,我认为domcom更加接近于前一种。当然,是否真正很难出现缺陷也许是我的个人判断。但是,不管在实现上还是使用上,domcom更简单这一点确实是客观的事实。因为它更简单,我想如果有缺陷,也会更容易发现。
看了一下相关的文档我觉得有一点react做的很好,组件之间可以随意组合,以html为骨架,辅以事件,flux,虽然也是抽象了组件的概念,但是至少组件内部的结构还是可以一目了然的,html和js的混合编写,更符合前端开发的所写所得的方式。domcom完全抽象了组件的概念,代码量会更少,但是代码的可读性,学习的难度会上升。建议还是不要完全脱离html的语法吧。
@songyunze:谢谢你提出的意见。
确实,关于支持xml风格的模板语言,你说的很有道理,xml风格应该是更加贴近很多前端开发者的习惯,特别是经常用html,要写大量静态网页的开发者。比如jade和ejs这两种模板语言,使用的人就都各有所好,见仁见智。我也非常期望有人能开发出为domcom开发出类似JSX风格的模板语言。作为js开发者,在开发全动态页面的时候,我个人还是更喜欢接近代码语言的api。
至于学习难度方面,我个人认为domcom应该是更胜一筹的。我可以从具体的方面给出很多的理由。最主要的一点是domcom只有一个概念是独有的,就是用函数做为声明特性的手段,也就是所谓响应函数。除此以外都是普通的js,再无其它特异之处。当然,学习难度这个东西和每个人的技术背景以及喜好密切相关,如果用一个新东西和已经很熟悉的旧的技术比, 难免会带点主观的色彩。我作为domcom的设计者,在这方面做的结论当然更加是主观的。实际如何总是要看大多数学习者的感觉。
关于组合性,我认为这是domcom的强点。不管是用普通的函数调用,或者用对象来组合,或者基于类的继承,都可以很方便地组合或扩展新的组件,比起rangularJS系或者ReactJS都要简单灵活很多。
@jiyinyiyong : 你提到的Om和elm我都有所了解。Halogen则第一次知道。 domcom受到virtual dom很多启发,但是实现上还是有不同。最大的不同是不必在render的时候重复generate,diff和patch,而是用响应函数自动标示失效的部分。从原理上孰优孰劣应该是不难分辨的。
对于Immutable, 我并不否认有其某方面的优势。然而,我认为是否使用immutable,更多应该从问题域出发,而不是作为框架的强制需求。另外,由于Domcom和模型以及控制器解耦得更彻底,因此,绝不会对某种模型或者控制器的解决方案有无法协作的情形。我认为反而会更容易适配各种方案,也包括immutable和flu在内。
todoMVC: https://github.com/taijiweb/domcom/tree/master/demo/todomvc git clone后打开demo/index-dist.html即可运行该应用。
虽然实现了todomvc.com要求的功能和界面,但是目前没时间添加到todomvc.com,还有些额外的工作要做。如果有谁能帮忙提交一下就好了。
Domcom的实现方案其实很简单:对于Tag节点特性,如果是普通值,创建后以后刷新就不必再更新了。对于普通函数,会被转换成强制响应函数,每次更新都会让这个特性再次无效,而其它响应函数,只有在其它场合调用了该响应函数的invalidate后,才会让这个特性无效。这样,在更新Tag部件的时候,就自动知道哪些特性是需要重新计算的。计算的新值会和缓冲值比较再决定是否刷新dom。 至于node节点是否需要替换,每个部件自己都是知道的。比如If部件,只有它的test域变了才会要替换内容部件。不象现在主流的算法,需要一个复杂耗时,同时还有很多其它限制的reconciliation算法来处理。比如React,如果两个render的时候生成的下层部件做了一些违法规则(不单纯修改props,不用setState之类)的事,有的时候就会影响性能,甚至可能发生不正确的更新。
@jiyinyiyong:domcom有将整个update由系统进行全局化管理的机制,可以利用renderAnimationFrame方案或者它的polyfill,api很简单(dc.renderWhen或dc.updateWhen)。用户也可以手动管理update,通过调用component.render(), coponent.update(), component.renderWhen(…), dc.updateWhen(…)等方法。virtual dom的优点是可以减少reflow和redraw,这一点在domcom也可以得到最大的发挥。在做domcom之前我研究过fastdom,那个库是专门针对reflow的。fastdom的问题是如果应用复杂了还是很难使用。domcom则系统化地彻底解决了这个问题。
@jiyinyiyong: https://github.com/wilsonpage/fastdom https://mattandre.ws/2014/05/really-fixing-layout-thrashing/ 作者是ft.com的开发人员。fastdom是他在ft做项目时提炼出来的。
@jiyinyiyong: 你说得很对。在domcom中,用户几乎可以完全不用管dom。基本完全杜绝读dom节点的特性。唯一需要读dom节点特性的时机我能想到的大概就只有在类似input的onchange一类指令中需要读node.value以反向更新数据。其它一切值都可以直接用部件的缓存数据。这是virtual dom比单纯数据绑定、响应式编程以及类angular框架强的地方。至少它们的指令还需要直接操作dom,所以有时候还离不开jq。domcom虽然有指令,其实语义已经不同于ng的指令,dc指令是方便管理部件而不是用来操作dom的。