这是一片 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://exploringjs.com/es6/ 了解。如果你有一定 JavaScript 功底,可以直接跳过看最终 demo。当然也可以跟随 demo,或者边看过做,这样或者能更好理解。
划 demo 核心点:
- View 作为展示层,会绑定一个 Model,然后根据Model里面的内容展示出内容
- Model 里面会储存要显示的图元信息和绑定他的组件,并在图元变化的时候更新组件
- Node 引用一个 DIV 来模拟一个图元
核心关系:View 绑定 Model,Model 管理很多 Node,Node 发生变化时通知 Model,然后 Model 更新绑定他的 View 组件。
demo 开始(下面有些地方说的 node,有些地方说的 data,暂时可以理解为一个概念,但其实不是,在学习 HT 的过程中你会了解到),新建一个 index.html,并插入如下内容
1<!DOCTYPE html> 2<html> 3<head> 4 <meta charset="UTF-8"> 5 <title>Document</title> 6</head> 7<body onload=init()> 8 <script> 9 function init(){ 10 11 } 12 </script> 13</body> 14</html>
下面开始建 View组件,View组件 主要用于展示作用,展示层元素挂载到组件的 _view 上面,script标签里插入如下代码:
1class View{ 2 constructor(){ 3 this._view = document.createElement('div'); 4 const style = this._view.style; 5 style.position = 'absolute'; 6 style.top = 0; 7 style.right = 0; 8 style.bottom = 0; 9 style.left = 0; 10 } 11 getView(){ 12 return this._view; 13 } 14 addToDom(parentNode){ 15 if(!!parentNode) { 16 parentNode.appendChild(this.getView()); 17 } else { 18 document.body.appendChild(this.getView()); 19 } 20 } 21}
并在 init 函数里面新建 view实例 并加入到 DOM 中,init 函数如下:
1function init(){ 2 view = new View(); 3 view.addToDom(); 4}
此时在浏览器中打开 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 组件代码如下:
1class Model{ 2 constructor() { 3 this._datas = []; 4 this.listeners = []; 5 } 6 addListener(fn){ 7 this.listeners.push(fn); 8 } 9 handleDataChange(){ 10 this.listeners.forEach(fn => fn()); 11 } 12 add(node){ 13 node.setModel(this); 14 if(this._datas.includes(node)){ 15 return; 16 } 17 this._datas.push(node); 18 this.handleDataChange(); 19 } 20 each(fn){ 21 this._datas.forEach((data, index, list) => { 22 fn(data, index, list) 23 }) 24 } 25 getDatas(){ 26 return this._datas; 27 } 28}
当然现在界面上依然什么都没有,因为还没有为 Model 加入任何展示的 Node,创建Node代码如下:
1class Node{ 2 constructor() { 3 this._node = document.createElement('div'); 4 this._name = ''; 5 const style = this._node.style; 6 style.position = 'absolute'; 7 style.top = 0; 8 style.left = 0; 9 style.height = '100px'; 10 style.width = '100px'; 11 style.overflow = 'hidden'; 12 style.background = '#D8D8D8'; 13 } 14 getElString(){ 15 return this._node.outerHTML; 16 } 17 fireChange(){ 18 !!this._model && this._model.handleDataChange(); 19 } 20 setPosition(x, y){ 21 const style = this._node.style; 22 style.left = x + 'px'; 23 style.top = y + 'px'; 24 this.fireChange(); 25 } 26 setX(x){ 27 this._node.style.left = x + 'px'; 28 this.fireChange() 29 } 30 setY(y){ 31 this._node.style.top = y + 'px'; 32 this.fireChange(); 33 } 34 setImage(url){ 35 const style = this._node.style; 36 if(!!url){ 37 this._node.innerHTML = ''; 38 style.background = `url(${url}) no-repeat center`; 39 this.fireChange(); 40 } 41 } 42 setSize(width, height){ 43 const style = this._node.style; 44 style.width = width + 'px'; 45 style.height = height + 'px'; 46 this.fireChange(); 47 } 48 setWidth(width){ 49 this._node.style.width = width + 'px'; 50 this.fireChange() 51 } 52 setHeigth(height){ 53 this._node.style.height = height + 'px'; 54 this.fireChange(); 55 } 56 setName(name){ 57 this._name = name; 58 this._node.innerHTML = name; 59 this.fireChange(); 60 } 61 setModel(model){ 62 this._model = model; 63 } 64}
这里暂时使用 _node 来挂载一个 div,然后操作 div 的一些属性显示出来,就像 canvas 上绘制一个矩形,如果你有基本的 JavaScript 功底,这里的 setXXX 函数功能应该都不会陌生,而 setModel 功能是让该 node 知道它是被哪一个 Model 管理,fireChange 功能则是通知 Model 有更新
当 Model 被通知更新调用 handleDataChange 的时候,功能则是执行注册的所有更新函数,来达到更新所有绑定该 Model 组件的目的。
此时 init 函数可以稍微修改一下来显示出一点内容,修改后 init 函数如下:
1function init(){ 2 model = new Model() 3 view = new View(model); 4 view.addToDom(); 5 6 node1 = new Node(); 7 node1.setPosition(30, 30); 8 node1.setName('我是node1'); 9 model.add(node1); 10}
此时刷新页面还是什么都没有,因为 View 组件暂时缺少绑定 Model 和更新的方法,View 组件更新后代码如下:
1class View{ 2 constructor(model){ 3 this._view = document.createElement('div'); 4 const style = this._view.style; 5 style.position = 'absolute'; 6 style.top = 0; 7 style.right = 0; 8 style.bottom = 0; 9 style.left = 0; 10 !!model && this.setModel(model); 11 } 12 getView(){ 13 return this._view; 14 } 15 setModel(model){ 16 this._model = model; 17 model.addListener(this.invalidate.bind(this)); 18 } 19 invalidate(){ 20 const view = this.getView(); 21 let innerHTML = ''; 22 view.innerHTML = ''; 23 this._model.each((data) => { 24 innerHTML += data.getElString(); 25 }) 26 view.innerHTML = innerHTML; 27 } 28 addToDom(parentNode){ 29 if(!!parentNode) { 30 parentNode.appendChild(this.getView()); 31 } else { 32 document.body.appendChild(this.getView()); 33 } 34 this.invalidate(); 35 } 36}
在 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,这部分代码参考如下:
1<body onload=init()> 2 <div id="view1"></div> 3 <div id="view2"></div> 4 <script> 5 class View{ 6 ...
然后为这两个 div 加一点样式,在 title 下面加入 style 标签并加入如下样式:
1<style> 2 div { 3 box-sizing: border-box; 4 overflow: hidden; 5 } 6 #view1 { 7 position: absolute; 8 top: 0; 9 left: 0; 10 right: 0; 11 width: 50%; 12 height: 400px; 13 border: 2px solid #4080BF; 14 } 15 #view2 { 16 position: absolute; 17 top: 0; 18 right: 0; 19 width: 50%; 20 height: 400px; 21 border: 2px solid #4080BF; 22 } 23</style>
最后在 init 函数里面建立两个 view 对象并分别挂载到 view1 和 view2 下面,修改后的init函数如下:
1function init(){ 2 model = new Model() 3 view = new View(model); 4 view.addToDom(document.getElementById('view1')); 5 6 node1 = new Node(); 7 node1.setPosition(30, 30); 8 node1.setName('我是node1'); 9 model.add(node1); 10 11 view2 = new View(model); 12 view2.addToDom(document.getElementById('view2')) 13}
现在刷新浏览器,会看到左右两个蓝框的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 代码修改如下:
1class View{ 2 constructor(model){ 3 this._view = document.createElement('div'); 4 const style = this._view.style; 5 style.position = 'absolute'; 6 style.top = 0; 7 style.right = 0; 8 style.bottom = 0; 9 style.left = 0; 10 !!model && this.setModel(model); 11 } 12 getView(){ 13 return this._view; 14 } 15 setModel(model){ 16 this._model = model; 17 model.addListener(this.invalidate.bind(this)); 18 } 19 addToDOM(parentNode){ 20 if(!!parentNode) { 21 parentNode.appendChild(this.getView()); 22 } else { 23 document.body.appendChild(this.getView()); 24 } 25 this.invalidate(); 26 } 27}
主要修改是去掉 invalidate 方法,然后让扩张的组件来实现这个方法,建立第一个扩张组件:
1class SimulateGraphView extends View{ 2 invalidate(){ 3 const view = this.getView(); 4 let innerHTML = ''; 5 view.innerHTML = ''; 6 this._model.each((data) => { 7 innerHTML += data.getElString(); 8 }) 9 view.innerHTML = innerHTML; 10 } 11}
此时的 demo 肯定是无法工作,因为 init 函数里面还在使用View来实例化组件,所以需要将 new View 修改为 new SimulateGraphView,init 函数此时如下:
1function init(){ 2 model = new Model() 3 view = new SimulateGraphView(model); 4 view.addToDOM(document.getElementById('view1')); 5 6 node1 = new Node(); 7 node1.setPosition(30, 30); 8 node1.setName('我是node1'); 9 model.add(node1); 10 11 view2 = new SimulateGraphView(model); 12 view2.addToDOM(document.getElementById('view2')) 13}
刷新浏览器代码工作正常。然后要开始建立第二个扩展组件 TableView,同样继承自 View,所以也拥有 setModel 等方法,与 SimulateGraphView 的主要不同在于 invalidate 函数,TableView 代码如下:
1class TableView extends View{ 2 constructor(model){ 3 super(model); 4 this.content = ` 5 <table> 6 <tr> 7 <th>name</th> 8 <th>x</th> 9 <th>y</th> 10 <th>width</th> 11 <th>height</th> 12 </tr> 13 __content__ 14 <table> 15 `; 16 } 17 invalidate(){ 18 const view = this.getView(); 19 let content = ''; 20 view.innerHTML = ''; 21 this._model.each((data) => { 22 content += ` 23 <tr> 24 <td>${data.getName()}</td> 25 <td>${data.getX()}</td> 26 <td>${data.getY()}</td> 27 <td>${data.getWidth()}</td> 28 <td>${data.getHeight()}</td> 29 </tr> 30 ` 31 }) 32 view.innerHTML = this.content.replace(/__content__/, content); 33 } 34}
可以看到此表格主要作用显示绑定的 Model 里面 node 的一些属性,比如 name,坐标 x 和 y 和宽度高度,此时 node 对象上还缺少这些方法,先给 Node 加上这些方法,修改后 Node 代码如下:
1class Node{ 2 constructor() { 3 this._node = document.createElement('div'); 4 this._name = ''; 5 const style = this._node.style; 6 style.position = 'absolute'; 7 style.top = 0; 8 style.left = 0; 9 style.height = '100px'; 10 style.width = '100px'; 11 style.overflow = 'hidden'; 12 style.background = '#D8D8D8'; 13 } 14 getElString(){ 15 return this._node.outerHTML; 16 } 17 fireChange(){ 18 !!this._model && this._model.handleDataChange(); 19 } 20 setPosition(x, y){ 21 const style = this._node.style; 22 style.left = x + 'px'; 23 style.top = y + 'px'; 24 this.fireChange(); 25 } 26 setX(x){ 27 this._node.style.left = x + 'px'; 28 this.fireChange() 29 } 30 setY(y){ 31 this._node.style.top = y + 'px'; 32 this.fireChange(); 33 } 34 getPosition(){ 35 return {x: this._node.style.left, y: this._node.style.top} 36 } 37 getX(){ 38 return this._node.style.left; 39 } 40 getY(){ 41 return this._node.style.top; 42 } 43 setImage(url){ 44 const style = this._node.style; 45 if(!!url){ 46 this._node.innerHTML = ''; 47 style.background = `url(${url}) no-repeat center`; 48 this.fireChange(); 49 } 50 } 51 setSize(width, height){ 52 const style = this._node.style; 53 style.width = width + 'px'; 54 style.height = height + 'px'; 55 this.fireChange(); 56 } 57 setWidth(width){ 58 this._node.style.width = width + 'px'; 59 this.fireChange() 60 } 61 getWidth(){ 62 return this._node.style.width; 63 } 64 setHeigth(height){ 65 this._node.style.height = height + 'px'; 66 this.fireChange(); 67 } 68 getHeight(height){ 69 return this._node.style.height; 70 } 71 setName(name){ 72 this._name = name; 73 this._node.innerHTML = name; 74 this.fireChange(); 75 } 76 getName(){ 77 return this._name; 78 } 79 setModel(model){ 80 this._model = model; 81 } 82}
此时 table 组件基本可以正常工作,但是还缺少一个挂载的 div,修改下 body 下里面内容如下:
1<body onload = init()> 2 <div id="view1"></div> 3 <div id="view2"></div> 4 <div id='view3'></div> 5 <script> 6 class View{ 7 ...
然后再修改一下 CSS,修改后 style 如下:
1<style> 2 div { 3 box-sizing: border-box; 4 overflow: hidden; 5 } 6 #view1 { 7 position: absolute; 8 top: 0; 9 left: 0; 10 right: 0; 11 width: 50%; 12 height: 400px; 13 border: 2px solid #4080BF; 14 } 15 #view2 { 16 position: absolute; 17 top: 0; 18 right: 0; 19 width: 50%; 20 height: 400px; 21 border: 2px solid #4080BF; 22 } 23 table { 24 border-collapse: collapse; 25 border-spacing: 0px; 26 } 27 table, th, td { 28 padding: 5px; 29 border: 1px solid black; 30 } 31 #view3 { 32 position: absolute; 33 top: 410px; 34 right: 0; 35 width: 100%; 36 height: 300px; 37 border: 2px solid #4080BF; 38 } 39</style>
接下来 new 一个 table 实例出来挂载到 view3 下面,此时 Model 只有一个图元,再加入一个演示,修改后 init 函数如下:
1function init(){ 2 model = new Model(); 3 view = new SimulateGraphView(model); 4 view.addToDOM(document.getElementById('view1')); 5 6 node1 = new Node(); 7 node1.setPosition(30, 30); 8 node1.setName('我是node1'); 9 model.add(node1); 10 11 node2 = new Node(); 12 node2.setPosition(30, 150); 13 node2.setName('我是node2'); 14 node2.setSize(200, 80) 15 node2.setImage('http://www.hightopo.com/images/logo.png'); 16 model.add(node2); 17 18 view2 = new SimulateGraphView(model); 19 view2.addToDOM(document.getElementById('view2')); 20 21 table = new TableView(model); 22 table.addToDOM(document.getElementById('view3')); 23}
刷新浏览器,可以在下方看到一个 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 下载地址: