照張办事 發表於 2019-8-14 20:42:00

JointJS:JavaScript 流程图绘制框架

<h1 id="jointjsjavascript-流程图绘制框架">JointJS:JavaScript 流程图绘制框架</h1>
<blockquote>
<p>最近调研了js画流程图的框架,最后选择了Joint。配合上 dagre 可以画出像模像样的流程图。</p>
</blockquote>
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>JointJS:JavaScript 流程图绘制框架<ul><li>JointJS 简介</li><li>JointJS Hello world</li><li>前后端分离架构</li><li>其他<ul><li>自动布局 Automatic layout</li><li>使用 HTML 定制元素</li></ul></li></ul></li></ul></div><p></p>
<h2 id="jointjs-简介">JointJS 简介</h2>
<p>JointJS 是一个开源前端框架,支持绘制各种各样的流程图、工作流图等。Rappid 是 Joint 的商业版,提供了一些更强的插件。JointJS 的特点有下面几条,摘自官网:</p>
<ul>
<li>能够实时地渲染上百(或者上千)个元素和连接</li>
<li>支持多种形状(矩形、圆、文本、图像、路径等)</li>
<li>高度事件驱动,用户可自定义任何发生在 paper 下的事件响应</li>
<li>元素间连接简单</li>
<li>可定制的连接和关系图</li>
<li>连接平滑(基于贝塞尔插值 bezier interpolation)&amp; 智能路径选择</li>
<li>基于 SVG 的可定制、可编程的图形渲染</li>
<li>NodeJS 支持</li>
<li>通过 JSON 进行序列化和反序列化</li>
</ul>
<p>总之 JoingJS 是一款很强的流程图制作框架,开源版本已经足够日常使用了。</p>
<p>一些常用地址:</p>
<p>API: https://resources.jointjs.com/docs/jointjs/v1.1/joint.html</p>
<p>Tutorials: https://resources.jointjs.com/tutorial</p>
<h2 id="jointjs-hello-world">JointJS Hello world</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" /&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;!-- content --&gt;
    &lt;div id="myholder"&gt;&lt;/div&gt;

    &lt;!-- dependencies 通过CDN加载依赖--&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"&gt;&lt;/script&gt;

    &lt;!-- code --&gt;
    &lt;script type="text/javascript"&gt;

      var graph = new joint.dia.Graph;

      var paper = new joint.dia.Paper({
            el: document.getElementById('myholder'),
            model: graph,
            width: 600,
            height: 100,
            gridSize: 1
      });

      var rect = new joint.shapes.standard.Rectangle();
      rect.position(100, 30);
      rect.resize(100, 40);
      rect.attr({
            body: {
                fill: 'blue'
            },
            label: {
                text: 'Hello',
                fill: 'white'
            }
      });
      rect.addTo(graph);

      var rect2 = rect.clone();
      rect2.translate(300, 0);
      rect2.attr('label/text', 'World!');
      rect2.addTo(graph);

      var link = new joint.shapes.standard.Link();
      link.source(rect);
      link.target(rect2);
      link.addTo(graph);

    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>hello world 代码没什么好说的。要注意这里的图形并没有自动排版,而是通过移动第二个 rect 实现的手动排版。</p>
<p><img src="https://img2018.cnblogs.com/blog/761792/201908/761792-20190814204022500-1136219124.png"></p>
<h2 id="前后端分离架构">前后端分离架构</h2>
<p>既然支持 NodeJs,那就可以把繁重的图形绘制任务交给服务器,再通过 JSON 序列化在 HTTP 上传输对象,这样减轻客户端的压力。</p>
<p>NodeJS 后端</p>
<pre><code class="language-javascript">var express = require('express');
var joint = require('jointjs');

var app = express();

function get_graph(){
    var graph = new joint.dia.Graph();

    var rect = new joint.shapes.standard.Rectangle();
    rect.position(100, 30);
    rect.resize(100, 40);
    rect.attr({
      body: {
            fill: 'blue'
      },
      label: {
            text: 'Hello',
            fill: 'white'
      }
    });
    rect.addTo(graph);

    var rect2 = rect.clone();
    rect2.translate(300, 0);
    rect2.attr('label/text', 'World!');
    rect2.addTo(graph);

    var link = new joint.shapes.standard.Link();
    link.source(rect);
    link.target(rect2);
    link.addTo(graph);

    return graph.toJSON();
}

app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    next();
});

app.get('/graph', function(req, res){
    console.log('[+] send graph json to client')
    res.send(get_graph());
});

app.listen(8071);
</code></pre>
<p>HTML 前端</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" /&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;!-- content --&gt;
    &lt;div id="myholder"&gt;&lt;/div&gt;

    &lt;!-- dependencies 通过CDN加载依赖--&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"&gt;&lt;/script&gt;

    &lt;!-- code --&gt;
    &lt;script type="text/javascript"&gt;
      var graph = new joint.dia.Graph;
      var paper = new joint.dia.Paper({
            el: document.getElementById('myholder'),
            model: graph,
            width: 600,
            height: 100,
            gridSize: 1
      });

      $.get('http://192.168.237.128:8071/graph', function(data, statue){
            graph.fromJSON(data);
      });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2 id="其他">其他</h2>
<h3 id="自动布局-automatic-layout">自动布局 Automatic layout</h3>
<p>JointJS 内置了插件进行自动排版,原理是调用 Dagre 库。官方 api 中有样例。</p>
<p>使用方法:</p>
<blockquote>
<p>var graphBBox = joint.layout.DirectedGraph.layout(graph, {<br>
nodeSep: 50,<br>
edgeSep: 80,<br>
rankDir: "TB"<br>
});</p>
</blockquote>
<table>
<thead>
<tr>
<th style="text-align: left">配置参数</th>
<th>注释</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">nodeSep</td>
<td>相同rank的邻接节点的距离</td>
</tr>
<tr>
<td style="text-align: left">edgeSep</td>
<td>相同rank的邻接边的距离</td>
</tr>
<tr>
<td style="text-align: left">rankSep</td>
<td>不同 rank 元素之间的距离</td>
</tr>
<tr>
<td style="text-align: left">rankDir</td>
<td>布局方向 ( <code>"TB"</code> (top-to-bottom) / <code>"BT"</code> (bottom-to-top) / <code>"LR"</code> (left-to-right) / <code>"RL"</code> (right-to-left))</td>
</tr>
<tr>
<td style="text-align: left">marginX</td>
<td>number of pixels to use as a margin around the left and right of the graph.</td>
</tr>
<tr>
<td style="text-align: left">marginY</td>
<td>number of pixels to use as a margin around the top and bottom of the graph.</td>
</tr>
<tr>
<td style="text-align: left">ranker</td>
<td>排序算法。 Possible values: <code>'network-simplex'</code> (default), <code>'tight-tree'</code> or <code>'longest-path'</code>.</td>
</tr>
<tr>
<td style="text-align: left">resizeClusters</td>
<td>set to <code>false</code> if you don't want parent elements to stretch in order to fit all their embedded children. Default is <code>true</code>.</td>
</tr>
<tr>
<td style="text-align: left">clusterPadding</td>
<td>A gap between the parent element and the boundary of its embedded children. It could be a number or an object e.g. <code>{ left: 10, right: 10, top: 30, bottom: 10 }</code>. It defaults to <code>10</code>.</td>
</tr>
<tr>
<td style="text-align: left">setPosition(element, position)</td>
<td>a function that will be used to set the position of elements at the end of the layout. This is useful if you don't want to use the default <code>element.set('position', position)</code> but want to set the position in an animated fashion via transitions.</td>
</tr>
<tr>
<td style="text-align: left">setVertices(link, vertices)</td>
<td>If set to <code>true</code> the layout will adjust the links by setting their vertices. It defaults to <code>false</code>. If the option is defined as a function it will be used to set the vertices of links at the end of the layout. This is useful if you don't want to use the default <code>link.set('vertices', vertices)</code> but want to set the vertices in an animated fashion via transitions.</td>
</tr>
<tr>
<td style="text-align: left">setLabels(link, labelPosition, points)</td>
<td>If set to <code>true</code> the layout will adjust the labels by setting their position. It defaults to <code>false</code>. If the option is defined as a function it will be used to set the labels of links at the end of the layout. <em><strong>Note:</strong> Only the first label (link.label(0)😉 is positioned by the layout.</em></td>
</tr>
<tr>
<td style="text-align: left">dagre</td>
<td>默认情况下,dagre 应该在全局命名空间当中,不过你也可以当作参数传进去</td>
</tr>
<tr>
<td style="text-align: left">graphlib</td>
<td>默认情况下,graphlib 应该在全局命名空间当中,不过你也可以当作参数传进去</td>
</tr>
</tbody>
</table>
<p>我们来试一下。NodeJS 后端</p>
<pre><code class="language-javascript">var express = require('express');
var joint = require('jointjs');
var dagre = require('dagre')
var graphlib = require('graphlib');

var app = express();

function get_graph(){
    var graph = new joint.dia.Graph();

    var rect = new joint.shapes.standard.Rectangle();
    rect.position(100, 30);
    rect.resize(100, 40);
    rect.attr({
      body: {
            fill: 'blue'
      },
      label: {
            text: 'Hello',
            fill: 'white'
      }
    });
    rect.addTo(graph);

    var rect2 = rect.clone();
    rect2.translate(300, 0);
    rect2.attr('label/text', 'World!');
    rect2.addTo(graph);

    for(var i=0; i&lt;10; i++){
      var cir = new joint.shapes.standard.Circle();
      cir.resize(100, 100);
      cir.position(10, 10);
      cir.attr('root/title', 'joint.shapes.standard.Circle');
      cir.attr('label/text', 'Circle' + i);
      cir.attr('body/fill', 'lightblue');
      cir.addTo(graph);

      var ln = new joint.shapes.standard.Link();
      ln.source(cir);
      ln.target(rect2);
      ln.addTo(graph);
    }

    var link = new joint.shapes.standard.Link();
    link.source(rect);
    link.target(rect2);
    link.addTo(graph);

    //autolayout
    joint.layout.DirectedGraph.layout(graph, {
      nodeSep: 50,
      edgeSep: 50,
      rankDir: "TB",
      dagre: dagre,
      graphlib: graphlib
    });

    return graph.toJSON();
}

app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    next();
});

app.get('/graph', function(req, res){
    console.log('[+] send graph json to client')
    res.send(get_graph());
});

app.listen(8071);
</code></pre>
<p>HTML 前端</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.css" /&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;!-- content --&gt;
    &lt;div id="myholder"&gt;&lt;/div&gt;

    &lt;!-- dependencies 通过CDN加载依赖--&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/jointjs/2.1.0/joint.js"&gt;&lt;/script&gt;

    &lt;!-- code --&gt;
    &lt;script type="text/javascript"&gt;
      var graph = new joint.dia.Graph;
      var paper = new joint.dia.Paper({
            el: document.getElementById('myholder'),
            model: graph,
            width: 2000,
            height: 2000,
            gridSize: 1
      });

      $.get('http://192.168.237.128:8071/graph', function(data, statue){
            graph.fromJSON(data);
      });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>结果:</p>
<p><img src="https://img2018.cnblogs.com/blog/761792/201908/761792-20190814204120938-1779921677.png"></p>
<h3 id="使用-html-定制元素">使用 HTML 定制元素</h3>
<p>流程图中的每个点,也就是是元素,都可以自定义,直接编写 html 代码能添加按钮、输入框、代码块等。</p>
<p>我的一个代码块 demo,搭配 highlight.js 可以达到类似 IDA 控制流图的效果。这个 feature 可玩度很高。</p>
<pre><code class="language-javascript">joint.shapes.BBL = {};
joint.shapes.BBL.Element = joint.shapes.basic.Rect.extend({
    defaults: joint.util.deepSupplement({
      type: 'BBL.Element',
      attrs: {
            rect: { stroke: 'none', 'fill-opacity': 0 }
      }
    }, joint.shapes.basic.Rect.prototype.defaults)
});

// Create a custom view for that element that displays an HTML div above it.
// -------------------------------------------------------------------------
joint.shapes.BBL.ElementView = joint.dia.ElementView.extend({
    template: [
      '&lt;div class="html-element" data-collapse&gt;',
      '&lt;label&gt;&lt;/label&gt;&lt;br/&gt;',
      '&lt;div class="hljs"&gt;&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/span&gt;&lt;/div&gt;',
      '&lt;/div&gt;'
    ].join(''),

    initialize: function() {
      _.bindAll(this, 'updateBox');
      joint.dia.ElementView.prototype.initialize.apply(this, arguments);

      this.$box = $(_.template(this.template)());
      // Prevent paper from handling pointerdown.
      this.$box.find('h3').on('mousedown click', function(evt) {
            evt.stopPropagation();
      });

      // Update the box position whenever the underlying model changes.
      this.model.on('change', this.updateBox, this);
      // Remove the box when the model gets removed from the graph.
      this.model.on('remove', this.removeBox, this);

      this.updateBox();
    },
    render: function() {
      joint.dia.ElementView.prototype.render.apply(this, arguments);
      this.paper.$el.prepend(this.$box);
      this.updateBox();
      return this;
    },
    updateBox: function() {
    // Set the position and dimension of the box so that it covers the JointJS element.
      var bbox = this.model.getBBox();
      // Example of updating the HTML with a data stored in the cell model.
      this.$box.find('label').text(this.model.get('label'));
      this.$box.find('code').html(this.model.get('code'));
      var color = this.model.get('color');

      this.$box.css({
            width: bbox.width,
            height: bbox.height,
            left: bbox.x,
            top: bbox.y,
            background: color,
            "border-color": color
      });
    },
    removeBox: function(evt) {
      this.$box.remove();
    }
});
</code></pre><br><br>
来源:https://www.cnblogs.com/helica/p/11354577.html
頁: [1]
查看完整版本: JointJS:JavaScript 流程图绘制框架