ht

这是一片面向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.htmlhttps://www.cnblogs.com/xhload3d/p/8249304.html 有讲解上面三者的关系,但是以前并没有看得很明白,我也是通过和ht的技术支持接触才慢慢理解 ht 是如何工作。下面通过一篇小文章像大家讲解下这三者总体上的关系,希望能帮助到刚接触这个框架的人。

既然你是在入门框架的时候遇到困难然后找到这篇博客,那么不妨先抛弃ht,通过一个小例子模拟下ht上三者的关系。
该例子使用了一些es6的语法,比如箭头函数和class,如果你对es6不熟悉,可以移步 http://es6.ruanyifeng.com/#docs/intro 了解。如果你有一定javascript功底,可以直接跳过看最终demo。当然也可以跟随demo,或者边看过做,这样或者能更好理解。

划demo核心点:

  1. View作为展示层,会绑定一个Model,然后根据Model里面的内容展示出内容
  2. Model里面会储存要显示的图元信息和绑定他的组件,并在图元变化的时候更新组件
  3. 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组件需要几个接口

  1. addListener: 用于给view层注册更新函数
  2. handleDataChange: 当管理的data元素更新时,调用view层注册的更新函数
  3. 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 classextends,对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重点:

  1. ht.graph.GraphView 是作为展示层的组件,也就是我们看到的东西都由他来呈现,每个组件上有个 _view 属性挂载着展示层的 div,可以通过graphView.getView()来获取,所以只要把这个组件插入到你的 DOM 里面, 就可以显示出图形。而显示的图形则是根据该组件绑定的DataModel决定。其他的功能性组件,如TablePane都需要一个DataModel来显示内容。
  2. ht.DataModel 是一个数据集,他管理着很多 ht.Data,可以通过 dotaModel.getDatas() 得到一个 ht.List,里面包含数据容器所管理的数据,每一个元素都是 ht.Data 或它的子类实例,而如果你需要在ht.graph.GraphView上面显示出类容,那么每一个数据必须是 ht.Node 或它的子类实例(ht.Node 继承于 ht.Data)。
  3. 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划重点:

  1. demo里面每一个node都是由div模拟,这是html里面实实在在存在的一个基本元素,但是ht.Data不是一个实实在在的HTMLElement,每一个data的呈现都是canvas上的一部分类容。
  2. demo主要内容只是为了介绍 ht.graph.GraphView 等展示层组件和 ht.DataModel 和 ht.Data 之间的关系,为了介绍总体关系和大体工作流程,所以请忽略demo里面Node会挂载一个div,这条更是强调上一条重点。
  3. ht的工作流程复杂到大概是这个demo的...额10个手指头算不过来还是不算了,所以不要以为ht就是这么简单!不要因为我的demo降低你的兴趣,请你深究并感受ht的美。

ht中文网地址:

http://www.hightopo.com/cn-index.html

最后demo下载地址:

https://github.com/MuyNooB/ht-start

猜你喜欢

转载自www.cnblogs.com/hawawa/p/8975428.html
ht
HT5