这是一片 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,并插入如下内容
<!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 下载地址: