從0實現一個流程渲染引擎

緣起

現在前端最火的莫過于低代碼編輯器這塊了,有可視化編輯器、流程編輯器、文章編輯器、腦圖編輯器等等

而低代碼編輯器最難的和最有技術含量的,莫過于渲染引擎這塊了,一般用戶使用低代碼編輯器進行拖拉拽操作,最后是生成一個JSON,而根據這個JSON再把畫面渲染出來,就是低代碼引擎做的事情了,引擎提供若干API,通過調用引擎的API,來實現向畫布添加編輯刪除節(jié)點、連線、序列化/反序列化、歷史管理等。

這次實現的是一個流程渲染引擎,流程可以用來繪制任務流、工作流等,用來做任務編排等,如下圖:

preview.png

簡介

實現了一個輕量級的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'))
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容