从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仓库里,大家可以在这里找到。

相关推荐
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel4 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼4 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT4 小时前
promise & async await总结
前端
Jerry说前后端4 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天4 小时前
A12预装app
linux·服务器·前端