Think Tech About Rss

在流程设计器的开发中感觉:钉钉的流程设计器是否有问题?

2019-07-13 Think

2018 年在做设计和开发流程设计器过程中去参考了钉钉的流程设计器,但是在后来的逻辑中发现它好像有 Bug ,现在应该是改好了

设计调研

最近在做系统中的流程管理功能,对比了各大流程设计器,很多都要结合脚本进行实现。作为一名追求完美用户体验的全栈设计师,这种方式一定要不得。其实对于我们系统来说也不必要那么复杂的操作体验。寻觅一番,发现钉钉的请假流程体验很好。

界面简单明确,适合固定流程类型的系统。于是开始对其进行解剖,看看他是如何去管控流程的,并联系我这边的系统进行移植和优化。

功能设计分析

钉钉这个流程设计器将审批流程的节点分为三类:

  • 审批节点
  • 条件分支
  • 抄送

其中“审批节点”的审批类型分为:指定成员、主管、角色、发起人自选、发起人自己、表单里的联系人、连续多级主管、……,和这些类型的更细分的属性操作。

然后“条件分支”是对“请假”(这个只有请假流程设计)内一些指定字段的大于、等于、小于、大于等于……的判断然后根据判断结果输出多个分支,后面再跟上审批节点。

抄送”这里其实可以融合到任何一个节点里面去,但钉钉团队选择分出来也是很好的选择,操作人员理解起来比较直观。我的系统不是基于IM基础的所以抄送功能就变得不太重要了,但确确实我在做做用户调研时拿出钉钉的抄送功能让用户对比,很多用户反馈这个功能很好(他们可能没用过邮件)。

钉钉的流程设计器还做了容错判断,比如发布流程时需要判断各节点内容是否完整、合法。问题就出在这里,我们后文会讲到。

UI实现

钉钉这个流程设计器采用的是DIV+CSS方式实现,可以说这位老兄的css功底很了得。听说阿里的技术选型是React,所以这里应该是一个组件递归,数据结构也就是类似树结构,像下面这样:

把这个树结构转成流程形状有很多种办法。

但是要编辑这个就和传统的树结构编辑有点区别了。比如当用户在两个节点之间增加了一个节点,那么下面的节点的父级就变成了这个新增的节点,上面的下级也变成了这个新增的节点。

也有一种办法,就是服务端返回的就是一个展开的树数据,像这样:

{
    "1":{
        "name":"XXXX"
        "pid":"2",
        "type":"approver"
        ……
    },
    "2":{
        "name":"XXXX"
        "pid":"1",
        "type":"approver"
        ……
    }
    ……
}

然后就可以写一个操作的构造函数(不考虑DOM生成),大致结构和设计思路可以看看下面的:

class TreeOperate {
    constructor(arguments){TODO}
    /*
     * id生成,根据时间戳做一些处理生成唯一id
     */
    generateId(){}
    /*
     * 将原始数据组合成树结构,每次对节点操作都要执行一次重组,也就是说操作的是上面的展开的树数据,而不是组装的树数据。
     */
    compose(){}
    /*
     * 校验流程的合法性
     */
    werification(){}
    /*
     * 插入节点,有三种节点类型(分支、审批、抄送)
     * 还需要判断插入节点是否存在子节点,如果存在则将子节点pid改为当前节点id
     */
    addNodes(){}
    /*
     * 移除节点,同样判断移除节点是否有子节点,有的话先将自己移除,在使用addNodes()将自己的子节点放到自己的父节点内
     */
    removeNodes(){}
    /*
     * 修改节点,钉钉的流程设计器不支持拖动改变父节点和节点内容修改,那么修改节点也仅仅是节点的内容进行修改
     */
    editNodes(){}
}

缺点

像钉钉这样在编辑完成后再提交整体数据结构,那么各节点ID生成的重担就放到了前端开发人员的肩上,而且这样提交后的数据,后端必须清空现有流程节点再重新解析新数据填入表中,费事费力(如果不清空就要去重删除,那样更麻烦),这样也不太安全的。

我就在想钉钉这样做是为啥呢??实在没有道理,虽然钉钉细节上的东西很多,但不至于要这样做,难道仅仅是为了全盘校验?!如果是这样,那下面这样的流程通过合法性校验就有点匪夷所思了,请看下图:

这样一个明显不合法的流程被认定为合法。试想没有审批,条件分支以后直接结束流程,这? @钉钉前端工程师

本土化改进版

设计思路

发现了这些缺点后,我这边的系统采用的是vue进行前端编写,看下实现效果:

因为没有抄送,所以就只有两种节点类型:“审批节点”、“条件分支”。
看看审批节点的选项,理解一下我是怎样的思路:

因为在权限那里对发起人做了限制,所以就不需要钉钉那样的在流程内限制发起人,所以如图所示,选择审批人为上级部门主管,就只需要给节点取个名字就可以了。服务端会根据树去一级一级找(有一个流程进度表,专门记录流程进展),相关审批人就会收到审批请求,操作后服务端再设置相关状态值,前端根据状态值去判断显示。为了安全起见服务端还会做状态检验和URL拦截。

重点来了

这里点击保存按钮就会直接提交节点内容到服务端,服务端会自动重新组装返回数据结构给我,那我只需要再渲染一次就OK了!
删除也一样,我只把当前节点ID传给后端就可以了,然后后端返回删除后的流程节点数据给我,再重新渲染就是了。用一张图来对比一下这两种方法:
图层 4sss.png

问题

这种逐个操作法,总大的问题就是不能验证整体流程的合法性,很可能用户因此提交一个错误的流程,导致业务进程受阻,但通过对比正反向用例,发现担心是多余的,因为只有一种上面提到的“条件节点后不能没有审批”的这种合法性校验,这种逐个校验逐个提交的方式可以handle所有用例(这种合法性校验顶顶虽然使用了全局提交,但也没有做)。

终归问题还是要解决的,万一用户弄了这么一个流程怎么办?很简单,新流程提交的时候如果流程不合法就为用户返回“流程设计出现问题,请联系流程设计相关人员修改!”并阻断流程提交。

实现

看看文件结构

|-workFlow.vue
|-node.vue

利用组件递归方法,进行流程树渲染。下面重点看看node.vue的代码:

    <template>
    <div class="work-flow-item">
        <!-- 判断流程是否是分支,是的话循环分支内部节点 -->
        <div v-if="data.type == 'branch'" class="work-flow-conditionNodes c-flex c-flex-center">
        ……
            <Item v-for="item in data.conditionNodes" :type="type" :config="config" :key="item.id" :data="item"/>
        </div>
        <!-- item 主体开始 -->
        <div class="c-flex c-flex-center card-warp" v-if="data.type != 'end' && data.type != 'branch'">
            <el-card :class="data.type">
                <span slot="header">{{data.name}}</span>
                <!-- 判断流程是否为条件,是的话按照条件渲染 -->
                <div v-if="data.type == 'condition'" class="sys-flow-content">
                    <font v-if="data.params && config[type]">{{config[type].long}}:</font>
                    <font v-if="!data.params">其他条件进入此流程</font>
                    <span v-for="(val,key) in data.condition" :key="key">
                         {{equation[key]}}{{val}}  
                    </span>
                </div>
                <div v-if="data.type == 'approver'" class="sys-flow-content">
                    <font v-if="approver[data.approver.type]">审批人:</font>
                    {{approver[data.approver.type]}} 
                    <span v-if="data.approver.name">{{data.approver.name}}</span>
                </div>
            </el-card>
            ……
        </div>
        <!-- item 主体结束 -->
        <!-- 判断流程是否存在nextNode,如果有则去递归,没有就结束 -->
        <Item v-if="data.nextNode" :data="data.nextNode" :type="type" :config="config"/>        
    </div>
</template>

重点的样式

盒模型如下所示:
图层 4sss.png

连线采用beforeafter伪类,优点是控制灵活。

看下面一段代码:

.work-flow-item{
    .el-card{
        overflow: visible;
        position: relative;
        &::after{
            content: "";
            position: absolute;
            width: 2px;
            height: @height;
            background-color: @color;
            bottom: -@height;
            left: 99px;
        }
        &::before{
            content: "";
            position: absolute;
            width: 2px;
            height: @height;
            background-color: @color;
            top: -@height;
            left: 99px;
        }
    }
    .work-flow-conditionNodes{
        background-color: #f5f6f8;
        &::after{
            content: "";
            position: absolute;
            width: calc(~"100% - 200px");
            height: 2px;
            background-color: @color;
            bottom: 0;
            left:99px;
        }
        &>.work-flow-item{
            position: relative;
            &::before{
                content: "";
                position: absolute;
                width: 2px;
                height: 100%;
                background-color: @color;
                top: 0;
                left:calc(~"50% - 1px");
            }
        }
        &::before{
            content: "";
            position: absolute;
            width: calc(~'100% - 200px');
            height: 2px;
            background-color: @color;
            top: 0;
            left: 99px;
        }
    }
}

数据库相关

大致上把节点分为5类:

  • 开始节点
  • 审批节点
  • 分支节点
  • 条件节点
  • 结束节点
    其中“开始节点”和“结束节点”是唯一的,条件节点必须包含在分支节点内。详细说说明如下图所示:
    图层-4.png
(图片过大可以右键新标签打开)

我们系统中有多种内容需要流程审批,所以要记录多个流程。当发起某种内容需要审批时就会启动当前流程,并进入start节点,然后把所有节点复制一份存放到状态表的字段内方便下次调用。具体流程如图所示:
图层-4.png

End

Pre Next