• 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 对象

    1. export class SVG {
    2. private xmlns!: string;
    3. private svg: Element;
    4. private svgID!: string;
    5. private draw: Draw; // 绘制实例
    6. private graphOption: IGraphOption | undefined;
    7. constructor(graphOption?: IGraphOption) {
    8. this.draw = new Draw();
    9. this.svgID = getNanoid();
    10. this.graphOption = graphOption;
    11. //SVG命名空间
    12. this.xmlns = graphOption?.xmlns || "http://www.w3.org/2000/svg";
    13. // 1. 判断是否存在当前命名空间的svg
    14. const svgElement = this.draw.getSvg(this.xmlns);
    15. // 2. 如果存在 则保存
    16. if (svgElement) throw new Error(messageInfo.isHaveSvgElement); // 如果已经存在相同xmlns属性的svg 则报错
    17. // 3. 不存在 则创建新的 svg
    18. this.svg = this.draw.createSvg(this.xmlns, this.svgID);
    19. }
    20. // 将当前创建的svg添加到html DOM 的节点上
    21. public addTo(container: string | Element) {
    22. this.draw.addTo(container, this.svg); // 添加到指定容器
    23. this.size(); // 设置默认大小
    24. const { gridLines, waterMark, waterMarkText } = this.graphOption || {};
    25. if (gridLines !== false) this.draw.gridLines(); // 绘制网格
    26. if (waterMark !== false) this.draw.waterMark(waterMarkText); // 绘制水印
    27. return this; // 返回 this 供链式调用
    28. }
    29. // 设置当前 svg 的大小
    30. public size(width?: number, height?: number) {
    31. this.svg.setAttribute("width", width?.toString() || "100%");
    32. this.svg.setAttribute("height", height?.toString() || "100%");
    33. return this;
    34. }

            相关的draw方法:

    1. import { messageInfo } from "../Message";
    2. // 绘制、DOM 操作的核心类 尽量将所有的DOM操作都汇集在该类中,防止多处操作DOM引起的其他问题
    3. export class Draw {
    4. constructor() {}
    5. // 通过指定的 xmlns 获取 svg
    6. public getSvg(xmlns: string) {
    7. return document.querySelector(`svg[xmlns="${xmlns}"]`);
    8. }
    9. // 创建 svg
    10. public createSvg(xmlns: string, svgID: string) {
    11. const svg = document.createElementNS(xmlns, "svg");
    12. svg.setAttribute("ID", svgID);
    13. svg.setAttribute("xmlns", xmlns);
    14. svg.setAttribute("version", "1.1");
    15. svg.setAttribute("baseProfile", "full");
    16. return svg;
    17. }
    18. // 将创建 svg 添加到指定容器
    19. public addTo(container: string | Element, svg: Element) {
    20. const type = typeof container === "string";
    21. // 判断传入参数是选择器还是dom
    22. let dom = type ? document.querySelector(container) : container;
    23. dom?.appendChild(svg);
    24. }
    25. // 绘制网格线
    26. public gridLines() {
    27. console.log("gridLines");
    28. }
    29. // 绘制水印
    30. public waterMark(waterMarkText?: string) {
    31. const text = waterMarkText || messageInfo.waterMarkText;
    32. }
    33. // 清除网格线
    34. public clearGridLines() {}
    35. // 清除水印
    36. public clearWaterMark() {}
    37. }

    构建 Rect 类

    1. import { Common } from "./Common";
    2. import { SVG } from "./index";
    3. // 矩形类
    4. export class Rect extends Common {
    5. private svg: SVG; // 根元素 svg
    6. private rect: Element;
    7. constructor(svg: SVG, width: number, height: number) {
    8. super();
    9. this.svg = svg;
    10. this.rect = super.getDraw().createRect(svg.getSvgXmlns());
    11. // 设置宽高
    12. this.setAttribute(width, height);
    13. // 将当前创建的元件添加到 svg 下
    14. super.addToSvg(this);
    15. }
    16. // 独有属性设置
    17. private setAttribute(width: number, height: number) {
    18. this.rect.setAttribute("width", width.toString());
    19. this.rect.setAttribute("height", height.toString());
    20. }
    21. // 获取基本Element
    22. public getElement() {
    23. return this.rect;
    24. }
    25. // 获取 xmlns
    26. public getXmlns() {
    27. return this.svg.getSvgXmlns();
    28. }
    29. }

    抽离公共类

            svg 元件具有的公共方法,例如 设置位置信息、设置宽高、设置样式等,还有事件处理机制,都是每一个元件都拥有的方法属性,因此,抽离为独立的类,实现 元件集成即可。

    1. // svg 元件公共类
    2. import { IGraphAttributes } from "../../interface/Graph";
    3. import { Draw } from "../Draw";
    4. import { Rect } from "./Rect";
    5. // 定义元件类型
    6. type IGraph = Rect;
    7. export class Common {
    8. private draw: Draw;
    9. constructor() {
    10. this.draw = new Draw();
    11. }
    12. // 设置元件ID
    13. public setID() {}
    14. // 获取ID
    15. public getID() {
    16. const element = (this as unknown as IGraph).getElement();
    17. return this.draw.getID(element);
    18. }
    19. // 将创建的元件 添加到 svg 下
    20. protected addToSvg(graph: IGraph) {
    21. // 创建了基本元件后,需要构建 g 分组,方便处理 hover 及 click 的锚点
    22. const xmlns = graph.getXmlns();
    23. const element = graph.getElement();
    24. const nodeID = graph.getID() as string;
    25. // 1. 获取分组
    26. const group = this.draw.createGroup(element, xmlns, nodeID);
    27. // 2. 获取当前的 svg 根元素
    28. const svg = this.draw.getSvg(xmlns);
    29. // 3. 初始化默认属性
    30. this.attr.call(graph, {});
    31. // 3. 将当前分组添加到根元素上
    32. this.draw.addTo(svg as Element, group);
    33. }
    34. // 设置位置
    35. public position(x: number, y: number) {
    36. const graph = this as unknown as IGraph;
    37. const element = graph.getElement();
    38. // 因为设置位置属性的时候,不同的元素不一致,因此需要建立 原型与属性的映射
    39. const { tagName } = element;
    40. const attrMap: { [key: string]: string[] } = {
    41. rect: ["x", "y"],
    42. circle: ["cx", "cy"],
    43. ellipse: ["cx", "cy"],
    44. };
    45. element.setAttribute(attrMap[tagName][0], x.toString());
    46. element.setAttribute(attrMap[tagName][1], y.toString());
    47. // 重新渲染
    48. this.draw.updateLinkAnchorPoint(
    49. graph.getID() as string,
    50. element,
    51. graph.getXmlns()
    52. );
    53. return this;
    54. }
    55. // 设置属性
    56. public attr({ stroke, fill }: IGraphAttributes) {
    57. // 设置样式
    58. const graph = this as unknown as IGraph;
    59. const element = graph.getElement();
    60. element.setAttribute("stroke", stroke || "black");
    61. element.setAttribute("fill", fill || "#F2F2F2");
    62. return this;
    63. }
    64. // 获取 draw 操作对象
    65. protected getDraw() {
    66. return this.draw;
    67. }
    68. }

    实现效果

     公共事件处理机制

    1. Common.ts
    2. // 为所有的子类构造事件
    3. public click!: (_fun: Function) => IGraph;
    4. public dblclick!: (_fun: Function) => IGraph;
    5. public mousedown!: (_fun: Function) => IGraph;
    6. public mousemove!: (_fun: Function) => IGraph;
    7. public mouseup!: (_fun: Function) => IGraph;
    8. public mouseover!: (_fun: Function) => IGraph;
    9. public mouseout!: (_fun: Function) => IGraph;
    10. // 初始化公共事件
    11. private initCommonEvent(graph: IGraph) {
    12. /**
    13. * 事件处理机制: 不管用户有没有添加 click ,都需要实现 addEventListener
    14. */
    15. const eventList: IEventList = {
    16. click: (e: Event, graph: IGraph) => this.commonEvent.click(e, graph),
    17. };
    18. const element = graph.getElement();
    19. Object.keys(eventList).forEach((eventname) => {
    20. let userfun: null | Function;
    21. // @ts-ignore 用户自定义事件
    22. graph[eventname] = (_fun: Function | null) => {
    23. userfun = _fun;
    24. return graph;
    25. };
    26. // 给元素添加事件
    27. element.addEventListener(eventname, (e) => {
    28. // 1. 先执行默认事件
    29. eventList[eventname](e, graph);
    30. // 在这里处理用户自定义的事件
    31. userfun && userfun(e);
    32. // 阻止事件冒泡
    33. e.preventDefault();
    34. });
    35. });
    36. }

    全局指令

    1. // 暴露对外操作API 需要经过 Command Adapt的中转,防止用户直接通过 Command 获取到内部对象
    2. import { Draw } from "../Draw";
    3. import { CommandAdapt } from "./CommandAdapt";
    4. export class Command {
    5. // 测试设置水印
    6. public executeWatermark: CommandAdapt["watermark"];
    7. constructor(draw: Draw) {
    8. const adapt = new CommandAdapt(draw);
    9. this.executeWatermark = adapt.watermark.bind(adapt);
    10. }
    11. }
    1. import { Draw } from "../Draw";
    2. // Command Adapt API 操作核心库
    3. export class CommandAdapt {
    4. private draw: Draw;
    5. constructor(draw: Draw) {
    6. this.draw = draw;
    7. }
    8. public watermark() {
    9. console.log("watermark");
    10. }
    11. }

    事件机制

            事件处理中主要使用event Bus 实现:

    1. export class EventBus<EventMap> {
    2. private eventHub: MapSet<Function>>
    3. constructor() {
    4. this.eventHub = new Map()
    5. }
    6. public onextends string & keyof EventMap>(
    7. eventName: K,
    8. callback: EventMap[K]
    9. ) {
    10. if (!eventName || typeof callback !== 'function') return
    11. const eventSet = this.eventHub.get(eventName) || new Set()
    12. eventSet.add(callback)
    13. this.eventHub.set(eventName, eventSet)
    14. }
    15. public emitextends string & keyof EventMap>(
    16. eventName: K,
    17. payload?: EventMap[K] extends (payload: infer P) => void ? P : never
    18. ) {
    19. if (!eventName) return
    20. const callBackSet = this.eventHub.get(eventName)
    21. if (!callBackSet) return
    22. if (callBackSet.size === 1) {
    23. const callBack = [...callBackSet]
    24. return callBack[0](payload)
    25. }
    26. callBackSet.forEach(callBack => callBack(payload))
    27. }
    28. public offextends string & keyof EventMap>(
    29. eventName: K,
    30. callback: EventMap[K]
    31. ) {
    32. if (!eventName || typeof callback !== 'function') return
    33. const callBackSet = this.eventHub.get(eventName)
    34. if (!callBackSet) return
    35. callBackSet.delete(callback)
    36. }
    37. public isSubscribeextends string & keyof EventMap>(eventName: K): boolean {
    38. const eventSet = this.eventHub.get(eventName)
    39. return !!eventSet && eventSet.size > 0
    40. }
    41. }

    总结

            至此,整体项目的框架已经跑通了,包括API的封装(command adapt)、事件处理机制、svg元件构建,本文先处理这么多事情。

  • 相关阅读:
    ES6:可迭代对象(Iterable object)
    2核2G3M带宽服务器腾讯云和阿里云价格、性能对比
    关于A level的习题答案
    独家首发!openEuler 主线集成 LuaJIT RISC-V JIT 技术
    docker file实战并将springBoot项目打包成镜像并运行
    台式电脑电源功率越大越费电吗?装机选购多少W电源
    二分查找【数组】
    [Lua][Love] "图块集与地图" 加载显示功能 TileMap
    全波形反演的深度学习方法: 第二章 正演 (草稿)
    Jumia、Shein流量逐渐上升,测评自养号如何实现订单突破?
  • 原文地址:https://blog.csdn.net/weixin_47746452/article/details/136452369