整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

用HTML5构建一个流程图绘制工具

我们的开发工程中经常会使用到各种图,所谓的图就是由节点和节点之间的连接所形成的系统,数学上专门有一个分支叫图论(Graph Theroy)。利用图我们可以做很多工具,比如思维导图,流程图,状态机,组织架构图,等等。今天我要做的是用开源的HTML5工具来快速构造一个做图的工具。

工具选择

工预善其事,必先利其器。第一件事是选择一件合适的工具,开源时代,程序员还是很幸福的,选择很多。

  • flowchart.js http://adrai.github.io/flowchart.js/ , 基于SVG创建Flow Chart
  • go.js http://www.gojs.net/latest/index.html go.js 提供一整套的JS工具 ,支持各种交互式图表的创建。有免费版和收费版
  • joint.js http://www.jointjs.com/ joint.js 是另一个创建图标库的工具,也提供免费版和商业版
  • jsPlumb http://www.jsplumb.org/ jsPlumb是一套开源的流程图创建工具 ,小巧精悍,使用简单
  • d3 http://d3js.org 在html5领域,d3可谓是最好的可视化基础库,提供方面的DOM操作,非常强大。

最终,我选择了jsPlumb,因为它完全开源,使用很简单,用D3的话可能会多花很多功夫。joint.js也不错。大家可以根据自己的需要选择。

构建静态应用

下面我们一步一步的来使用jsPlumb来创建我们的流程图工具。

第一步是等待DOM和jsPlumb初始化完毕,类似document.ready()和jquery.ready(), 要使用jsPlumb, 需要把代码放在这个函数里:

jsPlumb.ready(function() {
    // ... your code goes here ...
}


创建一个jsPlumb的实例,并初始化jsPlumb的配置参数:

//Initialize JsPlumb
var color = "#E8C870";
var instance = jsPlumb.getInstance({
    // notice the 'curviness' argument to this Bezier curve.  the curves on this page are far smoother
    // than the curves on the first demo, which use the default curviness value.      
    Connector : [ "Bezier", { curviness:50 } ],
    DragOptions : { cursor: "pointer", zIndex:2000 },
    PaintStyle : { strokeStyle:color, lineWidth:2 },
    EndpointStyle : { radius:5, fillStyle:color },
    HoverPaintStyle : {strokeStyle:"#7073EB" },
    EndpointHoverStyle : {fillStyle:"#7073EB" },
    Container:"container-id"
 });


这里给给出了一些配置包括,连接线(这里配置了一个贝塞尔曲线),线的风格,连接点得风格。Container需要配置一个对应的DIV容器的id。(这里也可以使用setContainer的方法)

下面我们要创建一个节点(node),每一个节点可以用一个DIV来实现。我这里提供了一个函数来创建节点。

function addNode(parentId, nodeId, nodeLable, position) {
  var panel = d3.select("#" + parentId);
  panel.append('div').style('width','120px').style('height','50px')
    .style('position','absolute')
    .style('top',position.y).style('left',position.x)
    .style('border','2px #9DFFCA solid').attr('align','center')
    .attr('id',nodeId).classed('node',true)
    .text(nodeLable);

  return jsPlumb.getSelector('#' + nodeId)[0];
}


这里做的事情就是创建了一个DIV元素,并放在对应的容器的制定位置上,注意为了支持拖拽的功能,必须使用position:absolute 。

我使用D3来操作DOM,大家可能会更习惯JQuery,这纯属个人喜好的问题。

最后返回创建节点的实例引用,这是的selector使用了jsPlumb.getSelector()方法,它和JQuery的selector是一样的,这样用的好处是你可以使用不同的DOM操作库,例如Vanilla

下面我使用一个函数来创建端点/锚点(anchor),锚点就是节点上的连接点,用于连接不同的节点。

function addPorts(instance, node, ports, type) {
  //Assume horizental layout
  var number_of_ports = ports.length;
  var i = 0;
  var height = $(node).height();  //Note, jquery does not include border for height
  var y_offset = 1 / ( number_of_ports + 1);
  var y = 0;

  for ( ; i < number_of_ports; i++ ) {
    var anchor = [0,0,0,0];
    var paintStyle = { radius:5, fillStyle:'#FF8891' };
    var isSource = false, isTarget = false;
    if ( type === 'output' ) {
      anchor[0] = 1;
      paintStyle.fillStyle = '#D4FFD6';
      isSource = true;
    } else {
      isTarget =true;
    }

    anchor[1] = y + y_offset;
    y = anchor[1];

    instance.addEndpoint(node, {
      uuid:node.getAttribute("id") + "-" + ports[i],
      paintStyle: paintStyle,
      anchor:anchor,
      maxConnections:-1,
      isSource:isSource,
      isTarget:isTarget
    });
  }
}


instance是jsPlumb的实例

node是我们用addNode方法创建的Node实例

ports,是一个string的数组,指定端点的个数和名字

type,可能是output或者input,指定端点的种类,一个节点的输出端口可以连接另一个节点的输入端口。

这里anchor是一个四维数组,0维和1维分别是锚点在节点x轴和y轴的偏移百分比。我这里希望把端口画在节点的左右两侧,并按照端口的数量均匀分布。

最后使用instance.addEndpoint来创建端点。注意这里只要指定isSource和isTarget就可以用drag&drop的方式来连接端点,非常方便。

下面一步我们提供一个函数来连接端点:

function connectPorts(instance, node1, port1, node2 , port2) {
  // declare some common values:
  var color = "gray";
  var arrowCommon = { foldback:0.8, fillStyle:color, width:5 },
  // use three-arg spec to create two different arrows with the common values:
  overlays = [
    [ "Arrow", { location:0.8 }, arrowCommon ],
    [ "Arrow", { location:0.2, direction:-1 }, arrowCommon ]
  ];

  var uuid_source = node1.getAttribute("id") + "-" + port1;
  var uuid_target = node2.getAttribute("id") + "-" + port2;

  instance.connect({uuids:[uuid_source, uuid_target]});
}


node1和node2是源节点和目标节点的引用,port1和port2是源端口和目标端口的名字。

使用instance.connect方法来创建连接。 overlays用来添加连接线的箭头效果或者其他风格,我这里没有使用,因为觉得都不是很好看。大家如果要用,只要把overlays加入到instance.connect的方法参数就可以了。

调用以上方法来创建节点,端点和连接线。

var node1 = addNode('container-id','node1', 'node1', {x:'80px',y:'20px'});
var node2 = addNode('container-id','node2', 'node2', {x:'280px',y:'20px'});

addPorts(instance, node1, ['out1','out2'],'output');
addPorts(instance, node2, ['in','in1','in2'],'input');

connectPorts(instance, node1, 'out2', node2, 'in');


这里我们创建了两个节点,第一个节点有两个输出端口,第二个节点有三个输入端口,然后把第一个节点的out2端口连接到第二个端点的in端口。效果如下:

最后我们给节点增加drag&drop的功能,这样我们就可以拖动这些节点来改变图的布局了。

instance.draggable($('.node'));


这里似乎依赖于JQuery-UI,我还不是很清楚。

交互式创建节点

我们已经初步具有了创建图的功能,可是节点的创建必须通过程序,我们希望用交互的方式来创建节点。

通常我们希望有一个tree view的控件,让后通过拖拽来创建对应类型的节点。这里我使用了这个开源的tree view,基于bootstrap https://github.com/jonmiles/bootstrap-treeview

我们先创建一个tree view:

function getTreeData() {
  var tree = [
    {
      text: "Nodes",
      nodes: [
        {
          text: "Node1",
        },
        {
          text: "Node2"
        }
      ]
    }
  ]; 

  return tree;
}
//Initialize Control Tree View
$('#control-panel').treeview({data: getTreeData()});


树上有两个节点:

然后我实现从树上拖拽对应的节点,到流程图上的逻辑。

//Handle drag and drop
$('.list-group-item').attr('draggable','true').on('dragstart', function(ev){
  //ev.dataTransfer.setData("text", ev.target.id);
  ev.originalEvent.dataTransfer.setData('text',ev.target.textContent);
  console.log('drag start');
});

$('#container-id').on('drop', function(ev){
  //avoid event conlict for jsPlumb
  if (ev.target.className.indexOf('_jsPlumb') >= 0 ) {
    return;
  }

  ev.preventDefault();
  var mx = '' + ev.originalEvent.offsetX + 'px';
  var my = '' + ev.originalEvent.offsetY + 'px';

  console.log('on drop : ' + ev.originalEvent.dataTransfer.getData('text'));
  var uid = new Date().getTime();
  var node = addNode('flow-panel','node' + uid, 'node', {x:mx,y:my});
  addPorts(instance, node, ['out'],'output');
  addPorts(instance, node, ['in1','in2'],'input');
  instance.draggable($(node));
}).on('dragover', function(ev){
  ev.preventDefault();
  console.log('on drag over');
});


这里要注意的是要避免和jsPlumb拖拽端点的逻辑冲突,当检测到target是jsPlumb对象是需要直接从drop方法中退出以执行对应的jsPlumb的drop逻辑。

好了,一个绘制流程图的软件工具初步完工。

我把代码放在oschina的代码托管服务上了, 大家有兴趣可以去试试。

anvas 简介

HTML Canvas,说得简单一点,就是使用 JavaScript 操作 Canvas 元素绘制各种图形的技术。

使用 Canvas 元素绘制图形大体需要三步:

  1. 获取 Canvas 对象;
  2. 获取上下文环境对象;
  3. 开始绘制图形。

入门示例

我们先来编写一个非常简单的入门示例 —— 绘制一条直线。

声明 Canvas DOM 元素

<canvas id="canvas" width="200" height="150" style="border: 1px dashed #333333"></canvas>

声明 Canvas DOM 元素时我们指定了4个属性:id、width、height 和 style。默认情况下,Canvas 的宽度为300px,高度为150px。

操作 Canvas 元素绘制图形

// 1、获取 Canvas 对象
var canvas = document.getElementById("canvas");
// 2、获取上下文环境对象
var ctx = canvas.getContext("2d");
// 3、开始绘制图形
ctx.moveTo(50, 100);
ctx.lineTo(150, 50);
ctx.stroke();

我们首先使用 document.getElementById()方法获取到 canvas 对象,然后使用 canvas 对象的getContext("2d")方法获取上下文环境对象 context,最后使用 context 对象的属性和方法绘制了一条直线。

Canvas 对象常用属性和方法

属性或方法

说明

width

Canvas 的宽度

height

Canvas 的高度

getContext("2d")

获取 Canvas 2D 上下文环境对象

获取 Canvas 的宽度和高度:

var canvas = document.getElementById("canvas");
var width = canvas.width;
var height = canvas.height;
console.log("Canvas的宽度为:", width, ",高度为:", height);

直线的绘制

在 Canvas 中,绘制一条直线需要 moveTo()和lineTo()两个方法配合使用。绘制一条直线的语法如下:

ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();

其中ctx表示上下文环境对象 context。

(x1, y1)表示直线的起点坐标。moveTo(x1, y1)的含义是将画笔移动到(x1, y1)位置。

(x2, y2)表示直线的终点坐标。lineTo(x2, y2)的含义是从起点(x1, y1)位置到终点(x2, y2)位置绘制一条直线。

ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);

上面两行代码只是确定了直线的起点和终点,还没有真正绘制直线。只有调用了上下文对象的stroke()方法才是真的执行了绘制操作。

绘制直线示例完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>绘制直线</title>
  </head>
  <body>
    <canvas
      id="canvas"
      width="500"
      height="500"
      style="border: 1px dashed #333333"
    ></canvas>
    <script>
      window.onload = function () {
        // 1、获取 Canvas 对象
        var canvas = document.getElementById("canvas");
        // 2、获取上下文环境对象
        var ctx = canvas.getContext("2d");
        // 3、开始绘制图形
        ctx.moveTo(50, 100);
        ctx.lineTo(150, 50);
        ctx.stroke();
      };
    </script>
  </body>
</html>

用直线画一个三角形

应该是最容易让人忽略的一个问题,容易到很多工作几年的前端开发工程师都不太注意如何写好html及css

HTML&CSS 重要吗



相比于JavaScript,HTML&CSS确实凸显不出它的重要性,因为HTML&CSS不承载业务逻辑,具体来说他们不能称之为编程语言,那HTML&CSS真的就那么简单到不起眼吗?

我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

布局是项目易维护的重要基础

当我们拿到项目ui图后,建议大家不要一行一行的去画页面,这样不经效率低而且写出来的页面代码比较臃肿,下面是Element-ui的栅格布局及布局容器图:


一旦有了项目的大体架构,我们就可以做到手里有粮心中不慌了
我们以京东首页为例,来说明下良好的布局应该是什么样子的


可以看到,我在首页任意调换两个div,页面的布局也跟着调换过来而且没有乱,这就是一个良好的布局。你也可以在自己的项目这样试一试



管理好CSS有助于提高项目性能

说到CSS我们是势必要说到两个概念:重绘&重排

  • 重绘 重绘是指当 DOM 元素的属性发生变化 (如 color) 时, 浏览器会通知render 重新描绘相应的元素, 此过程称为重绘。
  • 重排 重排是指某些元素变化涉及元素布局 (如width), 浏览器则抛弃原有属性, 重新计算,此过程称为重排。(重排一定会重绘,重绘不一定重排)。

页面渲染的一般过程为JS > CSS > 计算样式 > 布局 > 绘制 > 渲染层合并而在这个过程中其中,重排和重绘是整个环节中最为耗时的两环,从重绘和重排的概念上看,重排比重绘更加的消耗性能,所以我们尽量避免着这两个环节。从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,只需要做渲染层的合并即可。

如何更好的写CSS&HTML

说了这么多,那在项目中应该怎么规划我们的布局及样式呢,一下是我个人的总结,和大家分享

  • 首先定义项目的基准样式:如重置样式,公用样式变量,兼容性处理等,且最好用less/sass/stylus等来写我们的css
  • 把项目的公共布局及样式抽离出来:如公用的头部,公用的尾部,公用的tab等
  • 避免样式重复赋值,避免样式重叠:如避免在业务或者组件里面写全局样式,样式层级不要过深
  • 用好z-index,position



最后


作者:刘小灰
链接:https://juejin.im/post/6854573211548549127