# Domcom  

提供DOM部件的web框架

github.com/taijiweb/domcom


## 设计背景
  
  React无疑要算当前最流行的前端开发框架之一
###React的优势
React能成就今天的地位
    
* 组件化设计思想带来的强大力量。      
* 虚拟Dom的更新机制带来的运行效率的提升。

###React的不足
虽然React现在非常盛行，但是它也暴露出自己的弱点。主要有三个方面

* **数据管理问题**  
  因为React刷新实现机制倾向于部件数据的从上至下逐层比较，数据的自上至下逐层传递。更坏的是这种实现机制上弱点的竟然带偏并绑架了人们的开发设计观念，鼓吹什么全局store。单向数据流，偏离了更合理的传统软件开发观念，比如抽象、封装、信息隐藏，数据局部化等等，由此产生了Flux、reflux、redux、redux-thunk, saga, redux-observer, dva等等一个又一个轮子层出不穷，形成了蔚为大观的react全家桶，由于观念上的根本失误，这些新轮子的发明并没有给前端开发带来明显的便利，知识导致了学习曲线的提升和开发效率的降低（因为一项任务被分解到多个小轮子管辖的条条块块，经常为了完成一项业务需要改动七八个文件、跟踪若干层不同位置的函数代码）。
* **JSX语法**  
  因为React创建元素的API很繁琐，又为了照顾网页开发的传统，由此发明了JSX这种伪装的混杂xml的js代码。不得不说这两者混合在一起确实蛮丑的。互相混嵌的代码层次多了难免会出现语法错误。有的时候就会给新手平添很多麻烦。即使有经验的人员也会有因此影响开发效率的时刻。
* **React element与React component instance的区别**     
  React element不是普通的部件实例，因此很难当作普通的对象实例来管理，这也给开发带来某些便。比如整个React社区至今实现的对话框都没有出现当年桌面开发环境下那么使用方便的对话框部件。只需要dlg= dialog(); dlg.open(); dlg.close()即可完成任务的类似机制。我将这类部件称为自治部件。
  第一个问题与第二、第三个问题是相关联的。可以说第一个问题更多的第二第三个问题导致的自然结果。


## Domcom整体特性

  DomcomDOM和Component合并后的缩写，宗旨是为开发Web应用提供DOM部件，Domcom是针对上述问题给出的一个根本的解决方案。每个Domcom部件最终会由一个ReactProxy(一个React Component）代理。因此具有React部件的一切优点。同时Domcom部件所做的工作使得可以任意地传递数据和控制部件的挂载、更新和卸妆（可以方便自主的调用comp.mount()，comp.update()，comp.unmount())。
Domcom具有以下特性：

* 建基于React

  Domcom借助于React，因此兼具React组件化、虚拟Dom两大优点。

* 简明轻巧的API

  在React之上，秉承jQuery的传统，domcom设计了一套简明轻巧的API，数量少，易学易记，学习代价极低。比如Domcom无需定义类，只需调用dc({view, ...})即可定义部件。部件自动watch有关数据。方法如component.mount(),component.update(),component.unmount(),等都通俗易懂，顾名思义，不象React需要定义类，而且倾向于冗长的方法名。

* 用嵌套数组描述React元素结构。

  Domcom用js嵌套数组描述React元素结构，比如象这样[ClassA, {propss...},['div.class...#id##style...',{props...}, 'some text children, ...otherChildren]。这种方式易于阅读，输入便利，而且无需babel，减少了工具依赖，只需在script标签中引入React、ReactDom、Domcom的代码库，以及app本身的代码，页面即可以开始工作，开发更轻松了。 

* 最大化解耦模型和控制器

  类似React，Domcom作为Dom部件的提供者，，在MVC或者MVV*模式专注于解决视图的问题。但是超越React，Domcom对模型和控制器完全保持中立和开放的视角，因为Domcom突破了React的更新机制的限制，从而避免了它所带来的问题。  
  在Domcom中，任何普通的值、变量和函数都可以成为数据通往视图的桥梁和管道。借助这种方法，domcom最大化解耦视图与模型和控制器，给实现带来便利，简化设计。利用Domcom，很多时候我们甚至不需要为Model或者Controller作特意的设计。根据应用的复杂度和相关需求，Domcom当然也可以与POJO, 事件、observable, 基于类的扩展、Flux， immutable等不同解决方案联合使用，甚至可能借用backbone.js, ember.js， react.js等现有框架或库中的相关组件。



## 获取和使用Domcom

### 获取Domcom
  npm install --save domcom

  git clone： [github domcom](https://www.github.com/taijiweb/domcom)

  下载github发布版本： [Github releases](https://github.com/taijiweb/domcom/releases)

  使用cdn: 感谢`unpkg.com`和`cdn.jsdelivr.net`提供cdn链接：
  
        https://unpkg.com/domcom@0.8.4/dist/domcom.js
        https://unpkg.com/domcom@0.8.4/dist/domcom.min.js
        https://cdn.jsdelivr.net/npm/domcom@0.8.4/dist/domcom.js
        https://cdn.jsdelivr.net/npm/domcom@0.8.4/dist/domcom.min.js
  
https://www.jsdelivr.com/package/npm/domcom.

### 在页面中设置Domcom

根据开发和应用需要从安装或下载的文件夹中选择domcom/dist/下的domcom.js, domcom.min.js, domcom-basic.js， domcom-basic.min.js中合适的文件，按照通常方法向html页面添加script标签：

`<script src="path/to/domcom-???.???.js"/>`

在domcom的script标签之后添加自己的js脚本：

`<script src="path/to/my-app.js"/>`

## Domcom入门

  Domcom采用Coffee-script设计实现，转译成为javascript。Domcom鼓励使用Coffee-script, 但是并不妨碍采用javascript来编写基于Domcom的应用，相对而言代码会稍长一点，可读性稍差一点。这对大多数习惯于编写javascript原生代码的程序员来说，是完全可行的，没有不好之处。Domcom针对Coffee-script做了很多便于阅读和编写的设计，因此domcom应用的代码非常简洁可读。

  以下实例采用Coffee-script语言。Coffee-script和Javascript基本是一一对应的。即使不熟悉Coffee-script的朋友，我建议也先阅读一遍接下来的内容，有疑问的时候按照直觉理解就好。
  为便于理解下面的代码，这里提一下几个知识点，：CoffeeScript中用缩进表示语法块；从不需要声明变量，直接赋值就好；-> 或 => 表示函数定义。
  
### Hello, Domcom

     view = ['div', "Hello, Domcom"]
	 comp = dc({view})  
	 comp.mount()

  `Component.mount`方法的作用是挂载部件。执行完这段代码页面将会显示Hello Domcom。

### 在config中使用data字段和使用view函数
 
	
    data = {framework:"Angular"}
	 view = (data) ->
	    [‘div’，{}, "Hello, ", data.framework]
    comp = dc({data, view})
    comp.mount('#demo')
    data.framework = 'Domcom' //(*)

  本例借助config的data字段向view函数传递数据。`comp.mount('#demo')`表示部件会被挂载到id=demo的元素之下。上一例中因为没有mount调用没有给定挂载元素会直接挂载到document.body下。因为部件挂载后就会开始watch部件自身被config提供的字段，并watch 部件data上已有的字段，因此上述代码(*)所做的一行执行后页面就会字段更新。


### React事件处理

  domcom有很好的dom事件处理机制。可以直接在构造部件的时候将事件处理函数作为属性传递给Tag部件。基本的记法是`{..., onsomeevent:eventHandler,...}`。

	
	view = ['div', {onClick: -> alert("Hello, Domcom!")}, "Say hello!"]
	comp = dc({view})
	comp.mount('#demo')

  本例为div部件添加了onClick事件。在页面上点击`Say hello!`，将会弹出`Hello, Domcom!`

### React事件与部件更新

  在React事件中，应用可以改变数据，也可以调用部件的重绘或更新方法以更新部件和刷新Dom。
  
    onChange = (event)-> 
        data.who = event.target.value
        comp.update()
	view = (data) ->
	   ['div',
        ['text' { value: data.who, onChange}, //(1)
        ['div', "Hello, ", data.who]]
	comp.mount()
	comp.on('mounted', -> comp.stopWatch()) //(2)

  我们已经看到，Domcom的view中用[tag，{props...}, children...]这样的记法表示React元素。为了方便Domcom还提供了某些更易于书写的助记法。比如对input标签的输入类型如text，number，password等等可以写成[type,{props}]的形式。因此上述(1)行等同于`['input', {type:'text', value: data.who, onChange}`

  前面提到部件mount后会开始自动watch，这是可以关闭的。上面代码(2)所在行就是这样做的。其中‘mount'事件是在与Domcom部件`comp`对应的`ReactProxy`部件的`componentDidMount`生命周期方法中发出来的。

  执行本例页面将显示一个文本输入部件，输入完毕不同的文本, 比如`Domcom`，将触发文本输入部件的`onchange`事件，从而改变data.who，通过调用`comp.update()`，div的内容将同步改变为`Hello, Domcom`。

### Domcom指令
  
  出于方便，Domcom设计了指令。使用指令的方法是： `[someTag, { ... $directiveName: options, ...}`。Tag部件把该项以及options传给指令处理函数，并且指令处理函数的this会被绑定为部件本身。该指令处理函数对该该项做某些处理后再返回一个新项作为结果。

  Domcom提供了一组内置指令，包括$model, $options, $show等。我们可以在应用中使用这组指令，也可以参考这组指令的写法定义新的指令。

    view = ->
       [ ['text' {$model: 'who'}],
         ['p', this.who]]
    comp = dc({view})
	comp.mount()
    setTimeout((->
	  comp.who = 'Domcom'
	  comp.update()), 2000)

  $model指令可以用于所有的input元素，select，textarea。本例执行两秒后， 我们将能看到页面内容的自动变化，我们在文本框中输入的内容会引起p标签的变化。

### 双向绑定

  双向绑定是应用中一个常见的需求。前面“React事件与部件更新”小节就是一个典型场景。从“Domcom指令”小节我们可以看到双向绑定指令$model对代码的简化。  
  $model指令可以看作两个声明的结合，`{$model: field}`相当于`{value: comp[field], onChange: (event) -> comp[field] = event.target.value}`, 这里`comp`代表部件实例，`model`代表部件上的字段名。
  Domcom总是倾向于使代码更简洁。因为input标签是不能有children元素的，因此，在domcom中`['text' {$model: 'who'}]`可以进一步简写为`['text' 'who']`,更方便读写了。
  
### 使用React部件
在Domcom中当然也可以使用React已经实现的部件比如象下面的代码：

       import Button from '@material-ui/core/Button'
       view =  ['div'
        [Button, {variant:"contained", color:"primary"},'primary'],
        [Button, 'Default'],
        [Button, {variant:"dashed", color:"secondary", disabled:true},'secondary'],
        [Button, {variant:"danger", color:"danger"},'danger']
       ]
       comp = dc {view}
       comp.mount('#demo')   
           
再来一段更复杂一些的
    
    Transition = (props) ->
      React.createElement(Slide, {direction:"up", props...})
    data =
      emails: ['x1@y.z', 'x2@y.z', 'x3@y.z'],
      open:true,
      handleListItemClick: (message)->
        alert 'handleListItemClick: ' + message
        data.open = false
        dc.update()
    
      handleClickOpen: ->
        data.open = true
        dc.update()
    
      close: ->
        data.open = false
        dc.update()

    view = (data) ->
      ['div',
        [Button, {onClick:data.handleClickOpen}, "Open full-screen dialog"],
          [Dialog, {fullScreen:true, open:data.open, onClose:data.close, TransitionComponent:Transition},
            [AppBar, '.appBar',
              [Toolbar,
                [IconButton, {color:"inherit", onClick:data.close, 'aria-label':"Close"}, [CloseIcon]],
                [Typography, {variant:"h6", color:"inherit", className:'.flex'}, 'Sound']
                [Button, {color:"inherit",  onClick:data.close}, "save"]
              ]],
            [List,
              [ListItem, {button:true},
                [ListItemText, {primary:"Phone ringtone", secondary:"Titania"}]],
              [Divider]
              [ListItem, {button:true},
                [ListItemText, {primary:"Default notification ringtone", secondary:"Tethys"}]]]]]
    comp = dc({data, view})
    comp.mount('#demo')

### Domcom部件组合与嵌套
以下是从Domcom中摘录的一段测试代码，演示了Domcom中组合嵌套部件的方法  

      data = {show1:true, message1:"I am embedded 1", message2:"I am embedded 2"}
      view = (data) ->
        if data.show1
          ['div', data.message1]
        else
          ['div', data.message2]
      embedded = dc({data, view})
      embedded2 = embedded.copy().watch()
      comp = dc({view:['div', embedded, embedded2]})
      comp.mount('#demo')
      expect(embedded.node.innerHTML).to.equal 'I am embedded 1'
      expect(comp.node.innerHTML).to.equal '<div>I am embedded 1</div><div>I am embedded 1</div>'
      data.show1 = false
      expect(comp.node.innerHTML).to.equal '<div>I am embedded 2</div><div>I am embedded 2</div>'

### 更多例子

  `domcom/demo/`文件夹提供了更多的[演示示例](https://github.com/taijiweb/domcom/tree/master/demo)。在`domcom/`文件夹运行`gulp`命令行，然后打开`domcom/demo/index.html`(必须要有本地服务器，可以在webstorm打开，或者使用`python -m SimpleHTTPServer 8888`）,可以运行这些示例。

### todoMVC

  [`domcom/demo/todoMVC`文件夹](https://github.com/taijiweb/domcom/tree/master/demo/todomvc)提供了一个todoMVC(http://todomvc.com)的实现。在`domcom/`文件夹进入命令行运行`gulp`命令，然后打开`domcom/demo/todomvc/index.html`(必须要有本地服务器，可以在webstorm打开，或者使用`python -m SimpleHTTPServer 8888`），可以运行这个应用。

## 与其它框架的比较

### 更新检测

  angularjs使用脏值检测触发watcher的机制，当$apply某一个scope上的变化时，会反复运行该scope及以下各个层次scope上的watcher，有的时候因为持续的变化会导致watcher在一个$digest循环中执行多遍，甚至因为超过限制次数而触发infdig异常。infdig就象是马戏团的一个小丑，经常性地冒出来向我们提示angularjs存在这样一个不得已的补救措施，同时也表明整体设计上的某种缺憾，令人感觉十分奇怪和不爽。另外，angularjs这种方法还是需要使用者在directive和watcher中进行dom操作，并不能从根本上杜绝不必要的的DOM访问和dom更新。

  reactjs采用缓存和差异比较的方式，如果需要定制某个部件的刷新，普通办法是覆盖shouldComponentUpdate方法。如果props在重新绘制的时候没有发生变化，默认不更新下层部件。这和javascript的基于对象以及普遍运用副作用的的语言特性有些冲突。由此提出了flux的解决方案，并冒出了很多的扩展程序库，如reflux, redux, immutable.js等。我认为这些库从某种意义上是一种补救性的措施，反应了react.js某种设计上的不足。

  domcom并不以追求数据的完全彻底自动watch作为系统设计模板，而是提供component.update方法作为主要更新手段，watch也只不过是系统在被watch对象的property字段的set函数中调用了此方法而已。这样既不会产生angularJS中的无限循环或多次循环的问题，也不需要限制响应函数值的可变性，更加灵活，也更加高效。上述flux／reflux/redux等用于补救react.js的诸多程序库在domcom中直接没有多大价值，而domcom的应用自然而然地可以得到简化，结构也显得更清晰。而整体的代码量也可以大为减少，减少传输javascript脚本的网络流量。

### 与模型和控制器的解耦

  knockout.js, ember.js, backbone.js等采用定制对象作为model或者controler。这种方法对编码的影响和限制更为明显。当前的主流框架更少采用这种方法。

  angular.js的模型数据可以是普通的javascript对象，但是必须挂到各级scope下。controller必须用angular的方法定制。scope、controller都构成某种层次关系。

  react.js必须将模型数据从根部件通过this.props从上层到下层进行传递，对于动态数据要求用this.setState方法进行设置, 否则可能严重影响react.js的更新逻辑。react.js没有专门的controller的概念，但是习惯上controller方法都必须附属于部件类。

  domcom中的各种元素只是普通的常量，变量或者响应函数，除此以外，对于模型或者控制器完全保持中立和开放的态度。可以看到，domcom的解耦程度是最高的。虽然从习惯上domcom都将数据通过config对象对接到部件本身以供view函数使用，但是这并不是强制的要求。免除了上述框架带来的限制，domcom的api设计方式也使得view函数可以非常方便自如自然地使用任何来源的数据。应用可以根据问题域本身的需求任意组织数据，而不用感觉到框架的强制，这将极大地促进整个系统的架构设计，我们从domcom自身的[todomvc演示范例](https://github.com/taijiweb/domcom/blob/master/demo/coffee/todomvc.coffee)就能感受到这一点。

### 组合和扩展能力

  angularjs 1.x 具有一组复杂的概念框架，一方面提升了学习曲线，另一方面也阻碍部件之间的组合。要实现controller，directive的组合、继承和扩展，要采用很多不平常的手法，克服很多技术障碍，经常令人望而生畏，妨碍着这些实践的普及。

  react.js的组合性有所改善，但是因为一方面必须将数据组织在props和state层次下，另一方面每个部件都必须要实现render方法，同时React通常都必须以类的方式声明部件，需要很多的模版代码，又因为React部件多数时候只能以React元素的方式以jsx方式使用，没有简单有效的方法从部件之外管理部件实例，再加上因为前述的更新检测方案产生的限制，组合和扩展能力还没有达到理想的境界。

  domcom不管是采用函数方法，还是采用对象方法，都能很自然地组合、变换、继承和扩展部件。前面的示例已经部分地体现了domcom在这方面的特点。

### 响应式

  flyd.js, bacon.js, rx.js这些响应式框架虽然能为设计提供某种帮助，能以更少代码完成更多任务，但是我个人总觉得遵循这些框架的程序整体架构并没有变得更加清晰和更好理解，有陷入面条式响应流的感觉。虽然Domcom的大部分元素都是响应式的，但并不象上述框架一样按照响应流的模式来组织整体结构，而只是作为一种声明方法，因此程序显得更为直观和清晰，更有组织性。和这些响应式框架相比，Domcom的响应是更为懒惰的。源头的变化并不直接触发后继的实际计算，而是提供component.update方法来触发更新，这种设计可以让开发人员对系统更有掌控主动权，并且对系统的运转有更清晰的理解。

## 接下来？

  现在我们已经了解了Domcom的特点，优点，通过理解前面的范例，也初步掌握了通过domcom编写web应用的方法。domcom提供新的编写Web应用的方式，让我们在简化设计实现的同时追求最好的运行效率。接下来，我们可以进一步了解以下内容，更全面的去熟悉domcom提供的API，更深入的它的概念和方法。当然，我们也可以马上开始使用Domcom，边使用边学习。

  [概念和原理](https://github.com/taijiweb/domcom/blob/master/doc/Chinese/概念和原理.md)：了解Domcom有关的基本概念和原理。

  [速查表](https://github.com/taijiweb/domcom/blob/master/doc/Chinese/速查表.md)：熟悉Domcom的api，常用技巧和惯用法。

  [API参考](https://github.com/taijiweb/domcom/blob/master/doc/Chinese/API参考.md)：关于Domcom所有公开的API的参考资料。

  [常问问题](https://github.com/taijiweb/domcom/blob/master/doc/Chinese/常问问题.md)：大家经常想了解的一些关于Domcom的问题。

  [doc/Chinese/](https://github.com/taijiweb/domcom/blob/master/doc/Chinese)文件夹还有更多的文档内容。


