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

相關文章
相關標籤/搜索