jsPlumb之流程圖項目總結及實例
在使用jsPlumb過程中,所遇到的問題,以及解決方案,文中引用了《數據結構與演算法JavaScript描述》的相關圖片和一部分代碼.截圖是有點多,有時比較懶,沒有太多的時間去詳細的編輯.
前言
首先是UML類圖
然後是流程圖
使用了jsPlumb的相關功能,初版是可以看到雛形了,差不多用了兩個月的時間,中間斷斷續續的又有其它工作穿插,但還是把基本功能做出來了.
其實做完了之後,才發現jsPlumb的功能,只用到了很少的一部分,更多的是對於內部數據結構的理解和實現,只能說做到了數據同步更新,距離數據驅動仍然有一定的距離.
這裡會總結和記錄一下項目中遇到的問題,和解決的方法,如果有更好的方法,歡迎指出.
對於連線上的多個標籤的處理如上圖所示,一開始是認為是否是要在連線時,配置兩個overlays,
var j = jsPlumb.getInstance;
j.connect({
source:source,
target:target,
overlays:[
"Arrow",
["label",{label:"foo1",location:0.25,id:"m1"}],
["label",{label:"foo2",location:0.75,id:"m2"}]
]
})
當然,這裡也有坑,如果id重複,那麼會使用最後一個,而不會重合,包括jsPlumb內部緩存的數據都只會剩下最後的那個.
後面發現,其實也可以通過importDefaults
函數來動態修改配置項.
j.importDefaults({
ConnectionOverlays: [
["Arrow", { location: 1, id: "arrow", length: 10, foldback: 0, width: 10 }],
["Label", { label: "n", id: "label-n", location: 0.25, cssClass: "jspl-label" }],
["Label", { label: "1", id: "label-1", location: 0.75, cssClass: "jspl-label" }]
]
})
只不過這樣,只會在運行了函數之後的連線里,才能有兩個標籤顯示,而之前的則無法一起變化. 所以為了方便,直接在初始化里將其給修改了.
Groups的使用在做流程圖時,Group確實是個問題,如上圖的無限嵌套層級中,就無法使用jsPlumb提供的Groups
功能.
按照文檔中來說,如果標識一個元素為組,則該組中的元素則會跟隨組的移動而移動,連線也是,但問題就是一旦一個元素成為組了,那就不能接受其它組元素了,換句話說,它所提供的的Groups方法只有一層,自然無法滿足要求.
先把總結的組的用法貼出來:
j.addGroup({
el:el,
id:"one"
constrain:true, // 子元素僅限在元素內拖動
droppable:true, // 子元素是否可以放置其他元素
draggable:true, // 默認為true,組是否可以拖動
dropOverride:true ,// 組中的元素是否可以拓展到其他組,為true時表示否,這裡的拓展會對dom結構進行修改,而非單純的位置移動
ghost:true, // 是否創建一個子元素的副本元素
revert:true, // 元素是否可以拖到只有邊框可以重合
})
後面採用了新的方式,在節點移動時,動態刷新連線
j.repaintEverything;
而為了不阻塞頁面,需要用到函數節流throttle
function throttle(fn,interval){
var canRun = true;
return function{
if(!canRun) return;
canRun = false;
setTimeout(function{
fn.apply(this,arguments);
canRun = true;
},interval ? interval : 300);
};
};
這是一個簡單的實現方式,主要就是為了減少dom中事件移動時重複調用的事件,同時達到執行事件的目的(只允許一個函數在x毫秒內執行一次);
當然,也可以使用underscore.js中自帶的_.throttle
函數,同樣可以達到目的.
這裡的html結構就使用了嵌套的層級,將父級和子級使用這種層級保存到內部的數據源里
多層or一層 數據結構解析類似這種實際存在嵌套關係的數據體,有兩種方式可以進行管理,
多層級嵌套:類似
[
{
id:"1",
child:{
id:"2",
child:{
id:"3",
child:{}
}
}
}
]
用來進行管理的話,優點是直觀,能根據層級就知道整體結構大概是多少,轉換成xml或者html也很方便. 但缺點就是進行查找和修改,並不是那麼方便.
一層展示所有節點:類似
[
{
id:"1",
child:[{
id:"2"
}]
},
{
id:"2",
parentId:"1",
child:[{
id:"3"
}]
},
{
id:"3",
parentId:"2",
child:
}
]
這種結構好處就是全部在一個層級中,查找起來和修改數據非常方便,而如果想要解析成多層級的結構,只需要運用遞歸,來生成新結構:
function mt{
var OBJ;
this.root = null;
this.Node = function(e) {
this.id = e.id;
this.name = e.name;
this.parentId = e.parentId;
this.children = ;
};
this.insert=function(e,key){ 將一層的數組通過初始化函數 如果想轉成html結構,只需要稍微改下函數,就可以實現了. 這個就完全得靠演算法來實現了.首先,對於圖的理解是重點 我也懶得打字了,直接用圖表示一下,基本的圖大致是這樣,而具體的表現形式則是 可以看到,基礎的圖的表現形式,可以用一個鄰接表來表示; 而實現,則可以看到下列的代碼:
function add(obj,e){
if(obj.id == e.parentId){
obj.children.push(e);
} else {
for (var i = 0; i < obj.children.length; i++) {
add(obj.children[i], e);
}
}
}
if (e != undefined) {
e = new this.Node(e);
} else {
return;
}
if (this.root == null) {
this.root = e;
} else {
OBJ = this.root;
add(OBJ, e);
}
}
this.init = function(data){
var _this = this;
for(var i = 0;iinit
,就可以轉為多層級
function Graph1(v) {
this.vertices = v; // 總頂點
this.edges = 0; // 圖的邊數
this.adj = ;
// 通過 for 循環為數組中的每個元素添加一個子數組來存儲所有的相鄰頂點,[並將所有元素初始化為空字元串。]?
for (var i = 0; i < this.vertices; ++i) {
this.adj[i] = ;
}
/**
* 當調用這個函數並傳入頂點 v 和 w 時,函數會先查找頂點 v 的鄰接表,將頂點 w 添加到列表中
* 然後再查找頂點 w 的鄰接表,將頂點 v 加入列表。最後,這個函數會將邊數加 1。
* @param {[type]} v [第一個頂點]
* @param {[type]} w [第二個頂點]
*/
this.addEdge = function(v, w) {
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
}
/**
* 列印所有頂點的關係簡單表現形式
* @return {[type]} [description]
*/
this.showGraph = function {
for (var i = 0; i < this.vertices; ++i) {
var str = i + " ->";
for (var j = 0; j < this.vertices; ++j) {
if (this.adj[i][j] != undefined) {
str += this.adj[i][j] + " "
}
}
console.log("表現形式為:" + str);
}
console.log(this.adj);
}
}
而光構建是不夠的,所以來看下基礎的搜索方法: 深度優先搜索和廣度優先搜索;
深度優先搜索先從初始節點開始訪問,並標記為已訪問過的狀態,再遞歸的去訪問在初始節點的鄰接表中其他沒有訪問過的節點,依次之後,就能訪問過所有的節點了
/**
* 深度優先搜索演算法
* 這裡不需要頂點,也就是鄰接表的初始點
*/
this.dfs = (v) {
this.marked[v] = true;
for (var w of this.adj[v]) {
if (!this.marked[w]) {
this.dfs(w);
}
}
}
根據圖片和上述的代碼,可以看出深度搜索其實可以做很多其他的擴展
廣度優先搜索 /**
* 廣度優先搜索演算法
* @param {[type]} s [description]
*/
this.bfs = function(s) {
var queue = ;
this.marked[s] = true;
queue.push(s); // 添加到隊尾
while (queue.length > 0) {
var v = queue.shift; // 從隊首移除
console.log("Visisted vertex: " + v);
for (var w of this.adj[v]) {
if (!this.marked[w]) {
this.edgeTo[w] = v;
this.marked[w] = true;
queue.push(w);
}
}
}
}
而如果看了《數據結構與演算法JavaScript描述》這本書,有興趣的可以去實現下查找最短路徑
和拓撲排序
;
這算是找到的比較能理解的方式來計算
以上圖為例,這是一個簡單的流程圖,可以很簡單的看出,右邊的流程實際上是未完成的,因為無法到達終點,所以是一個非法點,而通過上面的深度搜索,可以看出,只要對深度優先搜索演算法進行一定的修改,那麼就可以找到從開始到結束的所有的路徑,再通過對比,就可以知道哪些點無法到達終點,從而確定非法點. 上代碼:
/**
* 深度搜索,dfs,解兩點之間所有路徑
* @param {[type]} v [description]
* @return {[type]} [description]
*/
function Graph2(v) {
var _this = this;
this.vertices = v; // 總頂點
this.edges = 0; //圖的起始邊數
this.adj = ; //內部鄰接表表現形式
this.marked = ; // 內部頂點訪問狀態,與鄰接表對應
this.path = ; // 路徑表示
this.lines = ; // 所有路徑匯總
for (var i = 0; i < this.vertices; ++i) { _this.adj[i] = ; } /** * 初始化訪問狀態 * @return {[type]} [description] */ this.initMarked = function { for (var i = 0; i < _this.vertices; ++i) { _this.marked[i] = false; } }; /** * 在鄰接表中增加節點 * @param {[type]} v [description] * @param {[type]} w [description] */ this.addEdge = function(v, w) { this.adj[v].push(w); this.edges++; }; /** * 返回生成的鄰接表 * @return {[type]} [description] */ this.showGraph = function { return this.adj; }; /** * 深度搜索演算法 * @param {[type]} v [起點] * @param {[type]} d [終點] * @param {[type]} path [路徑] * @return {[type]} [description] */ this.dfs = function(v, d, path) { var _this = this; this.marked[v] = true; path.push(v); if (v == d) { var arr = ; for (var i = 0; i < path.length; i++) { arr.push(path[i]); } _this.lines.push(arr); } else { for (var w of this.adj[v]) { if (!this.marked[w]) { this.dfs(w, d, path); } } } path.pop; this.marked[v] = false; }; this.verify = function(arr, start, end) { this.initMarked; for (var i = 0; i < arr.length; i++) { _this.addEdge(arr[i].from, arr[i].to); } this.dfs(start, end, this.path); return this.lines; }; }
可以看出修改了addEdge
函數,將鄰接表中的雙向記錄改為單向記錄,可以有效避免下圖的錯誤計算:
只計算起點到終點的所有連線有時並不客觀,如果出現
這種情況的話,實際上深度遍歷並不能計算出最右邊的節點是合法的,那麼就需要重新修改起點和終點,來推導是否能夠到達終點.從而判定該點是否合法.至於其他的,只是多了個返回值,存儲了一下計算出來的所有路徑.
而在dfs函數中,當滿足能夠從起點走到終點的,則記錄下當前的path中的值,保存到lines中去,而每一次對於path的推入或者推出,保證了只有滿足條件的點,才能被返回;
而this.marked[v] = false
,則確保了,在每一次重新計算路徑時,都會驗證每個點是否存在不同的相對於終點能夠到達的路徑是否存在.
當然,一定會有更加簡單的方法,我這裡只是稍微修改了下基礎的代碼!
redo和undo這是我覺得最簡單卻耗時最久的功能,思路都知道:創建一個隊列,記錄每一次創建一個流程節點,刪除一個流程節點,建立一個新的關聯關係,刪除一個新的關聯關係等,都需要記錄下來,再通過統一的介面來訪問隊列,執行操作.
但在具體實現上,jsPlumb的remove確實需要注意一下:
首先,如果需要刪除連線,那麼使用jsPlumb提供的detach
方法,就可以刪除連線,注意,傳入的數據應該是connection
對象.
當然,也可以使用remove
方法,參數為選擇器或者element對象都可以,這個方法刪除的是一個節點,包括節點上所有的線.
而jsPlumb中會內部緩存所有的數據,用於刷新,和重連.
那麼當我移除一個多層級且內部有連線的情況時,如果只刪除最外層的元素,那麼內部的連線實際上並沒有清除,所以當redo或者移動時,會出現連線的端點有一端會跑到坐標原點,也就是div上(0,0)的地方去.所以清除時,需要注意,要把內部的所有節點依次清除,才不會發生一些莫名其妙的bug.
而在刪除和連接連線上,我使用了jsPlumb提供的事件bind("connection")
和bind("connectionDetached")
,用於判斷一條連線被連接或者刪除.而在記錄這裡的redo和undo事件時,尤其要注意,需要首先確定刪除和連接時的連線的類型,否則會產生額外的隊列事件.
因此,在使用連接事件時,就可以使用
jsPlumb.connect({
source:"foo",
target:"bar",
parameters:{
"p1":34,
"p2":new Date,
"p3":function { console.log("i am p3"); }
}
});
來進行類型的傳參,這樣事件觸發時就可以分類處理.
也可以使用connection.setData
事件,參數可以指定任意的值,通過connection.getData
方法,就可以拿到相應的數據了.
而redo和undo本身確實沒有什麼東西
var defaults = {
"name": "mutation",
"afterAddServe":$.noop,
"afterUndo":$.noop,
"afterRedo":$.noop
}
var mutation = function(options){
this.options = $.extend(true,{},defaults,options);
this.list = ;
this.index = 0;
};
mutation.prototype = {
addServe:function(undo,redo){
if(!_.isFunction(undo) || !_.isFunction(redo)) return false;
// 說明是在有後續操作時,更新了隊列
if(this.canRedo){
this.splice(this.index+1);
};
this.list.push({
undo:undo,
redo:redo
});
console.log(this.list);
this.index = this.list.length - 1;
_.isFunction(this.options.afterAddServe) && this.options.afterAddServe(this.canUndo,this.canRedo);
},
/**
* 相當於保存之後清空之前的所有保存的操作
* @return {[type]} [description]
*/
reset:function{
this.list = ;
this.index = 0;
},
/**
* 當破壞原來隊列時,需要對隊列進行修改,
* index開始的所有存儲值都沒有用了
* @param {[type]} index [description]
* @return {[type]} [description]
*/
splice:function(index){
this.list.splice(index);
},
/**
* 撤銷操作
* @return {[type]} [description]
*/
undo:function{
if(this.canUndo){
this.list[this.index].undo;
this.index--;
_.isFunction(this.options.afterUndo) && this.options.afterUndo(this.canUndo,this.canRedo);
}
},
/**
* 重做操作
* @return {[type]} [description]
*/
redo:function{
if(this.canRedo){
this.index++;
this.list[this.index].redo;
_.isFunction(this.options.afterRedo) && this.options.afterRedo(this.canUndo,this.canRedo);
}
},
canUndo:function{
return this.index !== -1;
},
canRedo:function{
return this.list.length - 1 !== this.index;
}
}
return mutation;
每次在使用redo或者undo時,只需要判斷當前是否是隊列的尾端或者起始端,再確定是否redo或者undo就可以了.
調用時的undo
和redo
通過傳參,將不同的函數封裝進隊列里,就可以減少耦合度.
放大縮小
這裡想了想還是記錄一下,方法採用了最簡單的mousedown
和mousemove
,讓元素在節流中動態的變化大小,就可以了,
只需要用一個節點,在點擊元素時,根據元素的大小來確定該輔助節點四個點的位置,就可以了,只要監聽了這四個點的位置,再同步給該定位元素,就能實現這一效果,方法就不貼了,沒有太多東西
小結這次的項目我個人還是覺得蠻有意思的,可以學習新的演算法,了解新的數據結構,包括設計模式,也代入了其中,進行代碼的整合,所用到的中間件模式和發布訂閱者模式都讓我對於js有了一個新的理解.雖然已經用require來管理模塊,但結構仍然存在高度耦合的情況,應該還是被限制住了. 作為離職前的最後一次的項目來說,其實我感覺我的代碼能力仍然與年初沒有什麼太大的改變,也許是時候脫離安逸的環境,重新開始了.
※vue2.0引入騰訊地圖
※14:40-15:00博客站點web伺服器雪崩似的CPU 100%
※Elasticsearch學習隨筆(一)——原理理解與5.0核心插件部署過程
※net 中web.config一個配置文件解決方法 (其他配置文件引入方式)
TAG:達人科技 |
※用 jest 單元測試改善老舊的 Backbone.js 項目
※OpenStack自己的容器管理項目Zun的實踐
※Oculus啟動第三屆VR for Good計劃,繼續支持實驗性項目
※Oculus公布首批VR影視創作者實驗室Creators Lab入選項目
※Essentium、易生和Polymaker加入「Ultimaker材料聯盟項目」
※項目評級——RewardMob
※Google試圖僱用Vitalik Buterin進行秘密加密項目
※使用Jira software+Structure實現大規模跨團隊項目管理
※如何成為 Apache 項目的 committer
※Blazor正式成為Microsoft官方.NET 和WebAssembly項目
※Accurate檢測項目和意義
※TensorFlow 項目模板架構最佳實踐
※為刺激VR/AR領域發展 Digital Catapult再次啟動Augmentor項目
※Python web開發:Flask的項目配置
※教你使用Vue.js的DevTools來調試你的vue項目
※Google X實驗室又一項目「畢業」,熔鹽儲能項目Malta拆分並獨立
※專業解讀 Business Analytics項目
※springboot項目初始化
※即插即用 戴姆勒與Hubject、Ebee合作Plug&Charge充電項目
※項目簡說之block collider