Hightopo 2D 入门

这是一片 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.htmlhttp://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

  1. View 做为展现层,会绑定一个 Model,而后根据Model里面的内容展现出内容
  2. Model 里面会储存要显示的图元信息和绑定他的组件,并在图元变化的时候更新组件
  3. Node 引用一个 DIV 来模拟一个图元

核心关系: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 的做用框架

  • 被不一样的 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 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 重点:

  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

相关文章
相关标签/搜索