这是一片面向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 三者之间的关系还不是很了解,不知道如何工作的,那么不妨看下去,相信这篇文章能够帮到你。
之前在cnblog搜索到关于入门的例子,比如http://www.cnblogs.com/xhload3d/p/5911978.html,https://www.cnblogs.com/xhload3d/p/8249304.html 有讲解上面三者的关系,但是以前并没有看得很明白,我也是通过和ht的技术支持接触才慢慢理解 ht 是如何工作。下面通过一篇小文章像大家讲解下这三者总体上的关系,希望能帮助到刚接触这个框架的人。
既然你是在入门框架的时候遇到困难然后找到这篇博客,那么不妨先抛弃ht,通过一个小例子模拟下ht上三者的关系。
该例子使用了一些es6的语法,比如箭头函数和class,如果你对es6不熟悉,可以移步 http://es6.ruanyifeng.com/#docs/intro 了解。如果你有一定javascript功底,可以直接跳过看最终demo。当然也可以跟随demo,或者边看过做,这样或者能更好理解。
划demo核心点:
- View作为展示层,会绑定一个Model,然后根据Model里面的内容展示出内容
- Model里面会储存要显示的图元信息和绑定他的组件,并在图元变化的时候更新组件
- Node引用一个DIV来模拟一个图元
核心关系:View绑定Model,Model管理很多Node,Node发生变化时通知Model,然后Model更新绑定他的View组件。
demo开始(下面有些地方说的node,有些地方说的data,暂时可以理解为一个概念,但其实不是,在学习ht的过程中你会了解到),新建一个 index.html,并插入如下内容
<!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标签下面,那么代表到这里你是成功的。
下面开始创建Model组件,首先分析一下Model的作用
- 被不同的view组件绑定,然后在他管理的data元素发生改变时,通知绑定的view进行更新
- 增加data元素并附加遍历data功能。
所以Model组件需要几个接口
- addListener: 用于给view层注册更新函数
- handleDataChange: 当管理的data元素更新时,调用view层注册的更新函数
- add,each,getDatas 分别是增加data元素,遍历data和获取data数组
创建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重点:
- ht.graph.GraphView 是作为展示层的组件,也就是我们看到的东西都由他来呈现,每个组件上有个 _view 属性挂载着展示层的 div,可以通过graphView.getView()来获取,所以只要把这个组件插入到你的 DOM 里面, 就可以显示出图形。而显示的图形则是根据该组件绑定的DataModel决定。其他的功能性组件,如TablePane都需要一个DataModel来显示内容。
- ht.DataModel 是一个数据集,他管理着很多 ht.Data,可以通过 dotaModel.getDatas() 得到一个 ht.List,里面包含数据容器所管理的数据,每一个元素都是 ht.Data 或它的子类实例,而如果你需要在ht.graph.GraphView上面显示出类容,那么每一个数据必须是 ht.Node 或它的子类实例(ht.Node 继承于 ht.Data)。
- ht.Node 抽象要显示的每一个数据元,比如一个图形名字,宽高,和位置,图片等所有其他信息,处了 ht.Node 之外,ht 还提供了很多其他类型的图元如线段和组,详见 http://www.hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html#ref_node 及下面的内容。
现在结合demo的例子再来看这几条重点,应该好理解多了吧!
如果读到这里感觉没有问题,可以移步 http://www.hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html#ref_designpattern 阅读下官方关于 DataModel 及其他几个核心概念的说明。然后基本所有ht关于 2d 的demo应该都能看明白。
关于demo划重点:
- demo里面每一个node都是由div模拟,这是html里面实实在在存在的一个基本元素,但是ht.Data不是一个实实在在的HTMLElement,每一个data的呈现都是canvas上的一部分类容。
- demo主要内容只是为了介绍 ht.graph.GraphView 等展示层组件和 ht.DataModel 和 ht.Data 之间的关系,为了介绍总体关系和大体工作流程,所以请忽略demo里面Node会挂载一个div,这条更是强调上一条重点。
- ht的工作流程复杂到大概是这个demo的...额10个手指头算不过来还是不算了,所以不要以为ht就是这么简单!不要因为我的demo降低你的兴趣,请你深究并感受ht的美。
ht中文网地址:
http://www.hightopo.com/cn-index.html
最后demo下载地址: