吾来-画布功能要点汇总

要点汇总

  • 画布的前身今世
  • 技术选择
  • 技术实现

画布的前身今世

画布是任务场景中对于对话单元的设置以及排布的区域,以下介绍对比了画布的演进历史。

DM1时代

DM1(Dialog Manage 1st version)对话管理 即 任务对话管理第一版本;

现在(2019.11.25)依然呈现在项目中-任务对话-场景列表-「旧版」场景;

里面的触发器和对话单元呈现y轴排布,编辑场景对于对话单元的排布要求编辑人员有较高的水准,界面较为简陋,使用起来不是特别得心应手。

DM1.8时代

DM1.8 这个数字非常有趣,它不是1也不是2,是一个中间版本;
它属于DM2数据的前身,现在已经下线。
那为什么会出现这个版本呢?
想法来源 : 界面采用了和DM1一模一样的配置,不同点在于,算法团队领先于迭代2个月升级了数据库,升级了算法结构,在非常紧急的情况下需要兼容到DM2的数据结构以及DM1的用户交互,花费1个迭代 + 10.1黄金周的时间完成。

DM2时代

DM2(Dialog Manage 2nd version)任务对话管理第二版本
现在项目中的主流场景,也是现在维护的重心所在。

从十月中旬到11.22日上线,由乔岳领衔,张熠、康永胜通力合作,实际开发时间 为(10.27-11.22),胜利上线。

技术选择

在DM1的时期,就有非常多的抱怨声音:

对话单元排布不够直观;

找不到自己需要的对话单元;

流程必须脑子想;

顺序调整麻烦;

……

CEO做出指示

“ 我们要打造一款市面上与众不同的对话编辑产品 ”

于是,DM2的需求就这么拍下来了……

CEO再指示 “ 我们要在11.22日顺利上线 ”

于是,DM2需求的截止日期也拍下来了……

鲁迅先生说过:“ 调研技术方案很复杂 ”

的确是这样,按照惯例,我或许想要去扒一扒类似产品的技术栈,然而现实很残酷,我没找到类似产品。

好的,这难不倒我,我想了别的方法——在谷歌搜索中敲入「可视化操作 js 前端 模块拖拽」等的关键词。

结果非常喜人,D3.js 作为最高的推荐进入了我的选择。

D3.js

https://d3js.org/

D3js作为行业内非常受人关注的可视化工具,里面提供了大量的实例,沉浸在大家开发的各(hua)式(li)各(hu)样(shao)的工具中,我感到异常欣喜——或许调研工作就这么完成了!

我太天真了

D3js 它并不是一个开箱即用的工具,丰富的画布操作、酷炫的物理引擎,在他们的背后是一个又一个的数学函数在支撑。尽管我能找到在界面上有95%相近的模板,但是剩下的5%却可能遥不可及。

在处理了第三个DEMO转换失败之后,我放弃了选用D3js。

2. Canvas

https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API

几乎相同的理由,相比于D3js, Canvas 似乎更加原始,通过丰富的API去展示画布中的信息,但是操纵画布怎么返回信息到前端?

‘That really makes me confused !’

闪电般的速度,我就放弃了

3. JSPlumb

https://jsplumbtoolkit.com/

社区版的文档地址

感叹着 我太南了 的时候,不经意间进入了这个网站。

哦吼!

这看似一般的界面下,隐藏四个大字(就是他了)

那么好处都有啥?

  • 看起来就是个编辑流程的玩意儿;

  • 封装了足够多的拖拽连线的事件;

  • 提供了足够的API在社区版本中;

澳大利亚的团队好说话,写了个邮件人家就让我免费使用开发版(一个月);

距离截止日期又近了几天,要来不及了;

用简单的DEMO证明了从数据到画布是可行的,在画布中自定义单元的样式也是可行的,免费的,可以在React技术栈中使用可行的,连线的效果设计师看了都👏是可行的……

没错,最后我选择的技术栈就是使用JSPlumb。

技术实现

这是重点,这是重点,这是……

前端前技术

框架确定好了,那么根据框架特性,我们要确定一个总体的思想——数据流向问题。

需求为 做一个可以多版本的可视化的能拖拽的能连线的 画布,细节自己琢磨。

前端的目光先聚集在“多版本”之后:

  • 可视化:框架已经帮忙做好了;

  • 能拖拽:框架也帮忙了;

  • 能连线:框架也做好了;

收工!

然而真的是这样吗?

我们做一些对话单元的div放在那里,拖进画布中,画布返回结构,如果需要保存那就再做一次映射?

错,那样的产品不可能免费

实际上JSPlumb的行为完全是由我们操纵的:

渲染画布:从数据库中读取数据,按照JSPlumb官网的要求生成所需的数据结构,传递到画布实例中,渲染生成;

对话单元拖入画布中,放下: 画布监听鼠标放置的位置,返回位置信息(画布左上角为(0,0)向左向下递增),我们修改画布数据,渲染单元;

对话单元连线:连线需要知道两个端点,数据结构为

1
{from: id1, to: id2}

相应的,每个对话单元块会有一个自己的block_id,(如果有多种跳转关系)那么每个单元块内部的关系会有自己的relation_id,通过这些id,就能定义好数据结构,修改画布数据,渲染连线。

由此可见,实际上我们的数据流向是单向的,不必担心画布还会生成一份数据进行糅合处理,永远是外层监听画布内的事件,通过修改渲染画布的数据结构来达到修改画布信息的目的。

前后端技术

MongoDB

“画布内要多版本”-产品经理如是说

MySQL的操作过于繁重,不适合做版本的管理。

经验丰富的康永胜同志果断的学习并且采用MongoDB的技术栈。

MongoDB拥有轻量的特性,读写速度快,用于画布内的数据存储和版本管理再好不过了,只有在发布画布内信息的时候,MongoDB才会把数据跟MySQL进行一次同步,更新到MySQL当中。

代码细节

目录结构

└── Visual
├── block
│ ├── ask.block.tsx
│ ├── …
│ ├── collect.block.tsx
│ ├── …
│ └── block.tsx
├── common
│ ├── bounce.tsx
│ ├── dimension.tsx
│ ├── header.tsx
│ ├── lemma.tsx
│ ├── option.tsx
│ └── topbar.tsx
├── configs
│ ├── block.config.ts
│ ├── instance.config.ts
│ └── jsplumb.config.ts
├── forms
│ ├── test.form.tsx
│ ├── test.style.ts
│ ├── trigger.form.tsx
│ └── trigger.style.ts
├── hoc
│ ├── drag.hoc.tsx
│ └── endpoint.hoc.tsx
├── canvasView.tsx
├── controlsView.tsx
├── index.tsx
├── visual.data.ts
└── visual.style.ts

block

画布中的对话单元模块,使用的时候有两种模式:

  1. 简单模式:如同询问单元

只需要传递block的种类

  1. 复杂模式:如同收集单元
    如果需要自己画出来,那就得自己render。

区分简单复杂模式有一个简单的方法,如果这个单元需要填槽,那么大概率他就是一个复杂模式,如果不需要,对话流只需要简单地通过单元,那就是简单模式。

common

该目录下放置了一些通用的组件:

  • bounce.tsx 左侧的抽屉(点击打开关闭)

  • dimension.tsx 下方的缩略图(minimap)

  • header.tsx 画布上方的顶栏(意图名称、设置、发布等等)

  • topbar.tsx 对话单元模块的顶部(对话单元的icon,编辑按钮,删除按钮x)

  • option.tsx 对话单元模块的区域(跳转关系等,使用endpoint.hoc.tsx包裹生成端点间连接能力)

  • lemma.tsx 对话单元模块的区域(默认文案、添加关系等)

configs

  • block.config.ts 处理对话单元数据(格式化,一些ENUM定义,提取数据中有关单元信息等的文件)

  • instance.config.ts 画布实例配置(全局加锁,用于打开编辑框时)

  • jsplumb.config.ts 画布相关配置(点、线、画布等)

forms

表单类信息

test.form(style).ts 编辑对话单元详情的文件,非常非常大,希望以后有谁来拆分出来!

trigger.form.ts 以前用来编辑触发器的文件

hoc高阶组件

drag.hoc.tsx 赋予组件拖动能力,设置对话单元的初始位置(top left),新增端点

endpoint.hoc.tsx 赋予对话单元模块中的端点能力,包括鼠标进出的效果。

配合操作

index.tsx 作为画布的入口文件,是输出和渲染画布的主要文件。当路由解析到对应画布的路由的时候,index.tsx就被激活了!

首先会经历一段生命周期,在即将装载的时候会获取一部分数据,以及注册一些方法(展示自动保存)。

装载完成后有个监听和 pushState 操作,这一步操作是为了防止mac用户在操作电脑的时候「左滑 移动 画布」直接触发历史回退,做了拦截。

其他都比较常规,属于看看注释就能看懂的。

visual.data.ts 是定义了画布内所有接口的文件,基本上在书写画布内代码的时候想要找的接口在这里都能找的!

visual.style.ts 画布内的大部分样式都在这里,除了对话单元编辑抽屉 和 触发器编辑抽屉

controlsView.tsx 画布页面左侧的对话单元陈列区域,拥有点击添加、拖拽(拖拽起点监听)添加等的能力。

canvasView.tsx 画布区域!实例化画布,渲染画布所需要的各种数据结构, 绑定所有建立关联的事件:

  • 鼠标移入;

  • 鼠标移出;

  • 删除连接;

  • 建立连接(分为触发器-对话单元,对话单元-对话单元);

  • 拖拽放置新建(onDrop监听nativeEvent , 通过layerX\Y来确定放置位置等);

业务迷惑行为学大赏

1. 填槽顺序调整

1.1 什么是填槽顺序?

如果一个意图中有多个询问单元或多个隐藏单元,在两种情况下,任务机器人需要用户手动进行流程校准才可以保证机器人跳转正常:

第一种情况:用户的消息可以同时填入多个对话单元对应的槽,这时优先填哪个槽?

第二种情况:用户的消息能更新意图下的某个词槽,这个词槽没有跟当前停留的单元绑定。这时应该:

让当前单元处理这条消息,还是:

让这条消息去填充能够填充的那个词槽,再让流程跳回重新判断

我们通过规定绑定了词槽的单元顺序(询问单元、隐藏单元)来解决这个问题。对于以上两个问题,只要根据这个顺序确定就可以了。

对于case A:按照单元顺序,排在前面的优先填

对于case B:按照单元顺序,如果被更新的词槽在当前词槽前面,就更新词槽;如果在后面,就让当前单元处理这条消息;

拖动单元前的 “把手” 进行排序;

1.2 代码层面:

在这个功能当中,发挥作用的字段为 first_order

如图中所示,自上而下,降序排列;

不同于对话单元生成时候增长的 order_id,一开始所有单元的 first_order 都是0,只有 type === ‘BLOCK-ASK’ || type === ‘BLOCK-HIDE’的单元在第一次保存校准值之后才会拥有自己的first_order,而且是递减的降序排列;

FAQ

连线的数据结构?

1
A: [{ from: relation_id, to: block_id }......] 从 id 到 id。

拖拽后块结构之间的连线是怎么实现的?

A: 连线分为两种模式:1. 画布中拖拽连接;2. 编辑器中设置跳转关系;

方式不同,但是最后都是通过改变渲染画布中连接关系的数据结构来实现连\删线的。

是否有画布能承载的最大单元数?

A: 前端没有做限制,在数据层似乎有个最大数的限制,🎙康师傅?

单元边上的端点是如何设置的?

A: JSPlumb提供了方法 jsp.addEndpoint;

连线上的删除icon是怎么放上去的?

A: JSPlumb提供了方法 getInstance().onConnectionOver 去设置;

每次改变时候草稿自动更新,是做的差量计算吗,都保存了那些值来对比是否改变了并存在哪里了?

A: 不是。更新的时候就是各自对应的接口的调用、传递、保存,只不过是全局做了一个动画来表示我们刚刚调用过接口;

拖动时,连接线的变化是如何实现的?

A: JSPlumb 工具实现好了,只要线连接上,移动对话单元,连接线也可以自己跟着跑;

左侧拖拽组件是画布自带的吗?拖拽是什么实现的?左侧的展示区域中的对话单元是怎么做到拖动到画布中的?

A: 左侧展示区域并不是画布内的功能,参照 controlsView.tsx;

拖拽是给 div 增加了 draggable属性;

参照canvasView.tsx文件所述,监听了画布区域中的onDrop事件,触发了一个方法

dropHandle,记录了(新的单元,位置信息,名字,种类等),调用新增对话单元的接口,同时更新树,渲染到画布中,等到接口返回数据,再更新redux中的block-list;

hover多端点的对应项时候,如何做到对应项的连接线也同样变化?

A: 参照 block.tsx 中的 option.tsx ,他使用 endpoint.hoc.tsx 高阶组件包裹了每一个跳转关系选项,再联合上 jsplumb.config.tsx 中hover线变色的配置,达成了这个效果;

底部缩略图如何实现的?

A: 参照 dimension.tsx 下方的缩略图(minimap);

点击还原时,接口返回的值如何对应的,还原出之前的画布?

A: 返回的数据就和初始拿到的一样即可,就是一次重新打开(渲染)的过程而已;

画布内块结构位置是怎么保存的?

A: 画布内的对话单元的位置的改变有三种情况:

点击左侧展示区对应的对话单元生成;

拖拽左侧展示区相应的对话单元生成;

挪动画布中的对话单元;

画布中还有一个特殊单位:触发器,他的位置只有挪动改变;

点击生成的时候,直接按照规则给出当前的pos(x, y),存储到数据中;

拖拽生成的时候,通过监听事件onDrop,触发了dropHandle,记录pos(event.layerX, event.layerY);

挪动对话单元、触发器的时候,参照 drag.hoc.tsx JSPlumb 提供了 getInstance().jsp.draggable的api,监听了拖拽的始末时刻,得到数值,调用接口存储;

在重新打开画布的时候会恢复之前的位置,如何实现?

A: 拿到数据之后,通过 drag.hoc.tsx 文件中的 render 中的 style,告诉每个对话单元的位置,利用DOM的能力放置每一个对话单元;

redux都存了什么?

A: 存了好多东西。

画布的redux存储的是画布相关操作引起的变化:

增\删\改对话单元

初始化跳转关系连接

增\删\改 跳转关系

开\关抽屉

画布加锁

简而言之分成3类,画布中 对话单元 (block)、画布中 跳转关系 (connection)、界面 UI。

作者: 张熠
文章链接: https://crazyoctopusdan.github.io/2020/04/09/%E5%90%BE%E6%9D%A5-%E7%94%BB%E5%B8%83%E5%8A%9F%E8%83%BD%E8%A6%81%E7%82%B9%E6%B1%87%E6%80%BB/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.