要点汇总
- 画布的前身今世
- 技术选择
- 技术实现
画布的前身今世
画布是任务场景中对于对话单元的设置以及排布的区域,以下介绍对比了画布的演进历史。
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
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
社区版的文档地址
感叹着 我太南了 的时候,不经意间进入了这个网站。
哦吼!
这看似一般的界面下,隐藏四个大字(就是他了)
那么好处都有啥?
看起来就是个编辑流程的玩意儿;
封装了足够多的拖拽连线的事件;
提供了足够的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
画布中的对话单元模块,使用的时候有两种模式:
- 简单模式:如同询问单元
只需要传递block的种类
- 复杂模式:如同收集单元
如果需要自己画出来,那就得自己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。