Svg Flow Editor 原生svg流程图编辑器(一)
效果展示
项目概述
svg flow editor 是一款流程图编辑器,提供了一系列流程图交互、编辑所必需的功能,支持前端研发自定义开发各种逻辑编排场景,如流程图、ER 图、BPMN 流程等。
目前也有比较好的流程图设计框架,但是还是难满足项目个性化定制,BMPN.js、Jsplumb 的拓展能力不足,自定义节点支持成本很高。
技术选型
本项目使用typescript与svg、canvas等技术进行搭建,脱离vue、react等框架的限制,使得用户更快、更轻松融合到自己的项目中,在底层结合typescript,使得数据类型得到更加健壮、完整的支持,对图形元组使用 svg 技术进行绘制,使得用户操作、底层实现更加轻松,同时对其他模块(背景网格、水印)使用了canvas技术进行绘制。
功能规划
本项目大体功能模块如下:
background 背景
背景模块支持网格绘制、水印的绘制、水印定制化配置等
graph
graph 是系统交互的核心元素,支持Rect(矩形)、Circle(圆形)、Ellipse(椭圆)、Polygon(多边形)、Diamond(菱形)、Triangle(三角形)、Text(文本)、HTML(HTML元素)、Image(图片)、Line(线)等多种类型,后期会考虑慢慢完善元件库
websocket
websocket 是用于处理用户协同的模块
graphData
graph Data 是双向绑定的数据管理模块
tools
工具模块,包含图片导出、一键美化、层级处理、布局方式、元件组合、辅助线等
apis
API 是外部访问内部实现执行动作、获取数据的窗口,并在设计上提供了command、adapt 两个类,在command中隔离内部对象,通过调用adapt实现数据的处理,放置用户通过command对象对内部对象进行风险操作
event
提供统一的事件处理机制,支持对内部事件的监听、外部事件的注册等,同时,还对graph元件的统一事件进行处理,例如元件的点击事件、双击事件等
history
历史记录管理模块,支持 redo undo version 等历史相关操作
项目架构
项目对外暴露基础操作,例如: svg 构造器、command api操作、event事件中心以及全局api,通过暴露对象 sfEditor,实现对内部的数据访问、对象操作等。在核心模块中,需要考虑用户的使用习惯,封装完整的工具类,实现流程图的基本操作、拓展功能。底层依赖了svg对项目元件库的基础元件进行创作,同时使用了canvas对背景网格、水印等进行绘制,使用html进行页面布局,并且提供了typescript的全类型支持。
在API设计的设计上,采取了Command CommandAdapt 两个类实现,Command中不进行用户方法的直接处理,增加adapt类进行方法中转,防止用户通过API直接操作核心类。Command调用 adapt 的实例方法,在adapt 中获取draw、svg 等核心类进行用户的响应。
未来的功能模块规划中,还是以协同为核心重点。
项目结构说明
如上图,核心类在 core 中,index.ts 向外暴露了API,main.ts 则是测试结果的入口文件,interface是类型文件,命名上基本上都是按功能模块走的。
Graph 实体类
构建 svg 对象
export class SVG {private xmlns!: string;private svg: Element;private svgID!: string;private draw: Draw; // 绘制实例private graphOption: IGraphOption | undefined;constructor(graphOption?: IGraphOption) {this.draw = new Draw();this.svgID = getNanoid();this.graphOption = graphOption;//SVG命名空间this.xmlns = graphOption?.xmlns || "http://www.w3.org/2000/svg";// 1. 判断是否存在当前命名空间的svgconst svgElement = this.draw.getSvg(this.xmlns);// 2. 如果存在 则保存if (svgElement) throw new Error(messageInfo.isHaveSvgElement); // 如果已经存在相同xmlns属性的svg 则报错// 3. 不存在 则创建新的 svgthis.svg = this.draw.createSvg(this.xmlns, this.svgID);}// 将当前创建的svg添加到html DOM 的节点上public addTo(container: string | Element) {this.draw.addTo(container, this.svg); // 添加到指定容器this.size(); // 设置默认大小const { gridLines, waterMark, waterMarkText } = this.graphOption || {};if (gridLines !== false) this.draw.gridLines(); // 绘制网格if (waterMark !== false) this.draw.waterMark(waterMarkText); // 绘制水印return this; // 返回 this 供链式调用}// 设置当前 svg 的大小public size(width?: number, height?: number) {this.svg.setAttribute("width", width?.toString() || "100%");this.svg.setAttribute("height", height?.toString() || "100%");return this;}
相关的draw方法:
import { messageInfo } from "../Message";// 绘制、DOM 操作的核心类 尽量将所有的DOM操作都汇集在该类中,防止多处操作DOM引起的其他问题
export class Draw {constructor() {}// 通过指定的 xmlns 获取 svgpublic getSvg(xmlns: string) {return document.querySelector(`svg[xmlns="${xmlns}"]`);}// 创建 svgpublic createSvg(xmlns: string, svgID: string) {const svg = document.createElementNS(xmlns, "svg");svg.setAttribute("ID", svgID);svg.setAttribute("xmlns", xmlns);svg.setAttribute("version", "1.1");svg.setAttribute("baseProfile", "full");return svg;}// 将创建 svg 添加到指定容器public addTo(container: string | Element, svg: Element) {const type = typeof container === "string";// 判断传入参数是选择器还是domlet dom = type ? document.querySelector(container) : container;dom?.appendChild(svg);}// 绘制网格线public gridLines() {console.log("gridLines");}// 绘制水印public waterMark(waterMarkText?: string) {const text = waterMarkText || messageInfo.waterMarkText;}// 清除网格线public clearGridLines() {}// 清除水印public clearWaterMark() {}
}
构建 Rect 类
import { Common } from "./Common";
import { SVG } from "./index";// 矩形类
export class Rect extends Common {private svg: SVG; // 根元素 svgprivate rect: Element;constructor(svg: SVG, width: number, height: number) {super();this.svg = svg;this.rect = super.getDraw().createRect(svg.getSvgXmlns());// 设置宽高this.setAttribute(width, height);// 将当前创建的元件添加到 svg 下super.addToSvg(this);}// 独有属性设置private setAttribute(width: number, height: number) {this.rect.setAttribute("width", width.toString());this.rect.setAttribute("height", height.toString());}// 获取基本Elementpublic getElement() {return this.rect;}// 获取 xmlnspublic getXmlns() {return this.svg.getSvgXmlns();}
}
抽离公共类
svg 元件具有的公共方法,例如 设置位置信息、设置宽高、设置样式等,还有事件处理机制,都是每一个元件都拥有的方法属性,因此,抽离为独立的类,实现 元件集成即可。
// svg 元件公共类import { IGraphAttributes } from "../../interface/Graph";
import { Draw } from "../Draw";
import { Rect } from "./Rect";// 定义元件类型
type IGraph = Rect;export class Common {private draw: Draw;constructor() {this.draw = new Draw();}// 设置元件IDpublic setID() {}// 获取IDpublic getID() {const element = (this as unknown as IGraph).getElement();return this.draw.getID(element);}// 将创建的元件 添加到 svg 下protected addToSvg(graph: IGraph) {// 创建了基本元件后,需要构建 g 分组,方便处理 hover 及 click 的锚点const xmlns = graph.getXmlns();const element = graph.getElement();const nodeID = graph.getID() as string;// 1. 获取分组const group = this.draw.createGroup(element, xmlns, nodeID);// 2. 获取当前的 svg 根元素const svg = this.draw.getSvg(xmlns);// 3. 初始化默认属性this.attr.call(graph, {});// 3. 将当前分组添加到根元素上this.draw.addTo(svg as Element, group);}// 设置位置public position(x: number, y: number) {const graph = this as unknown as IGraph;const element = graph.getElement();// 因为设置位置属性的时候,不同的元素不一致,因此需要建立 原型与属性的映射const { tagName } = element;const attrMap: { [key: string]: string[] } = {rect: ["x", "y"],circle: ["cx", "cy"],ellipse: ["cx", "cy"],};element.setAttribute(attrMap[tagName][0], x.toString());element.setAttribute(attrMap[tagName][1], y.toString());// 重新渲染this.draw.updateLinkAnchorPoint(graph.getID() as string,element,graph.getXmlns());return this;}// 设置属性public attr({ stroke, fill }: IGraphAttributes) {// 设置样式const graph = this as unknown as IGraph;const element = graph.getElement();element.setAttribute("stroke", stroke || "black");element.setAttribute("fill", fill || "#F2F2F2");return this;}// 获取 draw 操作对象protected getDraw() {return this.draw;}
}
实现效果
公共事件处理机制
Common.ts// 为所有的子类构造事件public click!: (_fun: Function) => IGraph;public dblclick!: (_fun: Function) => IGraph;public mousedown!: (_fun: Function) => IGraph;public mousemove!: (_fun: Function) => IGraph;public mouseup!: (_fun: Function) => IGraph;public mouseover!: (_fun: Function) => IGraph;public mouseout!: (_fun: Function) => IGraph;// 初始化公共事件private initCommonEvent(graph: IGraph) {/*** 事件处理机制: 不管用户有没有添加 click ,都需要实现 addEventListener*/const eventList: IEventList = {click: (e: Event, graph: IGraph) => this.commonEvent.click(e, graph),};const element = graph.getElement();Object.keys(eventList).forEach((eventname) => {let userfun: null | Function;// @ts-ignore 用户自定义事件graph[eventname] = (_fun: Function | null) => {userfun = _fun;return graph;};// 给元素添加事件element.addEventListener(eventname, (e) => {// 1. 先执行默认事件eventList[eventname](e, graph);// 在这里处理用户自定义的事件userfun && userfun(e);// 阻止事件冒泡e.preventDefault();});});}
全局指令
// 暴露对外操作API 需要经过 Command Adapt的中转,防止用户直接通过 Command 获取到内部对象
import { Draw } from "../Draw";
import { CommandAdapt } from "./CommandAdapt";export class Command {// 测试设置水印public executeWatermark: CommandAdapt["watermark"];constructor(draw: Draw) {const adapt = new CommandAdapt(draw);this.executeWatermark = adapt.watermark.bind(adapt);}
}
import { Draw } from "../Draw";// Command Adapt API 操作核心库
export class CommandAdapt {private draw: Draw;constructor(draw: Draw) {this.draw = draw;}public watermark() {console.log("watermark");}
}
事件机制
事件处理中主要使用event Bus 实现:
export class EventBus<EventMap> {private eventHub: Map<string, Set<Function>>constructor() {this.eventHub = new Map()}public on<K extends string & keyof EventMap>(eventName: K,callback: EventMap[K]) {if (!eventName || typeof callback !== 'function') returnconst eventSet = this.eventHub.get(eventName) || new Set()eventSet.add(callback)this.eventHub.set(eventName, eventSet)}public emit<K extends string & keyof EventMap>(eventName: K,payload?: EventMap[K] extends (payload: infer P) => void ? P : never) {if (!eventName) returnconst callBackSet = this.eventHub.get(eventName)if (!callBackSet) returnif (callBackSet.size === 1) {const callBack = [...callBackSet]return callBack[0](payload)}callBackSet.forEach(callBack => callBack(payload))}public off<K extends string & keyof EventMap>(eventName: K,callback: EventMap[K]) {if (!eventName || typeof callback !== 'function') returnconst callBackSet = this.eventHub.get(eventName)if (!callBackSet) returncallBackSet.delete(callback)}public isSubscribe<K extends string & keyof EventMap>(eventName: K): boolean {const eventSet = this.eventHub.get(eventName)return !!eventSet && eventSet.size > 0}
}
总结
至此,整体项目的框架已经跑通了,包括API的封装(command adapt)、事件处理机制、svg元件构建,本文先处理这么多事情。
相关文章:

Svg Flow Editor 原生svg流程图编辑器(一)
效果展示 项目概述 svg flow editor 是一款流程图编辑器,提供了一系列流程图交互、编辑所必需的功能,支持前端研发自定义开发各种逻辑编排场景,如流程图、ER 图、BPMN 流程等。 目前也有比较好的流程图设计框架,但是还是难满足项目…...

头像剪切上传
头像剪切上传 文章说明核心Api示例源码效果展示源码下载 文章说明 本文主要为了学习头像裁剪功能,以及熟悉canvas绘图和转文件的相关操作,参考教程(Web渡一前端–图片裁剪上传原理) 核心Api 主要就一个在canvas绘图的操作 context…...
24计算机考研调剂 | 北京信息科技大学
北京信息科技大学接收调剂研究生 考研调剂招生信息 学校:北京信息科技大学 专业:工学->控制科学与工程->控制理论与控制工程 年级:2024 招生人数:- 招生状态:正在招生中 联系方式:********* (为保护个人隐私,联系方式仅限APP查看) 补充内容 各位同学,…...

06 - 镜像管理
1 了解镜像 Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。 但注意, 镜像不包含任何动态数据&#…...

最简单 导航栏 html css
dhl.html <!DOCTYPE html> <html><head><meta charset"utf-8"><title>导航栏</title><link type"text/css" rel"stylesheet" href"css/dhl.css"></head><div class"dhl&quo…...
PostgreSQL的学习心得和知识总结(一百三十一)|深入理解PostgreSQL数据库如何使用psql中的变量
目录结构 注:提前言明 本文借鉴了以下博主、书籍或网站的内容,其列表如下: 1、参考书籍:《PostgreSQL数据库内核分析》 2、参考书籍:《数据库事务处理的艺术:事务管理与并发控制》 3、PostgreSQL数据库仓库链接,点击前往 4、日本著名PostgreSQL数据库专家 铃木启修 网站…...

支付宝小程序模板开发,实现代小程序备案申请
大家好,我是小悟 支付宝小程序备案整体流程总共分为五个环节:备案信息填写、平台初审、工信部短信核验、通管局审核和备案成功。 服务商可以代小程序发起备案申请。在申请小程序备案之前,需要确保小程序基本信息已填写完成、小程序至少存在一…...
怎么培养孩子的学习习惯?
问:在亲子阅读中,应该用哪些方法引导孩子自己主动阅读呢? 有很多家长会问如何培养孩子主动阅读的兴趣? 我想给你四个词来分享,分别是环境、选择的权利、适龄,还有增强回路。第一个环境,就是把…...

deeplearning with pytorch (三)
一.基本概念 1.Convolutional Neural Network Intro mnist数据集 2.Image Filter / Image Kernel Image Kernels explained visually 访问这个网站可以直观看到image kernels对图片的影响 3.Convolutional Layer and RGB 为什么要用巻积神经网络代替人工神经网络 上图是…...
LLaMA模型的发布与创新潮流
时间线从2023年2月24日开始,Meta推出了LLaMA模型,虽然开源了代码,却没有开源模型权重。LLaMA模型可以说是相对较小的,拥有7B、13B、33B和65B参数的几种版本,训练时间相对较长,因此相对于其大小而言能力强大…...

Python之Web开发初学者教程—ubuntu中安装配置redis
Python之Web开发初学者教程—ubuntu中安装配置redis 准备环境:VMWare,ubuntu18.04.6 LTS 一、安装 从 Ubuntu 存储库安装 Redis,打开Ubuntu的终端,输入下面的命令: sudo apt-get install redis-server (…...

如何在Vue中进行单元测试?
前端开发中,单元测试是一个非常重要的环节,它可以帮助我们在开发过程中发现潜在的问题,并确保我们的代码在不断迭代的过程中依然能够保持稳定。在Vue中进行单元测试同样非常重要,本文将介绍如何在Vue项目中进行单元测试。 在Vue中…...

开源组件安全风险及应对
在软件开发的过程中,为了提升开发效率、软件质量和稳定性,并降低开发成本,使用开源组件是开发人员的不二选择(实际上,所有软件开发技术的演进都是为了能够更短时间、更低成本地构建软件)。这里的开源组件指…...

nginx出现 “414 request-uri too large”
nginx出现 “414 request-uri too large” 1.修改传参方式 POST 2.字段能变成后端获取就自己获取,不用前端传 3.修改nginx配置,添加client_header_buffer_size 512k;large_client_header_buffers 4 512k;配置...

堆和二叉树的动态实现(C语言实现)
✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨ 🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿…...

Vue前端+快速入门【详解】
目录 1.Vue概述 2. 快速入门 3. Vue指令 4.表格信息案例 5. 生命周期 1.Vue概述 1.MVVM思想 原始HTMLCSSJavaScript开发存在的问题:操作麻烦,耦合性强 为了实现html标签与数据的解耦,前端开发中提供了MVVM思想:即Model-Vi…...

day06_菜单管理(查询菜单,添加菜单,添加子菜单,修改菜单,删除菜单,角色分配菜单,查询菜单,保存菜单,动态菜单)
文章目录 1 菜单管理1.1 表结构介绍1.2 查询菜单1.2.1 需求说明1.2.2 页面制作1.2.3 后端接口SysMenuSysMenuControllerSysMenuServiceMenuHelperSysMenuMapperSysMenuMapper.xml 1.2.4 前端对接sysMenu.jssysMenu.vue 1.3 添加菜单1.3.1 需求说明1.3.3 页面制作1.3.3 后端接口…...
探究与以太坊智能合约的交互
# 概述 智能合约是部署在区块链上的一串代代码,通常我们与智能合约的打交道 可以通过前端的Dapp,etherscan,metamask 等方式。作为开发人员可以通过调用提供的相关包来与之交互,如web3.js,ether.js , web3.j(java 语言…...

Windows如何安装docker-desktop
下载 docker-desktop设置环境安装wsl可能遇到的错误 下载 docker-desktop 下载官网:https://www.docker.com/products/docker-desktop/ 设置环境 如果没有Hyper-V选项的,按照以下步骤 添加一个文件Hyper-V.bat 添加以下内容,并双击运行后重启电脑 pushd "%~…...
芯片设计后端遇到的各种文件类型和文件后缀
芯片设计后端遇到的各种文件类型和文件后缀 文件类型 描述 文件后缀 netlist网表文件 verilog文件格式,记录了芯片里各个instance的逻辑连接关系 .v (for Verilog netlists) Lib,liberty timing file 记录了cell的timing信息及一定power信息。有的…...

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明
LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造,完美适配AGV和无人叉车。同时,集成以太网与语音合成技术,为各类高级系统(如MES、调度系统、库位管理、立库等)提供高效便捷的语音交互体验。 L…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...

使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...