关于javascript:从0实现一个流程渲染引擎

5次阅读

共计 4772 个字符,预计需要花费 12 分钟才能阅读完成。

缘起

当初前端最火的莫过于低代码编辑器这块了,有可视化编辑器、流程编辑器、文章编辑器、脑图编辑器等等

而低代码编辑器最难的和最有技术含量的,莫过于渲染引擎这块了,个别用户应用低代码编辑器进行利落拽操作,最初是生成一个 JSON,而依据这个 JSON 再把画面渲染进去,就是低代码引擎做的事件了,引擎提供若干 API,通过调用引擎的 API,来实现向画布增加编辑删除节点、连线、序列化 / 反序列化、历史治理等。

这次实现的是一个流程渲染引擎,流程能够用来绘制工作流、工作流等,用来做工作编排等,如下图:

简介

实现了一个轻量级的 web 端流程渲染引擎,我称之为 SF.js,意为 simple flow,能够用来渲染相似工作流、业务流等流程图,流程图最终能够通过序列化保留成一个 json 构造存储起来,
后续再通过该引擎反序列化 json 数据到画布上。


git 地址:https://github.com/501351981/…
代码很简略,感兴趣的能够试一下,如果有帮忙,记得帮忙点赞。


外围能力包含:

  • 反对自定义注册不同类型的节点(输出节点、解决节点、输入节点等),配置节点款式
  • 反对在画布上增加节点、挪动节点、批改节点配置属性、删除节点等
  • 反对节点之间进行拖拽连线
  • 反对图纸的序列化和反序列化,数据格式为 json,可存储到数据库
  • 反对历史治理,能够进行 undo、redo 操作
  • 反对缩放画布
  • 反对框选
  • 反对复制 / 粘贴 / 删除等快捷键操作
  • 反对单选、多选节点
  • 基于 SVG 进行节点渲染,放大放大不失真

SF.js 次要包含由以下几个类形成:

  • GraphView:画布模型,负责画布相干的解决,包含初始化画布、事件绑定、快捷键绑定
  • DataModel:数据模型,负责图纸序列化 / 反序列化,增加、删除节点、增加连线,遍历节点,依据 id 获取节点信息等,通过对 dataModel 操作,实现画布的渲染,个别不间接操作 GraphView
  • SelectionModel:抉择模型,负责管理节点选中相干操作,单选、全选、勾销抉择、获取以后选中节点等
  • HistoryManager:历史治理模型,负责存储操作记录,反对 undo 和 redo
  • Node:节点模型,设置节点宽高 / 地位、业务属性、画布上的渲染(draw 和 redraw)
  • Wire:连线模型,负责节点之间的连线在画布上渲染(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 则能够进行各种编辑操作(增加 / 删除 / 批改节点等); 设为 false 个别用于运行态,只容许查看
    }
})

实例化 GraphView 时能够传入一些参数,来指定画布的默认款式,如宽高,最大 / 最小缩放比例等

注册节点

节点就是在画布上显示的一个一个的性能节点,不同类型的节点有不同的款式(背景色、文本色彩、icon、输出节点数量,输入节点数量,默认宽高)

语法:第一个参数为节点类型,第二个参数为配置项

graphView.registerNode(nodeType,options)

如,咱们注册 3 种节点,输出节点、函数解决节点和调试节点

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
})

在画布上增加节点和连线

可通过 new SF.Node(options) 来实例化一个节点,options 的选项有

  • type:节点类型,即在下面 graphView.registerNode(nodeType) 时的 type,指明要创立的节点是什么类型
  • id:可选,如未设置,SF 会主动创立一个 id
  • p:可选,节点的零碎属性,包含宽高地位和名称,

    • width:节点宽度
    • height:节点高度
    • position:地位,形如,{x:100,y:100}
    • displayName:节点名称,显示在节点之上
  • a:可选,Object,节点的业务属性,可用来存储节点的业务信息,如节点可能须要对外裸露一些须要绑定的属性,用户输出属性值之后,存储在这里

    • 比方该节点是个脚本节点,那须要存储节点的具体脚本,那么咱们能够把这个脚本信息,存在 a 属性中,通过 node.a(“script”,”function(){}”)
  • wires:可选,连线信息,存储该节点前面链接那些节点,数组如,[[“nodeId1”,’nodeId2′]],代表第一个 output 端口链接 nodeId1 和 nodeId2 两个节点
let node = new SF.Node({type: 'inject',})

node.setPosition(100,100) // 实例化节点时可先不晓得地位,而后通过办法调整地位
node.setDisplayName("定时触发流程")
dataModel.add(node) // 如果不增加到 dataModel,那么不会在画布上显示 

创立连线通过 new SF.Wires({source: sourceNode, target: targetNode}) 来实现

let node1 = new SF.Node({type: 'inject',})
node1.setPosition(100,100)
node1.setDisplayName("定时触发流程")


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,后续能够进行存储,如调用接口存储到数据库

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'))

正文完
 0