运维咖啡吧

享受技术带来的乐趣,体验生活给予的感动

最好用的流程编辑器bpmn-js系列之本地文件

最好用的流程编辑器bpmn-js系列文章

上一篇文章『最好用的流程编辑器bpmn-js系列之基本使用』介绍了bpmn的相关概念以及基本使用,基于bpmn-js的工作流编辑器已经run了起来,这篇文章将会介绍如何从本地加载xml格式的bpmn文件,以及如何将绘制好的工作流保作为bpmn文件或是svg图片保存到本地

以下演示代码基于上一节搭建好的vue环境,使用bpmn版本为当前最新版7.3.0

从本地文件中加载流程

上一节的演示代码中我们将bpmn流程图内容作为字符串赋予给了变量xmlStr,在渲染时通过importXML(xmlStr)直接读取了xmlStr变量的内容从而实现了绘图,核心代码如下所示

import { xmlStr } from "../mock/xmlStr";

...

async createNewDiagram() {
  try {
    const result = await this.bpmnModeler.importXML(xmlStr);
    const { warnings } = result;
    console.log(warnings);
  } catch (err) {
    console.log(err.message, err.warnings);
  }
},

如果想要从本地导入bpmn格式的流程文件该如何实现呢?借助于上边的思路,其实只需要以下两步即可

template模版,添加一个type为file的input输入框,用于文件上传,为了好看这里将input给隐藏,这里用一个a标签通过$refs.refFile.click()模拟点击触发弹出资源管理器,然后input通过change事件触发loadXML函数的执行

<template>
  <div class="containers">
    <div class="canvas" ref="canvas"></div>

    <ul class="buttons">
      <li>
        <a href="javascript:" @click="$refs.refFile.click()">加载本地BPMN文件</a>
        <input type="file" id="files" ref="refFile" style="display: none" @change="loadXML" />
      </li>
    </ul>
  </div>
</template>

loadXML主要用来接收用户上传的文件,然后将文件内容赋予变量xmlStr,之后调用createNewDiagram方法将bpmn文件内容渲染到编辑器,完整的代码如下

<script>
import BpmnModeler from "bpmn-js/lib/Modeler";
import { xmlStr } from "../mock/xmlStr";

export default {
  name: "ops-coffee",
  mounted() {
    this.init();
  },
  data() {
    return {
      bpmnModeler: null,
      container: null,
      canvas: null,
      xmlStr: xmlStr
    };
  },
  methods: {
    init() {
      const canvas = this.$refs.canvas;
      this.bpmnModeler = new BpmnModeler({
        container: canvas
      });

      this.createNewDiagram();
    },
    createNewDiagram() {
      try {
        const result = this.bpmnModeler.importXML(this.xmlStr);
        const { warnings } = result;
        console.log(warnings);
      } catch (err) {
        console.log(err.message, err.warnings);
      }
    },
    loadXML() {
      const that = this;
      const file = this.$refs.refFile.files[0];

      var reader = new FileReader();
      reader.readAsText(file);
      reader.onload = function() {
        that.xmlStr = this.result;
        that.createNewDiagram();
      };
    }
  }
};
</script>

需要注意的是,新增了一个xmlStr的变量,初始值赋予了默认的bpmn格式字符串,这样在每次新打开页面时都可以正常渲染默认的流程,当上传bpmn文件后会替换掉这个xmlStr变量的值为新上传的文件内容,再次渲染时就能渲染新上传的流程文件了

另外这里也可以做一些基础的判断,例如只接收.bpmn后缀的文件等等,让程序更健壮

为了让添加的按钮看起来好看一点,新增了如下CSS

.buttons {
  position: absolute;
  left: 20px;
  bottom: 20px;
}
.buttons li {
  display: inline-block;
  margin: 5px;
}
.buttons li a {
  color: #333;
  background: #fff;
  cursor: pointer;
  padding: 8px;
  border: 1px solid #ccc;
  text-decoration: none;
}

将流程保存到本地文件

上边讲了如何导入本地文件,那么本地文件从哪里来呢?官方提供了一个saveXML方法可以将绘制的工作流输出为xml格式的数据,而我们可以将xml格式的数据保存到本地文件,而这个文件就能通过上边的导入步骤导入渲染啦

saveXML使用也非常简单,第一步依然是添加一个导出的按钮

<li>
  <a href="javascript:" @click="saveXML" title="保存为bpmn">保存为BPMN文件</a>
</li>

然后编写一个saveXML的方法来实现将xml格式数据保存为本地文件,完整代码如下

async saveXML() {
  try {
    const result = await this.bpmnModeler.saveXML({ format: true });
    const { xml } = result;

    var xmlBlob = new Blob([xml], {
      type: "application/bpmn20-xml;charset=UTF-8,"
    });

    var downloadLink = document.createElement("a");
    downloadLink.download = "ops-coffee-bpmn.bpmn";
    downloadLink.innerHTML = "Get BPMN SVG";
    downloadLink.href = window.URL.createObjectURL(xmlBlob);
    downloadLink.onclick = function(event) {
      document.body.removeChild(event.target);
    };
    downloadLink.style.visibility = "hidden";
    document.body.appendChild(downloadLink);
    downloadLink.click();
  } catch (err) {
    console.log(err);
  }
},

同样的,官方除了支持导出为xml格式的bpmn文件外,还支持直接导出为svg格式的图片,方法与导出bpmn文件类似,代码如下

<li>
  <a href="javascript:" @click="saveSVG" title="保存为svg">保存为SVG图片</a>
</li>
async saveSVG() {
  try {
    const result = await this.bpmnModeler.saveSVG();
    const { svg } = result;

    var svgBlob = new Blob([svg], {
      type: "image/svg+xml"
    });

    var downloadLink = document.createElement("a");
    downloadLink.download = "ops-coffee-bpmn.svg";
    downloadLink.innerHTML = "Get BPMN SVG";
    downloadLink.href = window.URL.createObjectURL(svgBlob);
    downloadLink.onclick = function(event) {
      document.body.removeChild(event.target);
    };
    downloadLink.style.visibility = "hidden";
    document.body.appendChild(downloadLink);
    downloadLink.click();
  } catch (err) {
    console.log(err);
  }
},

监听变化下载文件

以上下载文件的方式是在每一次点击下载按钮时去构建下载链接,而官方示例里还给了另外一种下载文件的方式,那就是通过bpmnModeler.on的监听方法,监听到流程图变化然后直接构建下载链接,用户点击下载按钮时直接下载,不再构建下载链接

官方示例代码地址https://github.com/bpmn-io/bpmn-js-examples/blob/ebd40ecbb9672227f1d9eb5c00bc1aaf38127df6/modeler/app/app.js

优化后可在本项目中使用的代码如下

<li>
  <a href="javascript:" ref="saveXML" title="保存为bpmn">保存为BPMN文件</a>
</li>
<li>
  <a href="javascript:" ref="saveSvg" title="保存为svg">保存为SVG图片</a>
</li>

为了优化结构,参考了网上成熟的例子,在渲染完成后调用了success方法,而success方法调用addBpmnListener,在addBpmnListener内通过bpmnModeler.on来监听流程变化从而更新下载链接

<script>
export default {
  name: "ops-coffee",
  mounted() {
    this.init();
  },
  data() {
    return {
      bpmnModeler: null,
      container: null,
      canvas: null,
      xmlStr: xmlStr
    };
  },
  methods: {
    init() {
      const canvas = this.$refs.canvas;
      this.bpmnModeler = new BpmnModeler({
        container: canvas
      });

      this.createNewDiagram();
    },
    async createNewDiagram() {
      try {
        const result = await this.bpmnModeler.importXML(this.xmlStr);
        const { warnings } = result;
        console.log(warnings);

        this.success();
      } catch (err) {
        console.log(err.message, err.warnings);
      }
    },
    success() {
      this.addBpmnListener();
    },
    async loadXML() {
      const that = this;
      const file = this.$refs.refFile.files[0];

      var reader = new FileReader();
      reader.readAsText(file);
      reader.onload = function() {
        that.xmlStr = this.result;
        that.createNewDiagram();
      };
    },
    async addBpmnListener() {
      const that = this;
      const downloadLink = this.$refs.saveXML;
      const downloadSvgLink = this.$refs.saveSvg;

      async function opscoffee() {
        try {
          const result = await that.saveSVG();
          const { svg } = result;

          that.setEncoded(downloadSvgLink, "ops-coffee.svg", svg);
        } catch (err) {
          console.log(err);
        }

        try {
          const result = await that.saveXML();
          const { xml } = result;

          that.setEncoded(downloadLink, "ops-coffee.bpmn", xml);
        } catch (err) {
          console.log(err);
        }
      }

      opscoffee();
      this.bpmnModeler.on("commandStack.changed", opscoffee);
    },
    async saveSVG(done) {
      try {
        const result = await this.bpmnModeler.saveSVG(done);
        return result;
      } catch (err) {
        console.log(err);
      }
    },
    async saveXML(done) {
      try {
        const result = await this.bpmnModeler.saveXML({ format: true }, done);
        return result;
      } catch (err) {
        console.log(err);
      }
    },
    setEncoded(link, name, data) {
      const encodedData = encodeURIComponent(data);

      if (data) {
        link.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
        link.download = name;
      }
    }
  }
};
</script>

以上两个功能完成后就可以愉快的将流程图保存到本地或者选择本地流程文件渲染流程图啦

写在最后

接触bpmn-js不久,且第一次用VUE,边学边写,文章难免出错,各位多多包含。想要打造一个好用的适合自己的流程编辑器,需要了解的内容比较多,bpmn-js会分多篇文章来介绍,下一篇介绍bpmn-js的页面布局等内容,欢迎关注

部分小伙伴对流程编辑器不了解,或是对BPMN不了解,我搭建了个在线的Demo: https://bpmn.ops-coffee.cn,点击链接即可轻松体验,建议PC端打开效果更好