这是一片 HT 的入门级文章,若是您能读懂
http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html
http://www.hightopo.com/guide/guide/core/beginners/examples/example_node.html
两个例子,那么能够跳过这篇文章,若是你对 ht.graph.GraphView,ht.DataModel 和 ht.Node 三者之间的关系还不是很了解,不知道如何工做的,那么不妨看下去,相信这篇文章可以帮到你。html
以前在 cnblog 搜索到关于入门的例子,好比 http://www.cnblogs.com/xhload3d/p/5911978.html,http://www.javashuo.com/article/p-ectsmvml-mh.html 有讲解上面三者的关系,可是之前并无看得很明白,我也是经过和 HT 的技术支持接触才慢慢理解 HT 是如何工做。下面经过一篇小文章像你们讲解下这三者整体上的关系,但愿能帮助到刚接触这个框架的人。node
既然你是在入门框架的时候遇到困难而后找到这篇博客,那么不妨先抛弃 HT ,经过一个小例子模拟下 HT 上三者的关系。
该例子使用了一些 es6 的语法,好比箭头函数和 class,若是你对es6不熟悉,能够移步 http://exploringjs.com/es6/ 了解。若是你有必定 JavaScript 功底,能够直接跳过看最终 demo。固然也能够跟随 demo,或者边看过作,这样或者能更好理解。git
划 demo 核心点:es6
核心关系:View 绑定 Model,Model 管理不少 Node,Node 发生变化时通知 Model,而后 Model 更新绑定他的 View 组件。github
demo 开始(下面有些地方说的 node,有些地方说的 data,暂时能够理解为一个概念,但其实不是,在学习 HT 的过程当中你会了解到),新建一个 index.html,并插入以下内容canvas
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body onload=init()> <script> function init(){ } </script> </body> </html>
下面开始建 View组件,View组件 主要用于展现做用,展现层元素挂载到组件的 _view 上面,script标签里插入以下代码:数组
class View{ constructor(){ this._view = document.createElement('div'); const style = this._view.style; style.position = 'absolute'; style.top = 0; style.right = 0; style.bottom = 0; style.left = 0; } getView(){ return this._view; } addToDom(parentNode){ if(!!parentNode) { parentNode.appendChild(this.getView()); } else { document.body.appendChild(this.getView()); } } }
并在 init 函数里面新建 view实例 并加入到 DOM 中,init 函数以下:浏览器
function init(){ view = new View(); view.addToDom(); }
此时在浏览器中打开 index.html,暂时的确什么都没有,但若是你在控制台 Elements 里面看到有个 div 插入到 script 标签下面,那么表明到这里你是成功的。app
下面开始建立 Model 组件,首先分析一下 Model 的做用框架
因此 Model 组件须要几个接口
建立 Model 组件代码以下:
class Model{ constructor() { this._datas = []; this.listeners = []; } addListener(fn){ this.listeners.push(fn); } handleDataChange(){ this.listeners.forEach(fn => fn()); } add(node){ node.setModel(this); if(this._datas.includes(node)){ return; } this._datas.push(node); this.handleDataChange(); } each(fn){ this._datas.forEach((data, index, list) => { fn(data, index, list) }) } getDatas(){ return this._datas; } }
固然如今界面上依然什么都没有,由于尚未为 Model 加入任何展现的 Node,建立Node代码以下:
class Node{ constructor() { this._node = document.createElement('div'); this._name = ''; const style = this._node.style; style.position = 'absolute'; style.top = 0; style.left = 0; style.height = '100px'; style.width = '100px'; style.overflow = 'hidden'; style.background = '#D8D8D8'; } getElString(){ return this._node.outerHTML; } fireChange(){ !!this._model && this._model.handleDataChange(); } setPosition(x, y){ const style = this._node.style; style.left = x + 'px'; style.top = y + 'px'; this.fireChange(); } setX(x){ this._node.style.left = x + 'px'; this.fireChange() } setY(y){ this._node.style.top = y + 'px'; this.fireChange(); } setImage(url){ const style = this._node.style; if(!!url){ this._node.innerHTML = ''; style.background = `url(${url}) no-repeat center`; this.fireChange(); } } setSize(width, height){ const style = this._node.style; style.width = width + 'px'; style.height = height + 'px'; this.fireChange(); } setWidth(width){ this._node.style.width = width + 'px'; this.fireChange() } setHeigth(height){ this._node.style.height = height + 'px'; this.fireChange(); } setName(name){ this._name = name; this._node.innerHTML = name; this.fireChange(); } setModel(model){ this._model = model; } }
这里暂时使用 _node 来挂载一个 div,而后操做 div 的一些属性显示出来,就像 canvas 上绘制一个矩形,若是你有基本的 JavaScript 功底,这里的 setXXX 函数功能应该都不会陌生,而 setModel 功能是让该 node 知道它是被哪个 Model 管理,fireChange 功能则是通知 Model 有更新
当 Model 被通知更新调用 handleDataChange 的时候,功能则是执行注册的全部更新函数,来达到更新全部绑定该 Model 组件的目的。
此时 init 函数能够稍微修改一下来显示出一点内容,修改后 init 函数以下:
function init(){ model = new Model() view = new View(model); view.addToDom(); node1 = new Node(); node1.setPosition(30, 30); node1.setName('我是node1'); model.add(node1); }
此时刷新页面仍是什么都没有,由于 View 组件暂时缺乏绑定 Model 和更新的方法,View 组件更新后代码以下:
class View{ constructor(model){ this._view = document.createElement('div'); const style = this._view.style; style.position = 'absolute'; style.top = 0; style.right = 0; style.bottom = 0; style.left = 0; !!model && this.setModel(model); } getView(){ return this._view; } setModel(model){ this._model = model; model.addListener(this.invalidate.bind(this)); } invalidate(){ const view = this.getView(); let innerHTML = ''; view.innerHTML = ''; this._model.each((data) => { innerHTML += data.getElString(); }) view.innerHTML = innerHTML; } addToDom(parentNode){ if(!!parentNode) { parentNode.appendChild(this.getView()); } else { document.body.appendChild(this.getView()); } this.invalidate(); } }
在 View 组件的构造函数中支持了可选的 model,setModel 函数能够供组件在后期更换 Model,在该函数中会让 model 注册该 view 组件的 invalidate 函数,invalidate 会在 Model 发生更新的时候被调用,此时再刷新一下浏览器,会发现一个 div 处于屏幕上,他的位置由 node.setPosition 决定。
初版的 demo 到此完成,此时你应该理解 view<-->model<-->node 他们的关系,可是此时你可能会有一个疑问,node 的管理为何不直接在它要显示的 view 组件上,而是要一个专门的 Model 管理,而后 view 去使用 model,HT 的设计是强大的,他可让你在不一样的 view 上显示相同的 model 类容,并且当 node 改变时,全部的 view 会同步更新。
如今先用两个不一样的 view 来演示一下,在 body 下面加入两个 div 分别命名 view1 和 view2,这部分代码参考以下:
<body onload=init()> <div id="view1"></div> <div id="view2"></div> <script> class View{ ...
而后为这两个 div 加一点样式,在 title 下面加入 style 标签并加入以下样式:
<style> div { box-sizing: border-box; overflow: hidden; } #view1 { position: absolute; top: 0; left: 0; right: 0; width: 50%; height: 400px; border: 2px solid #4080BF; } #view2 { position: absolute; top: 0; right: 0; width: 50%; height: 400px; border: 2px solid #4080BF; } </style>
最后在 init 函数里面创建两个 view 对象并分别挂载到 view1 和 view2 下面,修改后的init函数以下:
function init(){ model = new Model() view = new View(model); view.addToDom(document.getElementById('view1')); node1 = new Node(); node1.setPosition(30, 30); node1.setName('我是node1'); model.add(node1); view2 = new View(model); view2.addToDom(document.getElementById('view2')) }
如今刷新浏览器,会看到左右两个蓝框的div左上角分别有两个灰色的方块,里面显示的内容经过 node.setName() 设定
到这里你应该更加理解 view 和 model 的关系,可是可能你还有一个疑惑,干吗须要两个相同的 view 来显示相同的内容。在一些场合,可能你不仅是须要展现图形,还须要一个表格来展现 model 里面 data 元素的一些具体属性,好比 http://www.hightopo.com/guide/guide/core/beginners/examples/example_overview.html 左下方 TableView 组件 所示,这儿用 demo 模拟一下他们的工做。要建立一个 TableView,会发现它和已有的 View 有些相似,好比 setModel 和 addToDom,固然二者的内容确定是不同的,因此依靠 es6 class 和 extends,对 view 作一些修改以知足它能够被扩展,View 代码修改以下:
class View{ constructor(model){ this._view = document.createElement('div'); const style = this._view.style; style.position = 'absolute'; style.top = 0; style.right = 0; style.bottom = 0; style.left = 0; !!model && this.setModel(model); } getView(){ return this._view; } setModel(model){ this._model = model; model.addListener(this.invalidate.bind(this)); } addToDOM(parentNode){ if(!!parentNode) { parentNode.appendChild(this.getView()); } else { document.body.appendChild(this.getView()); } this.invalidate(); } }
主要修改是去掉 invalidate 方法,而后让扩张的组件来实现这个方法,创建第一个扩张组件:
class SimulateGraphView extends View{ invalidate(){ const view = this.getView(); let innerHTML = ''; view.innerHTML = ''; this._model.each((data) => { innerHTML += data.getElString(); }) view.innerHTML = innerHTML; } }
此时的 demo 确定是没法工做,由于 init 函数里面还在使用View来实例化组件,因此须要将 new View 修改成 new SimulateGraphView,init 函数此时以下:
function init(){ model = new Model() view = new SimulateGraphView(model); view.addToDOM(document.getElementById('view1')); node1 = new Node(); node1.setPosition(30, 30); node1.setName('我是node1'); model.add(node1); view2 = new SimulateGraphView(model); view2.addToDOM(document.getElementById('view2')) }
刷新浏览器代码工做正常。而后要开始创建第二个扩展组件 TableView,一样继承自 View,因此也拥有 setModel 等方法,与 SimulateGraphView 的主要不一样在于 invalidate 函数,TableView 代码以下:
class TableView extends View{ constructor(model){ super(model); this.content = ` <table> <tr> <th>name</th> <th>x</th> <th>y</th> <th>width</th> <th>height</th> </tr> __content__ <table> `; } invalidate(){ const view = this.getView(); let content = ''; view.innerHTML = ''; this._model.each((data) => { content += ` <tr> <td>${data.getName()}</td> <td>${data.getX()}</td> <td>${data.getY()}</td> <td>${data.getWidth()}</td> <td>${data.getHeight()}</td> </tr> ` }) view.innerHTML = this.content.replace(/__content__/, content); } }
能够看到此表格主要做用显示绑定的 Model 里面 node 的一些属性,好比 name,坐标 x 和 y 和宽度高度,此时 node 对象上还缺乏这些方法,先给 Node 加上这些方法,修改后 Node 代码以下:
class Node{ constructor() { this._node = document.createElement('div'); this._name = ''; const style = this._node.style; style.position = 'absolute'; style.top = 0; style.left = 0; style.height = '100px'; style.width = '100px'; style.overflow = 'hidden'; style.background = '#D8D8D8'; } getElString(){ return this._node.outerHTML; } fireChange(){ !!this._model && this._model.handleDataChange(); } setPosition(x, y){ const style = this._node.style; style.left = x + 'px'; style.top = y + 'px'; this.fireChange(); } setX(x){ this._node.style.left = x + 'px'; this.fireChange() } setY(y){ this._node.style.top = y + 'px'; this.fireChange(); } getPosition(){ return {x: this._node.style.left, y: this._node.style.top} } getX(){ return this._node.style.left; } getY(){ return this._node.style.top; } setImage(url){ const style = this._node.style; if(!!url){ this._node.innerHTML = ''; style.background = `url(${url}) no-repeat center`; this.fireChange(); } } setSize(width, height){ const style = this._node.style; style.width = width + 'px'; style.height = height + 'px'; this.fireChange(); } setWidth(width){ this._node.style.width = width + 'px'; this.fireChange() } getWidth(){ return this._node.style.width; } setHeigth(height){ this._node.style.height = height + 'px'; this.fireChange(); } getHeight(height){ return this._node.style.height; } setName(name){ this._name = name; this._node.innerHTML = name; this.fireChange(); } getName(){ return this._name; } setModel(model){ this._model = model; } }
此时 table 组件基本能够正常工做,可是还缺乏一个挂载的 div,修改下 body 下里面内容以下:
<body onload = init()> <div id="view1"></div> <div id="view2"></div> <div id='view3'></div> <script> class View{ ...
而后再修改一下 CSS,修改后 style 以下:
<style> div { box-sizing: border-box; overflow: hidden; } #view1 { position: absolute; top: 0; left: 0; right: 0; width: 50%; height: 400px; border: 2px solid #4080BF; } #view2 { position: absolute; top: 0; right: 0; width: 50%; height: 400px; border: 2px solid #4080BF; } table { border-collapse: collapse; border-spacing: 0px; } table, th, td { padding: 5px; border: 1px solid black; } #view3 { position: absolute; top: 410px; right: 0; width: 100%; height: 300px; border: 2px solid #4080BF; } </style>
接下来 new 一个 table 实例出来挂载到 view3 下面,此时 Model 只有一个图元,再加入一个演示,修改后 init 函数以下:
function init(){ model = new Model(); view = new SimulateGraphView(model); view.addToDOM(document.getElementById('view1')); node1 = new Node(); node1.setPosition(30, 30); node1.setName('我是node1'); model.add(node1); node2 = new Node(); node2.setPosition(30, 150); node2.setName('我是node2'); node2.setSize(200, 80) node2.setImage('http://www.hightopo.com/images/logo.png'); model.add(node2); view2 = new SimulateGraphView(model); view2.addToDOM(document.getElementById('view2')); table = new TableView(model); table.addToDOM(document.getElementById('view3')); }
刷新浏览器,能够在下方看到一个 table 显示 Model 里面 node 的一些属性,固然须要一些改变才能感觉到效果,因此这时候能够打开控制台,而后在 Console 面板下面输入: node2.setPosition(200, 100) 并执行,这时候你会发现 graphView 和 table 都同步更新了,此时你能够在控制台里对 node1 和 node2 执行下其余的操做好比 node1.setSize(200, 60), graphView 和 table 一样都会更新。
这么长的 dmeo 到此就结束了,其实并不麻烦,主要目的是为了给你们介绍下 View,Model 和 Node 之间的关系,那么再回到 HT
划 HT 重点:
如今结合 demo 的例子再来看这几条重点,应该好理解多了吧!
若是读到这里感受没有问题,能够移步 http://www.hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html#ref_designpattern 阅读下官方关于 DataModel 及其余几个核心概念的说明。而后基本全部 HT 关于 2d 的demo应该都能看明白。
关于 demo 划重点:
HT 中文网地址:
http://www.hightopo.com/cn-index.html
最后 demo 下载地址: