我們上一篇《基於 WebSocket 實現 WebGL 3D 拓撲圖實時資料通訊同步(一)》主要講解了如何搭建一個實時資料通訊伺服器,客戶端與服務端是如何通訊的,相信通過上一篇的講解,再配合上資料庫的資料儲存,我們就可以實現一個簡易版的 Web 聊天工具了,有空的朋友可以自己嘗試下實現,那麼我們今天的主要內容真的是實現 WebGL 3D 拓撲圖實時資料通訊了,請大家接著往下看。
有了前面的知識儲備,我們就可以來真正實現我們 3D 拓撲圖元件上節點位置資訊的實時資料同步了,毋庸置疑,節點的位置資訊必須是在服務端統籌控制,才能達到實時資料同步,也就是說,我們必須在服務端建立 DataModel 來管理節點,建立 ForceLayout 彈力佈局節點位置,並在節點位置改變的過程中,實時地將位置資訊推送到客戶端,讓每個客戶端都更新各自頁面上面的節點位置。
在服務端我們該如何建立 HT 的 DataModel 和 ForceLayout 呢?其實也很簡單,我們可以看看下面的程式碼:
var ht = global.ht = this.ht = require('../../../build/ht-debug.js').ht, dataModel = new ht.DataModel(), reloadModel = require("../util.js").reloadModel; reloadModel(dataModel, { A: 3, B: 5 }); require("../../../build/ht-forcelayout-debug.js"); var forceLayout = new ht.layout.Force3dLayout(dataModel); forceLayout.onRelaxed = function() { var result = {}; dataModel.each(function(data) { if (data instanceof ht.Node) { result[data.getTag()] = data.p3(); } }); io.emit('result', result); }; forceLayout.start();
我們通過 require 將非 Node.js 模組包引入到程式中,並加以使用。在上面的程式碼中,我們確實建立了 HT 的拓撲節點,是通過 util.js 檔案中的 relowdModel 方法建立的節點,那這個檔案中到底是怎麼實現建立 HT 拓撲節點的呢?接下來就來看看具體的實現:
function createNode(dataModel, id){ var node = new ht.Node(); node.setId(id); node.setTag(id); node.s3(40, 40, 40); node.s({ 'shape3d': 'sphere', 'note': id, 'note.position': 17, 'note.background': 'yellow', 'note.color': 'black', 'note.autorotate': true, 'note.face': 'top' }); dataModel.add(node); return node; } function createEdge(dataModel, source, target){ var edge = new ht.Edge(source, target); edge.s({ 'edge.width': 10, 'shape3d.color': '#E74C3C', 'edge.3d': true }); dataModel.add(edge); return edge; } function reloadModel(dataModel, info){ dataModel.clear(); var ip = "192.168.1."; var count = 0; var root = createNode(dataModel, ip + count++); for (var i = 0; i < info.A; i++) { var iNode = createNode(dataModel, ip + count++); createEdge(dataModel, root, iNode); for (var j = 0; j < info.B; j++) { var jNode = createNode(dataModel, ip + count++); createEdge(dataModel, iNode, jNode); } } } this.reloadModel = reloadModel;
在這個檔案中,封裝了建立節點的方法 createNode,和建立連線的方法 createEdge,最後是通過 reloadModel 方法將前面的兩個方法連線起來,在這個檔案的最後,我們可以看到,只公開了 reloadModel 的函式介面。
當然光這些是不夠的,這些還不能夠達成實時資料通訊的功能,我們還需要監聽和派發一些事件才能夠達到效果,那麼我們都監聽了什麼藉口,派發了什麼事件呢?
io.on('connection', function(socket) { socket.emit('ready', dataModel.serialize(0)); console.log('a user connected'); socket.on('disconnect', function() { console.log('user disconnected'); }); socket.on('moveMap', function(moveMap) { dataModel.sm().cs(); for (var id in moveMap) { var data = dataModel.getDataByTag(id); if (data) { data.p3(moveMap[id]); dataModel.sm().as(data); } } }); });
上面那串程式碼是我們的事件監聽,我們通過監聽 moveMap 的事件,並獲取從客戶端傳遞上來的移動的節點座標資訊,根據引數的內容,我們將其改變服務端的 DataModel 中對應節點的座標,改變後 ForceLayout 就會根據當前的狀態去調整整個拓撲上所有節點的位置。那麼在調節的過程中,我們是怎麼知道 ForceLayout 是正在調整的呢?在前面介紹如何在 Node.js 上面建立 HT 相關的元件時貼出來的程式碼中就告訴我麼怎麼做了。
在建立 ForceLayout 元件的程式碼後面,緊跟著就是過載 ForceLayout 元件的 onRelaxed 方法,每次佈局玩後,都會呼叫這個方法,這樣我們就可以在這個方法中,編輯獲取到 DataModel 中的所有節點的當前位置,並通過 io.emit 方法通知給所有的客戶端,讓客戶端去實時更新對應節點的座標位置。
但是還有一個問題,我們要怎麼樣讓客戶端顯示的節點和服務端上的節點一一對應呢?首先不能讓客戶端自己建立節點,我們的做法其實也很簡單,雖然不能保證客戶端的節點 ID 會和服務端的節點 ID 一模一樣,但是我們可以保證其他關鍵屬性是一樣,因為我們利用了 HT 的序列化功能,當有客戶端連線到伺服器時,就會向客戶端派發 ready 事件,將 DataModel 序列化的結果返回到客戶端,讓客戶端反序列化,從而達到資料基本一致的效果。
那麼客戶端和服務端的節點是如何保持一一對應的呢?首先我們得了解 HT 在獲取節點物件上提供了幾個方法,熟悉的朋友應該知道,有 getDataById 和 getDataByTag 兩個方法,其中 ID 是 HT 系統自己維護的屬性,Tag 是提供給使用者自己維護其唯一性的屬性,一般不建議使用 ID 作為業務上面的唯一標識,因為在序列化和反序列化時候可能會有細微的差別,很難保證反序列話後的節點 ID 和序列化前的 ID 是一樣的。因此在本文中,我們是通過 Tag 屬性來控制伺服器和客戶端的節點一一對應的。
接下來我們來看看客戶端的實現吧:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <script src="/socket.io/socket.io.js"></script> <script src="/build/ht-debug.js"></script> <script> var socket = io(); var init = function() { var dm = window.dataModel = new ht.DataModel(), sm = dm.sm(), g3d = new ht.graph3d.Graph3dView(dm); g3d.setGridSize(100); g3d.setGridGap(100); g3d.setGridVisible(true); g3d.addToDOM(); var moveNodes = null; g3d.mi(function(evt){ if ( evt.kind === 'beginMove'){ moveNodes = sm.getSelection(); } else if (evt.kind === 'betweenMove'){ moveMap = {}; g3d.sm().each(function(data){ if(data instanceof ht.Node){ moveMap[data.getTag()] = data.p3(); console.info(data.p3()); } }); socket.emit('moveMap', moveMap); } else if (evt.kind === 'endMove') { moveNodes = null; } }); socket.on('ready', function(json) { dm.clear(); dm.deserialize(json); }); socket.on('result', function (result) { for(var id in result){ var data = dm.getDataByTag([id]); if (!data) continue; if (moveNodes && moveNodes.indexOf(data) >= 0) continue; data.p3(result[id]); } }); }; </script> </head> <body onload="init();"> </body> </html>
程式碼並不長,我來介紹下具體的實現。首先是建立 3D 拓撲圖元件,並做一些設定,讓場景上出現線條,然後就是監聽拓撲圖上面的操作,當監聽到 betweenMove 時,或許當前被移動的節點位置資訊,向伺服器派發該資訊;接下來是監聽伺服器的 ready 事件,在事件回撥中做了反序列化的操作,但是在反序列化之前,為什麼要將場景中的所有節點 Clear 掉呢?是因為頁面有可能是斷線重連,如果是斷線重連的話,沒有將場景中的節點都 Clear 掉的話,反序列化後就會有節點重疊了,而且 Tag 屬性也不再是唯一的了,所以這時候操作節點的話,將會很混亂;最後呢,就是監聽伺服器的 result 事件,在事件的回撥中,跟新回撥引數中對應節點的位置資訊,但是其中做了些過濾,這是過濾正在移動的節點,因為正在移動的節點位置是認為控制的,所有不需要更新其節點位置資訊。
那麼實時資料通訊系列到這裡就介紹完了,如有什麼問題,歡迎批評指正。