从0到1搭建一个属于自己的工作流站点——羽翼渐丰(bpmn-js、Next.js)

往期回顾

  1. 入门篇

    • 对bpmn-js的基本概念以及BPMN基础元素进行解释说明,并演示了如何使用bpmn-js来构建和配置一个简单的工作流程。
  2. 筑基篇

    • 更具体地介绍了什么是流程图,一张规范的流程图里都有哪些元素,配置流程图的编辑器都具备哪些功能,以及如何使用流程编辑器去配置一张流程图。
  3. 初具雏形

    • 正式进入手写代码阶段,通过npm依赖安装的方式导入相关模块,成功创建 Modeler 实例,并在此之上成功绘制一张符合规范的流程图。

观前提醒

本期包含大量的源码分析 和 问题的定位解决过程 ,因此篇幅较长,阅读本文时,方便的同学可以备好一些糖果🍬和巧克力🍫,边吃边看😋。

同样的,我在文章的最后附上了本文中相关项目的代码,如果你期望复现本文所提到的各种场景,可以git clone到本地。

前言

⚡⚡⚡开幕雷击!⚡⚡⚡

什么情况,为什么我们在上一篇文章中创建好的流程图,在拖动的时候长成了这个鬼样子?

出于一个老前端的直觉,这应该是样式出了问题。

在本篇文章,我们将探讨bpmn-js的绘图原理,并解决开篇出现的样式问题。然后我们会增加左侧工具栏,并给我们的画布新增导出按钮,确保我们能够实现自定义绘制流程图,并能够将绘制好的流程图导出,从而完成我们期望这个组件能够实现的最最基本的要求。

初始化过程发生了什么

还记得在上一篇文章中的【创建实例】章节里的这部分内容嘛?

在本篇文章里,我们就结合 bpmn-js 的源码聊一聊,这个初始化操作到底做了什么。

DOM元素结构

回顾一下我们之前是如何创建一个 Modeler 实例的:

jsx 复制代码
setBpmnModeler(new Modeler({ container: "#canvas" }));

我们将一个包含 container 属性的对象传递给了构造函数,用于创建实例。

然后我们用 F12 打开控制到,可以看到这样的一个DOM元素结构:

由上图可以看到,container: "#canvas",这行代码所指定的对应id的DOM元素(即 <div id='canvas'/> )下面生成了很多节点。

👆我们找到 <svg/> 标签,发现它正是对应了画布内容本身。👆

接下来我们拖动画布中的部分节点,将其置为下图状态:

再次审查元素:

我们可以看到一些似乎是造成展示异常的 style 属性设置。并且通过这几步审查元素的操作,我们也明白了,之前我们调用的 Modeler的实例方法importXML所做的事情就是将 xml 转换成 svg

这里我们顺便也回顾一下 <svg/><rect/><g/><path/>这些标签。

<svg/>

可缩放矢量图形Scalable Vector Graphics,SVG )基于 XML 标记语言,用于描述二维的矢量图形 ------MDN

要理解上方的定义文字,我们就得先理解 XML 和 矢量图形。

  • XML:XML(Extensible Markup Language)是一种类似于 HTML,但是没有使用预定义标记 的语言。因此,可以根据自己的设计需求定义专属的标记。这是一种强大将数据存储在一个可以存储、搜索和共享的格式中的方法。最重要的是,因为 XML 的基本格式是标准化的,如果你在本地或互联网上跨系统或平台共享或传输 XML,由于标准化的 XML 语法,接收者仍然可以解析数据。

    • 定义太长了,会审美疲劳,这里我们其实着重关注 预定义标记没有使用预定义标记 这一不同点即可。

      这个不同点其实可以用这样一句话概括:

      html的预定义是指我们写一个html,使用的标签元素一定得是html定义好的,而我们写xml,我们可以自定义标签。

      • HTML:
      html 复制代码
      <!DOCTYPE html>
      <html>
      <head>
        <title>示例页面</title>
      </head>
      <body>
        <h1>这是一个标题</h1>
        <p>这是一个段落。</p>
        <img src="image.jpg" alt="示例图片">
      </body>
      </html>
      • XML
      xml 复制代码
      <?xml version="1.0" encoding="UTF-8"?>
      <library xmlns:books="http://www.example.com/books">
        <books:book>
          <books:title>我喜欢的人是红色的</books:title>
          <books:author>亚历山大 · 达威尼亚</books:author>
        </books:book>
      </library>
    • SVG 图像及其相关行为被定义于 XML 文本文件之中,这意味着可以对它们进行搜索、索引、编写脚本以及压缩。此外,这也意味着可以使用任何文本编辑器和绘图软件来创建和编辑它们。

  • 矢量图形:和传统的点阵图像模式(如 JPEGPNG)不同的是,SVG 格式提供的是矢量图,这意味着它的图像能够被无限放大而不失真或降低质量,并且可以方便地修改内容,无需图形编辑器。通过使用合适的库进行配合,SVG 文件甚至可以随时进行本地化。

    • 失真是啥意思呢,比如我截图一张 117*112 大小的png图片,我将其放大几倍,图片会很显然地变得模糊。

      这是因为当我们放大图像时,每个像素的大小增加,但像素本身并没有增加额外的信息。放大过程通常是简单地复制周围的像素来填充空间,这个过程称为插值。因为原始图像的像素信息是固定的,所以放大后的图像看起来会模糊,尤其是在像素边界处更为明显。

      我们可以通过下方的动图演示直观地看到:

    • 但是SVG就没这个问题,我们也可以通过动图演示直观地看一下:

说了半天,我们最后再以一个简单的例子做个展示:

xml 复制代码
<svg version="1.1"
     baseProfile="full"
     width="300" height="200"
     xmlns="http://www.w3.org/2000/svg">

  <rect width="100%" height="100%" fill="#007fff" />
  <text x="150" y="125" font-size="60" text-anchor="middle" fill="white">海石</text>

</svg>

我们创建了一个很简单的带有矩形和文本的 <svg/>

接下来,我们就要讲一讲用于SVG绘图的一些基本形状(上面那个展示用的例子就使用到了矩形:<rect/>

<rect/>

rect元素会在屏幕上绘制一个矩形。

我们可以通过设置它的6个基本元素,来控制一个矩形在屏幕上的位置和形状。

比如我们期望绘制一个宽为400,高为300,以左上角为坐标原点,水平距离为60,垂直距离为10的矩形,我们可以这一设置:

xml 复制代码
<svg version="1.1"
     baseProfile="full"
     width="300" height="200"
     xmlns="http://www.w3.org/2000/svg">

  <rect width="400" height="300" x="60" y="10" />

</svg>

还可以通过设置剩余2个元素:rx 和 ry,来设置圆角的x方位和y方位的半径。

<path/>

path可能是 SVG 中最常见的形状。可以使用 path 元素绘制矩形(直角矩形或者圆角矩形)、圆形、椭圆、折线形、多边形,以及一些其他的形状,例如贝塞尔曲线、2 次曲线等曲线。

xml 复制代码
<svg version="1.1"
     baseProfile="full"
     width="300" height="200"
     xmlns="http://www.w3.org/2000/svg">

<svg width="190" height="160" xmlns="http://www.w3.org/2000/svg">

  <path d="M 10 10 C 20 20, 40 20, 50 10" stroke="black" fill="transparent"/>
  <path d="M 70 10 C 70 20, 110 20, 110 10" stroke="black" fill="transparent"/>
  <path d="M 130 10 C 120 20, 180 20, 170 10" stroke="black" fill="transparent"/>
  <path d="M 10 60 C 20 80, 40 80, 50 60" stroke="black" fill="transparent"/>
  <path d="M 70 60 C 70 80, 110 80, 110 60" stroke="black" fill="transparent"/>
  <path d="M 130 60 C 120 80, 180 80, 170 60" stroke="black" fill="transparent"/>
  <path d="M 10 110 C 20 140, 40 140, 50 110" stroke="black" fill="transparent"/>
  <path d="M 70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
  <path d="M 130 110 C 120 140, 180 140, 170 110" stroke="black" fill="transparent"/>

</svg>

</svg>

<g/>

元素g是用来组合对象的容器。添加到g元素上的变换会应用到其所有的子元素上。添加到g元素的属性会被其所有的子元素继承。此外,g元素也可以用来定义复杂的对象,之后可以通过<use>元素来引用它们。

------MDN

在我们的流程设计器中,我们可以看到,这种容器出现的频率非常的高:

我们也可以自己写一个<g/>标签,去尝试体验一把成组管理元素的感受:

xml 复制代码
<svg
  width="100%"
  height="100%"
    viewBox="0 0 195 150"

  xmlns="http://www.w3.org/2000/svg">
  <g stroke="#007fff" fill="white" stroke-width="5">
    <rect x="25" y="25" width="40" height="30" />
    <rect x="40" y="25" width="40" height="30" />
    <rect x="55" y="25" width="40" height="30" />
    <rect x="70" y="25" width="40" height="30" />
    <rect x="85" y="25" width="40" height="30" />
    <rect x="100" y="25" width="40" height="30" />
    <rect x="115" y="25" width="40" height="30" />

  </g> 
</svg>

我们统一设置了组内元素(7个矩形)的填充颜色和线条颜色。

OK,在对DOM元素结构以及几个标签元素有了一定程度的了解后,我们正式去看 bpmn-js 的源码,看看 new Modeler() 到底做了什么。

源码分析

我们可以从依赖的路径(bpmn-js/lib/Modeler)快速定位到源码中相关文件的位置:

在源码中我们可以很快速地找到Modeler的构造函数:

js 复制代码
export default function Modeler(options) {
  BaseModeler.call(this, options);
}

inherits(Modeler, BaseModeler);

这是一个很标准的原型链继承,Modeler 继承了 BaseModeler

当然,得出这个结论我们需要去看一下 inherits 方法的源码,它来自于 inherits-browser这个包。

js 复制代码
if (typeof Object.create === 'function') {
  // implementation from standard node.js 'util' module
  module.exports = function inherits(ctor, superCtor) {
    if (superCtor) {
      ctor.super_ = superCtor
      ctor.prototype = Object.create(superCtor.prototype, {
        constructor: {
          value: ctor,
          enumerable: false,
          writable: true,
          configurable: true
        }
      })
    }
  };
} else {
  // old school shim for old browsers
  module.exports = function inherits(ctor, superCtor) {
    if (superCtor) {
      ctor.super_ = superCtor
      var TempCtor = function () {}
      TempCtor.prototype = superCtor.prototype
      ctor.prototype = new TempCtor()
      ctor.prototype.constructor = ctor
    }
  }
}

源码读到这里,不知道你是否和我一样产生了以下几个疑问:

  1. 为什么还在使用原型链继承,而不是使用ES6+提供的 class 关键字去实现继承?
  2. 为啥 inherits 的源码中有2种实现方式?
  3. 什么是原型链继承?(对于直接就接触 class 关键字的同学来说,可能并不清楚)

遇到问题不解决可不行呀,放着不管继续看源码的话,心里也会痒痒,影响后续的学习。因此,我们先来一一回答上述的疑问。

问题答疑

问题一

为什么还在使用原型链继承,而不是使用ES6+提供的 class 关键字去实现继承?

这个问题是最好回答的,因为 bpmn-js 是十几年前的代码了,我们将源码 git clone 到本地之后,查看【时间线】:

写的时候还没 class 关键字可以用呢。

后续的一系列改造(本文还未提到的部分),全都是以原型链继承为基础的,因此如果要重构成ES6+的class写法,显然是巨额工作量。

问题二

为啥 inherits 的源码中有2种实现方式?

我们从源码中可以看到,typeof Object.create === 'function'这行条件语句决定了实现方式的不同。这是因为并不是所有浏览器都支持Object.create这个静态方法的。

因为Object.create是ES5引入的,旧版的浏览器比如IE8、chorme3等不支持。

于是我们就需要通过一个函数作为原型链继承的中转站,让父级的原型先赋值给TempCtor的原型, 然后再把TempCtor 的实例作为子级的原型。

js 复制代码
 var TempCtor = function () {}
 TempCtor.prototype = superCtor.prototype
 ctor.prototype = new TempCtor()

最后,不要忘记修改原型后,constructor也会改变,我们还需要把子级原型对象上的constructor改回它本身。

js 复制代码
ctor.prototype.constructor = ctor

实际上,当我们抛开Object.create的其他逻辑不谈,它的核心内容就是上述这样的一个过程:

js 复制代码
  Object.create = function (proto, propertiesObject) {
    // .....
    // 上方被省略的内容是一些边界处理的逻辑
    function F() {}
    F.prototype = proto;
    return new F();
  };

问题三

什么是原型链继承?(对于直接就接触 class 关键字的同学来说,可能并不清楚)

要完整地回答这个问题,估计可以单独出一篇博客了,一起聊聊原生链继承、寄生继承、寄生组合继承等等。

这里为了避免喧宾夺主,简单地提一嘴:

原型链继承是 JavaScript 中实现继承的一种机制。在 JavaScript 中,每个函数都有一个原型属性(prototype),而每个对象都有一个指向其原型对象的内部链接,称为__proto__(在现代浏览器中,建议通过Object.getPrototypeOf()访问,因为 __proto__ 已经添加了废弃声明)。当访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,解释器就会沿着这个对象的__proto__链向上查找,直到找到这个属性或方法,或者到达原型链的顶端(通常是Object.prototype)。

简单来说,原型链继承允许子类的原型(prototype)指向父类的实例,从而实现子类能够访问父类原型上的属性和方法。

以下是一个简单的原型链继承的例子:

js 复制代码
function Parent() {
  this.parentProperty = true;
}

Parent.prototype.getParentProperty = function() {
  return this.parentProperty;
};

function Child() {
  this.childProperty = false;
}

// 继承Parent
Child.prototype = new Parent();

// 修正构造函数指向,我们在回答第二个问题时提到过。
Child.prototype.constructor = Child;

// 创建Child的一个实例
var childInstance = new Child();

console.log(childInstance.getParentProperty()); // 输出 true

在这个例子中,Child 的原型(Child.prototype)被设置为 Parent 的一个实例。这意味着 Child 的实例 childInstance 可以通过原型链访问 Parent 原型上的方法 getParentProperty

好的,经过这几个问题的回答,我们扫清了眼前的阴霾,继续往下阅读源码。

问题根因的定位和解决

因为我们发现Modeler 继承了 BaseModeler,那么想搞明白构造函数中的内部逻辑,就得再去找 BaseModeler

我们很快便找到了 BaseModeler的源码,如下:

js 复制代码
export default function BaseModeler(options) {
  BaseViewer.call(this, options);

  // hook ID collection into the modeler
  this.on('import.parse.complete', function(event) {
    if (!event.error) {
      this._collectIds(event.definitions, event.elementsById);
    }
  }, this);

  this.on('diagram.destroy', function() {
    this.get('moddle').ids.clear();
  }, this);
}

inherits(BaseModeler, BaseViewer);

可以看到 BaseModeler 又继承了 BaseViewer,于是我们再去找 BaseViewer 的源码。

(这里的 this.on 的写法不知道你是否感兴趣,是不是和热门的面试考题:"手写一个 发布/订阅 " 很相似?下回再说吧。)

我们照样很快便找到了 BaseViewer的源码,将近860行的代码量,看来秘密就藏在它这了。

这里我想要多一句嘴🥺,那就是我们不以速成为目标,而是循序渐进,按部就班。因此我们不会在本文中就把这几百行的代码全部都搞懂,这不现实,也不好看。

别忘了我们最开始期望通过阅读源码来找到解决方法的那个问题:

为什么我们在上一篇文章中创建好的流程图,在拖动的时候长成了这个鬼样子

因此我们这一次阅读源码的目的就只要搞明白这个问题就可以了,其他的我们会在系列后续的文章中慢慢聊~

线索一

我们检索和 container 相关的代码,找到了几处地方,先来看第一处:

js 复制代码
BaseViewer.prototype._init = function(container, moddle, options) {

  const baseModules = options.modules || this.getModules(options),
        additionalModules = options.additionalModules || [],
        staticModules = [
          {
            bpmnjs: [ 'value', this ],
            moddle: [ 'value', moddle ]
          }
        ];

  const diagramModules = [].concat(staticModules, baseModules, additionalModules);

  const diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
    canvas: assign({}, options.canvas, { container: container }),
    modules: diagramModules
  });

  // invoke diagram constructor
  Diagram.call(this, diagramOptions);

  if (options && options.container) {
    this.attachTo(options.container);
  }
};

注意这行注释:

js 复制代码
// invoke diagram constructor

这意味着,我们创建Modeler实例的过程,实际上调用的构造函数来自 Diagram 的,我们通过 Diagram.call 的形式去让 Diagram 帮我们完成了绝大部分的初始化操作。

好的,我们先记下这个结论,等会再一起看。

线索二

接着,我们看下一处源码:

js 复制代码
BaseViewer.prototype._createContainer = function(options) {

  const container = domify('<div class="bjs-container"></div>');

  assignStyle(container, {
    width: ensureUnit(options.width),
    height: ensureUnit(options.height),
    position: options.position
  });

  return container;
};

还记得我们之前审查元素时看到的结构嘛?

来自 'min-dom'assignStyle() 方法赋予了画布(<svg/>)外层容器 <div class="bjs-container"></div>的样式。

看样子,源码里确实有样式相关的处理逻辑。

线索三

再看最后一处代码:

js 复制代码
function addProjectLogo(container) {
  const img = BPMNIO_IMG;

  const linkMarkup =
    '<a href="http://bpmn.io" ' +
    'target="_blank" ' +
    'class="bjs-powered-by" ' +
    'title="Powered by bpmn.io" ' +
    '>' +
    img +
    '</a>';

  const linkElement = domify(linkMarkup);

  assignStyle(domQuery('svg', linkElement), LOGO_STYLES);
  assignStyle(linkElement, LINK_STYLES, {
    position: 'absolute',
    bottom: '15px',
    right: '15px',
    zIndex: '100'
  });

  container.appendChild(linkElement);

  domEvent.bind(linkElement, 'click', function(event) {
    openPoweredBy();

    event.preventDefault();
  });
}

这个是基于 bpmn-js 的开源协议,要求一定要带上logo,用于在实例初始化时自动给画布绘制logo图案的方法。

根据找到的这几处代码,我们发现,最终的秘密还是藏在了来自第三方依赖 'diagram-js'Diagram 这个构造函数中。

因此,我们去看一下 'diagram-js'

'diagram-js'

我们可以在之前提到的《操作手册》中看到这样的信息:

我们使用 diagram-js 来绘制形状和连接。它为我们提供了与这些图形元素互动的方式,以及额外的工具,如覆盖层,帮助用户构建强大的 BPMN 视图。对于高级用例,如图形建模,它提供了上下文面板、调色板以及重做/撤销等功能。

我们查看 Diagram 的测试文件,可以直观地看到它的使用传参:

js 复制代码
let diagram = new Diagram();

diagram = new Diagram({
  modules: [
    CoreModule,
    CommandModule,
    ModelingModule
  ],
  canvas: {
    deferUpdate: true
  }
});

这里的使用方式和我们在 BaseViewer中是一样的:

js 复制代码
const diagramOptions = assign(omit(options, [ 'additionalModules' ]), {
  canvas: assign({}, options.canvas, { container: container }),
  modules: diagramModules
});

// invoke diagram constructor
Diagram.call(this, diagramOptions);

同样都具备 modulescanvas 属性。

此刻我们得知了一个信息,那就是画布上所有的图形、线条,也就是之前我们在审查元素中查看到的<svg/>下的各种元素,均是 'diagram-js' 创建的。

这也就意味着,这些被创建的元素的样式应该都是被 'diagram-js' 所设置好的。

这里面的 className 均是以 djs- 开头,如果我们在 'diagram-js' 的源码中尝试搜索这些类名时:

我们能够找到diagram-js.css这份样式文件,里面定义了这些图元应该有的样式

这下我们知道为什么我们的线条被hover 被拖动 被点击时展示的样子是那么的奇怪了,因为根本就没有对应的样式文件应用在上面!

于是我们尝试在 bpmn-js 中搜索是否存在和diagram-js.css相关的样式文件,果不其然:

TestHelper.js文件,用于设置和配置测试环境,包括样式表的加载、Chai匹配器的配置以及文件拖放功能的实现。它是测试套件的一部分,用于测试BPMN图的可视化和编辑功能。

解决

于是我们照猫画虎,将TestHelper.js文件所插入的CSS文件,全部也在我们的项目中引入,并把画布的宽高改成100%:

tsx 复制代码
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn.css";
import "bpmn-js/dist/assets/diagram-js.css";
import "bpmn-js/dist/assets/bpmn-js.css";
//...


return <div id="canvas" style={{ width: "100%", height: "100%" }}></div>;

改版后,我们在试着去做一些操作,如下图所示:

可以看到,样式问题已经完全解决,非常的正常,美观👍。

导出

经历过上一章节的阅读,我想大家多少有一丝丝疲惫,那么在这个章节,我们就做一些简单粗暴的事情,调库!

读了那么多代码,是时候写点代码转换转换心情了。

我们写一个简单的导出按钮,让它处于画布的中上方:

tsx 复制代码
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <div id="canvas" style={{ width: "100%", height: "100%" }} />
      <Button
        style={{ position: "absolute", top: 20, right: "50%" }}
        size="small"
        onClick={() => {
          // exportXML();
        }}
      >
        导出
      </Button>
    </div>

然后我们再去写点击后的导出逻辑,即 exportXML()

我们期望能够以 SVG 的形式保存我们的流程图,于是我们去源码里找一下相关的方法:

我们找到了 BaseViewer.prototype.saveSVG 这个方法,它会返回一个包含错误信息和svg的对象。

由于我们的 Modeler 是通过原型链继承,继承了 BaseViewe ,因此我们可以直接调用,如下:

tsx 复制代码
      const { svg } = await bpmnModeler.saveSVG();

接下来就是很常见的需求了,即如何在网页上提供用户一个下载文件的入口。

在掘金上随便搜一下都有这么多的解决方案:

这里我们就使用最基础的方式,即利用 <a/> 标签结合 href 的形式帮我们下载文件。

tsx 复制代码
  const fileDownload = (href: string, filename: string) => {
    if (href && filename) {
      let aLink = document.createElement("a");
      aLink.download = filename; //指定下载的文件名
      aLink.href = href; //  URL对象
      aLink.click(); // 模拟点击
      document.body.removeChild(aLink); // 从文档中移除a标签
      URL.revokeObjectURL(aLink.href); // 释放URL 对象
    }
  };

最后的问题就是如何获取到这个 href 了。

我们需要调用encodeURIComponent对之前利用BaseViewer.prototype.saveSVG生成的字符串格式的SVG进行编码。

tsx 复制代码
    const encodedData = encodeURIComponent(data);

编码后,再拼接成 href 期望的格式即可。

tsx 复制代码
  href: `data:application/${
        type === "svg" ? "text/xml" : "bpmn20-xml"
      };charset=UTF-8,${encodedData}`,

完整代码如下:

tsx 复制代码
  // 根据所需类型进行转码并返回下载地址
  const encodedFile = (type: string, filename = "diagram", data: string) => {
    const encodedData = encodeURIComponent(data);
    return {
      filename: `${filename}.${type}`,
      href: `data:application/${
        type === "svg" ? "text/xml" : "bpmn20-xml"
      };charset=UTF-8,${encodedData}`,
      data: data,
    };
  };

最终代码如下:

tsx 复制代码
const fileDownload = (href: string, filename: string) => {
    if (href && filename) {
      let aLink = document.createElement("a");
      aLink.download = filename; //指定下载的文件名
      aLink.href = href; //  URL对象
      aLink.click(); // 模拟点击
      document.body.removeChild(aLink); // 从文档中移除a标签
      URL.revokeObjectURL(aLink.href); // 释放URL 对象
    }
  };

  // 根据所需类型进行转码并返回下载地址
  const encodedFile = (type: string, filename = "diagram", data: string) => {
    const encodedData = encodeURIComponent(data);
    return {
      filename: `${filename}.${type}`,
      href: `data:application/${
        type === "svg" ? "text/xml" : "bpmn20-xml"
      };charset=UTF-8,${encodedData}`,
      data: data,
    };
  };

  const exportXML = async () => {
    try {
      const allShapes = bpmnModeler.get("elementRegistry").getAll();

      const { svg } = await bpmnModeler.saveSVG();

      const { href, filename } = encodedFile(
        "SVG",
        allShapes[0].businessObject.name,
        svg
      );
      fileDownload(href, filename);
    } catch (e) {}
  };

运行后的效果如下图所示:

结语

在本篇文章里,我们通过源码阅读,定位样式问题出现的根因,并找到了相应的解决方案,成功解决。之后,我们写了一个简单的导出按钮,使得我们的流程设计器成功地能够导出自定义的流程图。

可以说,经过这一次的改造,我们的组件确实称得上"羽翼渐丰"。

但是离成熟的组件还存在不少距离:

  • 如何完成流程图的合法校验?一张不符合规范的流程图没有存在的必要。(比如包含多个开始节点的流程图)
  • 属性面板呢?我期望给节点进行名称、状态、类型的修改,目前没有这种入口。
  • 解决完样式异常后,自定义样式该如何实现?

我们将在本系列的后续文章里接着完善我们的组件,并且也是时候聊一聊Next.js了。

期待与你在下一篇文章相遇~

示例完整代码

我将代码push到了我的 git仓库里,大家可以在这里找到。

相关推荐
hackeroink14 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css