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 三者之間的關系還不是很了解,不知道如何工作的,那么不妨看下去,相信這篇文章能夠幫到你。

之前在 cnblog 搜索到關於入門的例子,比如 http://www.cnblogs.com/xhload3d/p/5911978.htmlhttps://www.cnblogs.com/xhload3d/p/8249304.html 有講解上面三者的關系,但是以前並沒有看得很明白,我也是通過和 HT 的技術支持接觸才慢慢理解 HT 是如何工作。下面通過一篇小文章像大家講解下這三者總體上的關系,希望能幫助到剛接觸這個框架的人。

既然你是在入門框架的時候遇到困難然后找到這篇博客,那么不妨先拋棄 HT ,通過一個小例子模擬下 HT 上三者的關系。
該例子使用了一些 es6 的語法,比如箭頭函數和 class,如果你對es6不熟悉,可以移步 http://exploringjs.com/es6/ 了解。如果你有一定 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 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM