緣起
現在前端最火的莫過于低代碼編輯器這塊了,有可視化編輯器、流程編輯器、文章編輯器、腦圖編輯器等等
而低代碼編輯器最難的和最有技術含量的,莫過于渲染引擎這塊了,一般用戶使用低代碼編輯器進行拖拉拽操作,最后是生成一個JSON,而根據這個JSON再把畫面渲染出來,就是低代碼引擎做的事情了,引擎提供若干API,通過調用引擎的API,來實現向畫布添加編輯刪除節(jié)點、連線、序列化/反序列化、歷史管理等。
這次實現的是一個流程渲染引擎,流程可以用來繪制任務流、工作流等,用來做任務編排等,如下圖:

簡介
實現了一個輕量級的web端流程渲染引擎,我稱之為SF.js,意為simple flow,可以用來渲染類似工作流、業(yè)務流等流程圖,流程圖最終可以通過序列化保存成一個json結構存儲起來,
后續(xù)再通過該引擎反序列化json數據到畫布上。
git地址:https://github.com/501351981/simple-flow-web
代碼很簡單,感興趣的可以試一下,如果有幫助,記得幫忙點贊。
核心能力包括:
- 支持自定義注冊不同類型的節(jié)點(輸入節(jié)點、處理節(jié)點、輸出節(jié)點等),配置節(jié)點樣式
- 支持在畫布上添加節(jié)點、移動節(jié)點、修改節(jié)點配置屬性、刪除節(jié)點等
- 支持節(jié)點之間進行拖拽連線
- 支持圖紙的序列化和反序列化,數據格式為json,可存儲到數據庫
- 支持歷史管理,可以進行undo、redo操作
- 支持縮放畫布
- 支持框選
- 支持復制/粘貼/刪除等快捷鍵操作
- 支持單選、多選節(jié)點
- 基于SVG進行節(jié)點渲染,放大縮小不失真
SF.js主要包括由以下幾個類構成:
- GraphView:畫布模型,負責畫布相關的處理,包括初始化畫布、事件綁定、快捷鍵綁定
- DataModel:數據模型,負責圖紙序列化/反序列化,添加、刪除節(jié)點、添加連線,遍歷節(jié)點,根據id獲取節(jié)點信息等, 通過對dataModel操作,實現畫布的渲染,一般不直接操作GraphView
- SelectionModel:選擇模型,負責管理節(jié)點選中相關操作,單選、全選、取消選擇、獲取當前選中節(jié)點等
- HistoryManager:歷史管理模型,負責存儲操作記錄,支持undo和redo
- Node:節(jié)點模型,設置節(jié)點寬高/位置、業(yè)務屬性、畫布上的渲染(draw和redraw)
- Wire:連線模型,負責節(jié)點之間的連線在畫布上渲染(draw和redraw)
安裝使用
通過html直接引入
可下載lib目錄下的文件sf.js和sf.css,在html中直接引入
<html>
<head>
<!-- 引入sf.js和sf.css -->
<link rel="stylesheet" href="./lib/sf.css">
<script src="./lib/sf.js"></script>
</head>
<body>
...
</body>
</html>
通過npm安裝
通過npm install安裝simple-flow-web
npm install simple-flow-web
在項目中通過import引入
import SF from 'simple-flow-web'
import 'simple-flow-web/lib/sf.css'
用法示例
實例化
由于HistoryManager和GraphView都需要用到數據模型DataModel,所以先實例化DataMode
let dataModel = new SF.DataModel()
let historyManager = new SF.HistoryManager(dataModel)
let graphView = new SF.GraphView(dataModel, {
graphView: {
width:6000,
height:6000,
scale:{
max:3
},
editable:true, //設為true則可以進行各種編輯操作(添加/刪除/修改節(jié)點等); 設為false一般用于運行態(tài),只允許查看
}
})
實例化GraphView時可以傳入一些參數,來指定畫布的默認樣式,如寬高,最大/最小縮放比例等
注冊節(jié)點
節(jié)點就是在畫布上顯示的一個一個的功能節(jié)點,不同類型的節(jié)點有不同的樣式(背景色、文本顏色、icon、輸入節(jié)點數量,輸出節(jié)點數量,默認寬高)
語法:第一個參數為節(jié)點類型,第二個參數為配置項
graphView.registerNode(nodeType,options)
如,我們注冊3種節(jié)點,輸入節(jié)點、函數處理節(jié)點和調試節(jié)點
graphView.registerNode('inject',{
class: 'node-inject',
align:'left',
category: 'common',
bgColor: '#a6bbcf',
color:'#fff',
defaults:{},
icon: require('../icons/node/inject.svg'),
inputs:0,
outputs:1,
width:150,
height: 40
})
graphView.registerNode('function',{
align:'left',
category: 'common',
bgColor: 'rgb(253, 208, 162)',
color:'#fff',
defaults:{},
icon: require('../icons/node/function.svg'),
inputs:1,
outputs:1,
width:150,
height: 40
})
graphView.registerNode('debug',{
align:'right',
category: 'common',
bgColor: '#87a980',
color:'#fff',
defaults:{},
icon: require('../icons/node/debug.svg'),
inputs:1,
outputs:0,
width:150,
height: 40
})
在畫布上添加節(jié)點和連線
可通過new SF.Node(options) 來實例化一個節(jié)點,options的選項有
type:節(jié)點類型,即在上面graphView.registerNode(nodeType)時的type,指明要創(chuàng)建的節(jié)點是什么類型
id:可選,如未設置,SF會自動創(chuàng)建一個id
-
p:可選,節(jié)點的系統(tǒng)屬性,包括寬高位置和名稱,
- width:節(jié)點寬度
- height:節(jié)點高度
- position:位置,形如,{x:100,y:100}
- displayName:節(jié)點名稱,顯示在節(jié)點之上
-
a:可選,Object,節(jié)點的業(yè)務屬性,可用來存儲節(jié)點的業(yè)務信息,如節(jié)點可能需要對外暴露一些需要綁定的屬性,用戶輸入屬性值之后,存儲在這里
- 比如該節(jié)點是個腳本節(jié)點,那需要存儲節(jié)點的具體腳本, 那么我們可以把這個腳本信息,存在a屬性中,通過node.a("script","function(){}")
wires:可選,連線信息,存儲該節(jié)點后面鏈接那些節(jié)點,數組如,[["nodeId1",'nodeId2']],代表第一個output端口鏈接nodeId1和nodeId2兩個節(jié)點
let node = new SF.Node({
type: 'inject',
})
node.setPosition(100,100) //實例化節(jié)點時可先不知道位置,然后通過方法調整位置
node.setDisplayName("定時觸發(fā)流程")
dataModel.add(node) //如果不添加到dataModel,那么不會在畫布上顯示
創(chuàng)建連線通過new SF.Wires({source: sourceNode, target: targetNode })來實現
let node1 = new SF.Node({
type: 'inject',
})
node1.setPosition(100,100)
node1.setDisplayName("定時觸發(fā)流程")
let node2 = new SF.Node({
type: 'function',
})
node2.setPosition(300,200)
node2.setDisplayName("函數組件")
let wire = new SF.Wires({
source: node1,
target: node2
})
dataModel.add(node1)
dataModel.add(node2)
dataModel.add(wire)
序列化圖紙為json
繪制完圖紙后,希望將圖紙序列化為JSON,后續(xù)可以進行存儲,如調用接口存儲到數據庫
let json = dataModel.serialize()
//可調用接口將json存儲到數據庫
反序列化圖紙到畫布上
我們一般真實使用時,是先有圖紙的信息,JSON格式,然后通過反序列化,渲染到圖紙上
//圖紙json正常是通過接口請求回來的
let json = {"v":"1.0.0","p":{"width":5000,"height":5000,"gridSize":20,"background":"#fff"},"a":{"init":true},"d":[{"type":"inject","id":"1aa6129ca0eb2042","p":{"displayName":"注入數據","position":{"x":295,"y":106},"width":200,"height":40},"a":{"payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1},"wires":[["49536505a4488892"]]},{"type":"function","id":"49536505a4488892","p":{"displayName":"函數處理","position":{"x":565,"y":117},"width":200,"height":40},"a":{"payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1},"wires":[["a2a0ae774c68190b"]]},{"type":"function","id":"a2a0ae774c68190b","p":{"displayName":"函數處理2","position":{"x":589,"y":217},"width":200,"height":40},"a":{"payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1},"wires":[["cbe4c17ebc4b7c03"]]},{"type":"debug","id":"cbe4c17ebc4b7c03","p":{"displayName":"調試","position":{"x":911,"y":229},"width":150,"height":40},"a":{"payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1},"wires":[]}]}
dataModel.deserialize(json)
將畫布掛載到頁面上
在畫布掛載到dom之前,頁面上是不顯示的,可通過addToDom將畫布掛載到頁面上
graphView.addToDom(document.getElementById('simple-flow-wrapper'))