這是一片 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 下載地址:
