【一】逻辑与Runtime
一、从可视化搭建说起
页面可视化搭建系统从16年开始如雨后春笋般涌现而出,从活动页搭建到中后台搭建,有开源有仅公司内部使用的,都致力于将前端从繁复的体力劳动中解脱出来,提高页面生产效率。优酷内部也有一套营销活动搭建系统,每年生产2K+活动页;能够满足这么多页面的需求,除了沉淀了大量可复用的组件外,围绕着搭建系统的前端研发每天都在不停地维护升级老的组件,同时生产新的组件。
痛点
页面生产能力上去了,研发还是一直埋头在组件开发需求中。这些需要都是从哪里来的呢?其实上面也有提到,就是两点:
- 老的组件需要添加新的能力,可能是UI改动,可能是逻辑变更
- 新的业务组件开发
需求是永远也做不完的,为了提高开发效率,研发侧不断地沉淀通用的基础库,与服务端商定标准化的接口,以此来减少维护成本,但基于现有模式锦上添花的优化远远不够。说白了,现有的可视化搭建效率和研发效率都已经达到瓶颈了,我们急需一种新的生产模式,给我们带来生产效率的突破性提升。
解法
我们思考一下,页面可视化搭建是如何解放生产效率的。它将完整的页面进行拆解,拆分为可以复用的组件,研发负责组件生产,产品运营负责组件配置,形成了一种简单的流水线作业的模式,这种模式的好处在于:
- 业务组件复用,避免重复开发,研发只需要专注于单个组件
- 产品运营可以介入页面生产过程,减少沟通损耗的同时分担了部分先前研发承担的工作
现在的瓶颈已从页面开发效率转变到组件开发效率上,我们做一个设想,让非研发角色也能介入到组件生产过程中,进一步提高生产效率。在此之前,我们需要先基于现有模式进一步拆分组件结构:
组件可以拆解为 UI + 逻辑。UI 可以通过细粒度的文字、图片、slider等组件搭建出来;逻辑主要涉及到接口请求、数据处理、能力调用,我们将一些常用的api调用比如跳转、常用的接口请求比如查询登录态等进行封装沉淀,加上语义化的描述,非研发人员可以将他们拖拽绘制成流程图,完成业务逻辑的编排。两者结合,就可以生产出完整的业务组件。而对于业务逻辑的组合编排,我们称之为逻辑编排。
举个栗子
我现在有个需求,需要实现一个简单的抽奖活动。页面分为两块,第一块是奖池,五个奖品横铺开,第二块是抽奖按钮,点击按钮,如果用户没登录,拉起登录面板,如果用户已登录,则进行抽奖并高亮展示对应的奖品。
我现在把UI搭出来了,就差逻辑来让对应的坑位展示了,我们把欠缺的逻辑部分拆成两段:
- 进入页面,也就是 componentDidMount 阶段,查询奖池
- 点击抽奖按钮时,查询是否登录,未登录拉起登录面板,已登录则调用抽奖接口
除了开始和结束以外的这种逻辑片段都是我们已经沉淀下来的,可以直接拖拽进来,绘制流程图。绘制完了后,我们选择模块的页面主动触发(设计给非研发用的,他们肯定不理解didMount),将它和查询奖品关联上;选择抽奖按钮的点击事件,关联上抽奖逻辑,这样模块就能在不同的时机做正确的事情了。可能你还有点小疑惑,我查询了奖品后,数据是如何映射到UI展示上的呢?要想知道如何映射,你得先知道逻辑是怎么运行的。
下图是我们在寒假战役中的一个应用场景:
二、Logic is Core
扯一点远的,不愿意看可以直接跳到“逻辑如何运行”小节。前端的逻辑编排相比服务编排要更复杂一点,并不是说前端逻辑编排更难,而是因为:场景更加多样化,C端模块搭建和中后台搭建的编排差异就很大;人员更加多样化,除了服务研发人员还要服务非研发人员;除此以外,还需要和UI进行结合,可能性就更多了。
这段时间比较火的 iMove ,初始被设计服务优酷活动搭建平台的逻辑编排,后面服务imgcook后,与优酷的逻辑编排在形态上已经截然不同,所以在前端比较难像服务编排一样,平台化、中心化然后去服务多种多样的业务。如果你真的很想去服务多样化的场景及业务,提供一个轻量级的库,让它足够灵活、可定制,这也是 iMove 正在走的路。
前面讲了很多,都是在说,逻辑编排需要follow不同的平台做对应的定制,但是不管形态怎么变,它的核心是不会变的,我们一定是在围绕着 “逻辑” 去做包装,将之打造为不同的产品形态。所以,逻辑才是核心!
逻辑如何运行
说了半天逻辑多重要,假使我现在抽离了一个函数出来,把它发布了,我又用它拖了个流程图,它怎么才能执行呢?
逻辑最后想要运行,无论你是出码(to code) 还是在线执行,你都需要一个“逻辑编排解释器”,这个解释器可以读懂编排后的逻辑并且去执行逻辑,这个解释器我们称之为 Runtime。代码是冰冷的,解释器是没有智商的,它并不能真的读懂逻辑,只是因为我们约定了一个规则,定好了编排后的逻辑可能有几种情况,每种情况应该如何做,Runtime 只是在依章办事,而这个规则就是 DSL 。所以逻辑想要运行,它依赖于 DSL + Runtime ,要做一个逻辑编排平台,也一定是先定好 DSL,实现 runtime ,后面才是去做平台。
每个沉淀下来的逻辑,我们将其定义为逻辑元件,加上语义化的元件名称及详细的描述,然后发布到元件市场。使用者只需要根据元件名称来挑选需要的逻辑,通过连接线将他们连接起来,就可以组合成一个流程图,这个流程图也就是一段完整的逻辑。
可是流程图导出来的 graph json 都是点和线的集合:
这种 json 如果直接用 runtime 执行有两个问题:
- 流程走向不直观,每个元件执行完后需要浪费时间去查找下一个元件是哪个
- 无效信息太多导致json体积太大进而影响加载性能,比如坐标信息(x/y)、标签信息(label) …
所以我们需要先去约定 DSL ,保证足够直观且只包含必要信息,所以我们设计了一个转换器将 graph json 转换为 DSL 。每个逻辑流程图对应一个 DSL 产物,借助 runtime 的 interpret 方法,它可能会运行在 useEffect 中,也有可能运行在某个元素的点击事件中,或者是页面的滚动事件中。
逻辑元件生产与消费的分工
逻辑元件从被编排到被运行的过程,也是它被消费的过程。文章到这里,相信大家可以感受到这个消费的过程,非研发同学确实是可以进入的,因为我们设计的足够简单。我们把整个页面的生产当做一条流水线的话,以前是 模块生产 —> 模块市场 —> 页面搭建,有了逻辑编排后,我们的流水线比之前划分的更细了。
这张图是产品进入到中期的一个形态,前期的时候产品的角色更多是由研发来承担的,然后逐渐过渡到产品,到了后期呢,产研联合给运营同学做培训,中间这部分会逐渐地过渡到运营同学那边。
因为整条线上大家需要关注的事情越来越细了,页面生产线出错的几率也会低一些。
三、逻辑编排
上面一直在从业务角度去聊,接下来就要深入到逻辑编排里去了,讲一讲逻辑编排的设计思想。逻辑编排最主要是分为三块,元件、编排器、runtime,我将它们称作逻辑编排三板斧。那问题就转换成了:
- 元件该怎么做?
- 编排器该怎么做?
- runtime该怎么做?
这三块各自拆解出基本要素,用基本要素来描述它们,互相之间建立起连接关系,这些构成了逻辑编排的规范协议。这样不管什么业务进来,只要遵循这套规范,它们的底层就是一致的,各业务也不会显得散乱。
DSL
在探讨我们的 DSL 之前,一定要先聊一聊DAG(有向无环图),因为我们的 DSL 设计本质上就是 DAG 。
DAG
DAG 的 G 指 Graph(图),图是数据结构中最为复杂的一种,我们在了解 DAG 之前,回顾图的几个要点:
- 顶点(vertices):图中的一个点
- 边(edge): 连接两个顶点的线段
- 度数(degree): 从一个顶点出发有几条边,这个顶点的度数就是几
图就是由一些顶点和边组成,边就是顶点间的关联关系。DAG 中的 D 是 Directed,代表是有方向的,就是说顶点之间的边带箭头,常见的比如食物链,就是有向的;A 是 Acyclic,代表无环,从某个顶点出发,无论走那条路,都不会回到那个顶点。
基于有向无环图来约定我们的 DSL 正合适不过:
- 我们需要有向边来告诉我们逻辑的走向
- 流程从开始节点出发一定要遇到结束节点才结束
- 我们的逻辑元件可能有多个出口,就像顶点可能会有好几个度,比如说判断是否登录,就算是2度
- 逻辑编排中我们没法要求流程图一定绘制成树(一个顶点到另一个顶点,只有一条路径)那样
基于DAG的DSL
每一个顶点都是逻辑元件的实例,继承自逻辑元件,也可以修改自身属性。逻辑元件分了两大类 - 基础元件和业务元件。基础元件目前只有开始和结束,其他所有需要研发开发的都是业务元件,这么设计是为了降低编排使用门槛,你不需要有任何编程基础。
- type: 目前只有三种类型,Start | End | Custom
- func: Custom 类型专用,可以是函数体,可以是函数名,如果是函数名,需要提前在 runtime 中注册
- payload: 元件会开放配置项给运营配置,form表单数据会作为payload传入给顶点对应的函数
- next: 每个顶点都会有一或多个出度(出口),next指向目标顶点的uuid
跟 Flow-based programming 不一样的是,我们移除了顶点之间的值的传递,运营不是研发,他们很难理解计算值如何流动以及如何操作它们。
Runtime
前面说到数据如何映射到UI时,不是故意卖关子,实在是要配合着 runtime 一起给您讲解一下。逻辑与UI的结合这里也只是粗略提一下,要留到后面的文章细讲。
Runtime与UI
React自身定位是用于构建UI的Javascript库,它做的就是通过 data 驱动 UI 展示,react 的 data 通常都存储在 state 中,我们刚才已经通过逻辑编排拿到了奖品数据,对此 runtime 唯一要做的就是在内部 data 与外部 react 中间架一座桥。
将逻辑内部生成的数据全部存储在内部 context 中,利用发布订阅模式,每当 context 有更新时,通知 UI 组件执行 setState,state 更新后,React 自动更新 UI。runtime.subscribe('context', (val) => { // handle data this.setState({ data });})
轻量的Runtime
YOHO 的 runtime 很小,不到 10k,麻雀虽小,五脏俱全,接下里我们看看这小麻雀是如何支撑起流程编排的最后一公里 - “执行逻辑” ,又是如何和业务打交道的😊。
逻辑元件本质上就是基于一段代码,给它加了一些描述信息,可是我的页面中是不会内置这一段段代码的,runtime 也不可能内置这些代码,也就是说我们的执行上下文中是没有元件对应的函数的;我们的业务是 No Code 形式,不出码,所以 Runtime 内部实现了一个元件管理器,你可以通过他进行元件的注册,在 DSL 执行过程中,它也会帮你进行元件检查。
Runtime 对每一次编排实例(流程)的调用都是在沙箱中进行,实例的执行是互不干扰的,而执行过程中每个元件输出的结果我们也做了隔离;其实我们还给元件之间互相通信提供了方法,但是目前不建议使用,因为元件之间随意通信有很大的副作用,我们目前还没有足够的产品形态去约束它。
在 runtime 设计过程中,我们预见业务会不断地有各种需求进来,可是我们不希望 runtime 过于业务定制化,导致将来积重难返,所以设计了生命周期。在编排实例执行的各个阶段,业务都可以进行干预。
使用发布订阅模式,而不是观察者模式的原因,也是我们希望 runtime 足够灵活。举例来说,上下文因为数据隔离,我们把它拆解为各个子上下文,你可以把每一个 childContext 当做一个 topic 来订阅,其他 childContext 更新并不会附带影响当前的。childContext 。
除了以上,runtime 只做了一个事情 —— 逻辑走向的调度,就是有向无环图中的 “有向”。正是基于以上的设计,我们的 runtime 足够小,但是又足够灵活,便于定制。
三板斧在本文中就先讲这第一斧😄
四、后话
在做逻辑编排平台的过程中,方案做过好几版——从一开始两周搞出 iMove 初版给老板去展示,然后想要拥抱集团,和优秀的编排平台 Logic Force 想着共建前端编排,因为我们最终的产品形态差异过大,LF想要支持也需要投入很大的精力,同时我们的业务想要快速验证,所以又重新自研了一个轻量的逻辑编排平台。期间有很多感悟,我们也都在逻辑编排的设计中添加进去了。
逻辑编排讲到这里还没有结束,我们后面还会有系列文章,元件和编排器是如何设计的呀,和UI进行联动,UI侧又是怎么设计的呀,我们在逻辑编排方面又做了哪些前端特色的东西呀?如果你也感到好奇,记得关注我们噢!
【二】编排器与业务
一、背景与价值
说到逻辑编排大家应该都不陌生了,目前我们集团有多面向后端的逻辑编排技术专项,且没有统一的标准、沉淀通用的方案。也有前端逻辑编排项目,但均面向前端开发提效的逻辑编排,而我们是要打造一个面向非研发人员,可让他们根据图形化组件搭建出逻辑的平台。
为什么要做这个呢,随着页面可视化搭建的蓬勃发展,互动营销类的页面/组件需求日益增长,为了提高开发效率,研发侧不断地沉淀通用的基础库,与服务端商定标准化的接口,以此来减少维护成本,但现有的可视化搭建效率和研发效率都已经达到瓶颈了,再多的需求进来也是在堆人力了,经常会出现资源不足、排期紧张的情况,我们急需一种新的生产模式,给我们带来生产效率的突破性提升。
那我们前端尝试了页面级、模块级的复用,减少代码重复开发,提升产研效率。我们是否也可以拆分到更小粒度,函数层的逻辑复用呢?这就是我们近期打造的具有前端特色的逻辑编排平台 - YOHO,建立令非研发人员自由且规范化编排逻辑的编排器,以NoCode的形式,借助图形化组件,完成逻辑的搭建生产,并可复用于多业务场景。
本文通过逻辑编排可视化搭建的设计与业务实践,来和大家做一些交流。
二、编排器
基础概念
编排器是逻辑编排中通过可视化搭建,生成一条具有业务逻辑的容器;将逻辑流程图导出,通过DSL转化,生成业务中实际执行的代码逻辑。可以理解为逻辑编排中生产逻辑的环节。
平台侧编排器设计为如图四个部分。工具栏、元件列表、搭建画布、基础配置。
工具栏包含保存逻辑、检查搭建规范性和导出逻辑这些基础操作。
元件是逻辑中的一个节点,一个函数,是将业务中常用的逻辑抽离出来,按照编码规范沉淀下来。如下图列表中有不同形状色彩的元件,是我们根据函数类型对元件的进行的分类。
中间部分是进行逻辑搭建的画布,主要操作动作是将逻辑元件拖拽进来,通过具有指向性的线条将元件连接起来,组合成一条完整有始有终的逻辑,实现简单的或者复杂的具有特定业务含义的流程图,也是核心部分。
右侧是点击某个元件所显示的对应基础配置信息面板,即函数的入参,我们的设计一定是令用户可灵活配置元件基础信息。
在设计该编排器的时候,我的目的是通过技术手段,建立令用户最大化自由编排且规范的编排器,使之无侵入、可复用各业务场景。下面介绍我们是如何具体设计的。
三、整体设计
元件设计
基于G6绘图引擎能力,根据元件定义类型,注册出不同形状、色彩、结构的元件。元件由主体形状、分支线、描述文字、节点四部分组成。其中节点如图也分上下两部分,上方节点是其他元件连接至当前元件的节点,下方节点是当前元件可连接至其他元件的节点。
为了令用户更丝滑且更标准化地搭建逻辑,我们在设计元件时将一切限制条件,得已显示,在用户所有可执行的动作中,得已体现。例如我们在初始化元件环节,开发者可设定好该元件会有几种输出情况、几个分支,每个元件可连接几个其他元件,每条分支代表哪种结果导向,定向去做关联。所见即所得,不让用户去猜或任意自定义连接。
这样设计的目的:减少用户操作成本,减少解释成本,增强搭建规范性。
元件类型
根据元件是否含有逻辑,将元件分为两大类:基础元件、业务元件。如上树形图。
基础元件不带有任何业务逻辑,包含开始、结束元件,固定为一条逻辑的起点或终点标识,每条逻辑必有开始元件和结束元件。把基础元件设计为圆形、绿色;
业务元件均带有业务逻辑,仅有一个输入节点,依据元件可输出的结果数量,将业务元件又更细粒度地分为三种类型。(详情如下表)
结束业务元件,虽然带有业务逻辑,但也可能是一条逻辑的终点。如跳转新页面逻辑,这个动作执行后会跳转到一个新地址,无法在当前页面当前逻辑继续传递至下一动作,所以称之为结束业务元件,所以设计它为仅有一个输入节点,无输出节点。因为同样标识一条逻辑的结束,我也将它设计为圆形,用黄色作为区分。
普通业务元件就是一个正常有输入且有固定一种结果导向的元件,比如通过接口请求获取数据,会有固定schema格式的返回数据输出;所以普通业务元件有一个输入节点,一个输出节点,形状为长方形。
判断业务元件会复杂点,根据元件设计规范,在创建元件时即可确定有多种结果输出,该结果为>1种,且也会有固定schema格式的返回数据输出。如判断用户是否登录,分为已登录、未登录两种情况;判断业务元件也不是非1即0的情况输出,也可能会有a 、b、c、d…多结果导向。因此普通业务元件有一个输入节点,N个输出节点,形状设计为菱形。
我们根据函数类型定义、注册元件,尽可能涵盖所有的类型,目的就是让这套逻辑编排方案通用化、可复用,适用于多业务、多场景。
注册元件
元件有多种形状、颜色、分支、节点不定,平台侧注册元件的思路:拿判断业务元件为例,使用SVG绘制菱形 - 元件主体形状,根据元件类型自定义元件样式,根据元件的分支数量,动态绘制分支线的长度、节点位置、锚点信息等等。
G6.registerNode(
'diamond',
{
draw(cfg: Cfg, group: GGroup) {
const size = [94, 64]; // 转换成 [width, height] 的模式
const width = size[0];
const height = size[1];
// svg 自定义主体形状
const path = [
['M', 0, 0 - height / 2], // 上部顶点
['L', width / 2, 0], // 右侧顶点
['L', 0, height / 2], // 下部顶点
['L', -width / 2, 0], // 左侧顶点
['Z'], // 封闭
];
...
},
afterDraw(cfg: Cfg, group: GGroup) {
const size = [100, 64];
const width = size[0];
const height = size[1];
// 分支描述信息
addDesc(cfg, group, width, height, cfg.outLine);
// 分支线
addLine(group, width, height, cfg.outLine);
// 连接锚点(小圆点)
addNode(group, width, height, cfg.outLine, cfg.showNode);
},
...
},
'base-node',
);
元件分支线也是长短、方向各异,线的长度由分支数量决定,即代码中outline字段:
const addLine = (group, width, height, outLine) => {
group.addShape('path', {
attrs: {
path: [
['M', -width / 2 * (outLine - 1), height + 3],
['L', width / 2 * (outLine - 1), height + 3],
['Z']
],
...edgeStyle
},
draggable: true,
});
}
元件的节点分为一个输入节点,一组输出节点,输出节点的位置依然由outline决定
const addNode = (group, width, height, outLine, isShow) => {
// x坐标
let xPosition = i => {
return -width / 2 * (outLine - 1) + width * i;
}
outLine && new Array(outLine).fill(0).forEach((d, i) => {
group.addShape('circle', {
attrs: {
x: xPosition(i),
y: height + 4,
r: 4.5,
...dotStyle
},
name: `circle-shape-${i + 1}`
});
})
}
创建元件
用户需要根据规范,去创建一个新的元件,初始化项目包含四个核心文件,拿基础业务元件举例,如图所示。
index.js 是元件的主体函数逻辑文件,函数一般是有入参的,且可以让用户灵活配置。
input.json 文件即定义为函数基础配置信息的标准Schema协议,该内容在平台侧可渲染为表单,供用户配置基础信息,在逻辑执行过程作为payload参数传入对应函数。
基础业务元件,函数执行有固定一种结果输出的情况,那branch.json这个决定分支数量与分支信息的文件就有数组长度为1,只输入一项内容,录入即可。
output.json 是主体函数执行后最终return返回的data数据对应的标准json schema协议信息。输出数据是逻辑编排的产物,如图,可供业务侧绑定对应字段,可视化显示出对应内容。
不同元件类型,项目结构是一样的,不同的是,输出结果的数量不同,主体函数异步返回的内容结构有区别。
如检查登录元件,会返回已登录、未登录两种情况,那主体函数会调用相应的api后,经过判断在两种返回情况里做选择。返回信息branch为0,即为branch文件对应的数组第一项的未登录的返回情况;branch为1,即为branch文件对应的数组第二项的已登录情况,返回data也对应已登录的消息提示信息。Branch文件的数组长度即为元件输出结果的数量,数组内容也是语义化对应的。这就是元件整体结构的设计与对应开发录入规范,开发者根据这样的规范即可创建对应类型的新元件。
input、output文件的schema标准协议数据我们也提供了【可视化搭建 表单设计器】供开发者快速搭建出对应内容。
基于form-render的可视化搭建
平台嵌入了基于 form-render的表单设计器,可联动、自定义组件快速接入,满足多元定制化需求。考虑到form-render在 schema 的设计上,寻求合适的现有标准作为载体在其上适度扩展,经过长时间积累已经可以覆盖大部分业务场景,所以基本可以实现我们的业务诉求,即可能会有的扩展,所以选择该schema to form方案。
技术同学可根据功能需求,通过可视化拖拽的方式搭建出配置表单;点击导出Schema,即可导出标准json schema协议信息,这就是元件注册环节的基础配置信息InputSchema协议来源,也是输出数据outputSchema标准协议信息来源。
提供搭建的元件市场
逻辑编排
搭建逻辑,是将元件列表中的元件拖拽至画布中,通过具有指向性的线条将节点与节点对应连接起来,即可将逻辑片段组合成一条完整有始有终的逻辑。(效果展示)
通过技术手段,增添了搭建智能对齐、检查规范、自动纠错等能力。点击工具栏中检查按钮、保存按钮,都会根据规则对不规范搭建行为提示说明,例如元件基础配置表单某字段不可为空;在用户搭建过程,我们也设计了元件连接规则,对不规范搭建行为自动纠错,例如元件不可闭环连接、不可重复连接等。我们建立令用户最大化自由编排且规范化的编排器,使用户在搭建、发布逻辑时,已经是规范可使用的逻辑。
当然我们在设计编排器上,也将物料列表、画布、属性面板,做解耦处理;每个功能面板不会强依赖于另一个,有通用适配器,设定好的基础规范与可供选择使用的工具,也有可根据不用业务场景做定制化的板块。我们的初衷是,以逻辑搭建为中心,可根据实际业务场景打造不同的产品形态。
四、业务落地
UI & 逻辑编排结合设计
我们首个尝试落地的业务平台也是我们团队的活动搭建平台,与UI编排的结合尝试。UI编排的概念:也称为通用热区模块,是活动搭建平台推出的模块生产工具,提供易用、强大的操作界面,让运营和设计同学像制作PPT一样制作模块。如图,UI编排完全可实现一个静态页面的搭建;用户可任意创建热区模块,热区作为一个载体可配置文字、样式、图片等基础信息,在对应热区绑定“动作”,动作的概念就是用户主动点击后所触发的一个行为,如跳转页面、关闭页面、打开小程序等等。这是纯UI编排搭建。
在近期的活动应用中,一个模块使用UI编排进行了搭建尝试。我们来看下UI编排与逻辑编排结合起来的整体设计。
在模块中触发逻辑分为两种情况,一种是主动触发逻辑,即伴随组件加载自动开始执行的逻辑。一种是被动触发逻辑,需要用户点击才可触发执行的逻辑。在对应功能配置区选择提前搭建好的逻辑即可将逻辑挂载到当前模块。
如图,每一条逻辑都拥有独立上下文,逻辑执行后对应的数据产物,会存于上下文并与当前模块共享;模块将所有数据以key.value的格式打平,通过语义,将视图中的热区绑定对应字段;一旦逻辑执行过程中数据状态发生改变,就会驱动模块的局部UI动态渲染。若模块的状态绑定了对应字段,那字段数据状态发生改变,也会驱动模块整体UI改变。
平台侧实际操作,如下图右侧,热区选择动作中的“逻辑编排”,二级下拉框选择一个 从逻辑编排平台搭建好导出的逻辑。
逻辑的数据产物根据outputSchema协议可视化展示在业务平台侧,对应热区模块可选择/绑定对应字段内容,逻辑真正执行时,数据驱动视图改变。
前后对比 - 优势
从前:产品提了需求后,前端开发同学需要依赖设计稿,开始页面开发&逻辑开发,开发完成后发布、录入模块至平台侧,运营同学方可配置模块基础信息。
现在:在了解需求后,前端&设计可并行启动工作了,前端同学开始开发该模块需要依赖的逻辑元件,开发完成后产品根据需求搭建出逻辑;设计同学也可开始绘设计稿并在平台侧可导入整体样式,进而运营同学去配置基础信息、对应动作选择匹配的逻辑。
这样的优点是:项目进行中,UI、逻辑的频繁改动不会太过于依赖技术开发、技术发版,将重开发拆分为多角色轻工作快迭代; 我们从模块复用,下沉到更小粒度的元件复用,释放研发人员的 “搬砖” 工作。经验证,传统源码开发从接到需求到上线大概需要评估2天时间,通过UI逻辑编排结合的方案仅需要2h。真正实现降本提效。并且最大的优点是做到逻辑与UI的解耦。
当UI设计稿有变动时,前后流程的对比:
纯UI改动已经完全释放开发资源,节省源码开发 -> 代码发布 -> 模块录入/上架 -> 运营升级模块的流程,既节省开发人力成本,运营同学通过增/删/改配置信息也不再依赖开发。
虽然逻辑变动看似多了一个判断步骤,但若已有可复用的逻辑元件,产品直接拖拽更新逻辑即可;无可复用元件需开发同学新开发。但长远角度思考,我们从模块复用,下沉到更小粒度的元件复用,元件库不断沉淀着逻辑,未来元件复用率一定是增长的。我们从开发角度,也在规划从本地元件开发升级为在线开发,一键发布并上架元件。让流程编排效率更大一步提升。
五、结构图
这张结构图展示了逻辑编排与业务平台侧整体上下游关系
逻辑编排分为编排器、DSL、Runtime三部分
- 编排器基于Schema解析和G6绘图引擎,结合对元件类型的定义注册绘制出不同形态与功能的四种元件,供画布搭建使用;元件是由开发者开发,且可在元件管理页面对元件进行管理。画布包含缩放、撤销等工具栏功能,用户搭建过程,我们通过技术手段提供智能对齐、检查规范、自动纠错、在线测试等能力,让用户更顺滑且规范地搭建出逻辑。
- DSL 转换器:将画布所有信息转换为json,提供给业务方
- 业务平台侧(活动搭建平台)逻辑的起点在主动触发逻辑&被动触发逻辑,一旦逻辑被触发,由Runtime提供的api进行注册元件,并开始执行,每一个元件的执行对应的数据产物会通过context与业务共享,一旦绑定字段对应的数据发生状态变动,会引起局部UI变动,模块整体状态切换。动作执行完流转到下一动作,如此直到逻辑结束。
这就是逻辑与UI结合的整体 / 核心思路,逻辑编排产生数据,UI编排监听数据变化,驱动试图改变。
六、总结
逻辑编排在以下三个方面做出了突破:
逻辑可复用:避免研发同学重复代码开发,节省人力成本,实现降本提效,也使需求变动成本降低;
分工更明确:实现了UI与逻辑解耦,技术在开发环节仅开发逻辑,将重开发工作,拆分为多角色轻工作的配合。且产品搭建逻辑的同时,也将需求可视化,便于他人理解;
业务可复用:通过对元件、画布、DSL、Rumtime的通用化、灵活性且可扩展地设计,使可适用于多业务场景。我们的规划是,以逻辑搭建为中心,可根据实际业务场景打造不同的产品形态,希望可以服务更多平台。
【三】元件与平台
一、前言
前一篇文章里讲解了逻辑与Runtime&DSL,也提到了逻辑编排三板斧:元件 + 编排器 + Runtime,我在本篇将主要聊一聊元件设计以及YOHO的平台化。
二、元件
元件在我们的设计中,分为基础元件和业务元件,业务元件就是我们需要创建仓库、编写源码、提交发布的的元件,它是逻辑片段的实体;除它以外的都是基础元件,我们在设计给好莱坞使用的编排方案中。基础元件只有开始元件和结束元件,只是为了降低非研发人员的使用成本,元件越少越好。
虽然分成了基础元件和业务元件,但业务元件实质上是一种特殊类型的基础元件,它们的关系类似于树和图,树也是图的一种,但书里常常会把书拆成单独的一个章节,因为树的体系足够大。业务元件也是,它就是基础元件中的 Custom 元件,但在元件体系中它的比重足够大,所以我们也将它抽出来作为一个大类来讲。
元件语言
业务元件的几个重要属性如下:
初始化业务元件后,我们定义的文件结构:
.
|-- schema
| |-- input.json
| |-- output.json
| `-- branch.json
|-- index.js
|-- package.json
- index.js: 元件的入口文件,源码就写在里面,发布时文件中的源码会被作为 code 字段进行存储
- package.json: 元件的 dependency 就是从里面读取,作为元件的依赖项进行存储,和 code 字段配合使用
- input.json: 配置元件编排时渲染属性表单的JSON Schema,作为元件的 input 字段进行存储
- output.json: 用于描述元件的返回值,同样为JSON Schema,作为元件的 output 字段进行存储
- branch.json: 描述元件有几个分支,以及每种值的中文定义是什么
我们对元件中分支判断的代码制定了规范,配合 branch.json ,将这种分支型业务元件的使用成本降到了最低,使用的复杂度转嫁为开发的约束性。拿《判断是否登录》元件举例,它有两个分支,已登录 与 未登录,代码进行分支判断是必须按照如下的格式书写:
function foo() {
if (isLogined) {
return { branch: 0, data: { key: value } }
} else {
return { branch: 1, data: { key: value } }
}
}
如果已登录,返回 branch 值为 0 ,如果未登录,返回 branch 值为 1。那么对应的 branch.json 必须如此定义:
['已登录', '未登录']
元件中返回的 branch 的值,会作为 branch.json 中的索引,元件中分支实际判断标准要和 branch.json 中的数组一一对应上,这样 runtime 才会按照正确的走向执行逻辑。
元件整体设计
基础工具侧,我们提供了模板初始化的能力、提供对应的元件开发脚手架,保障元件的基础开发能力,正在建设的 VSCode 插件用于提升研发体验,基于元件 OpenApi 提供的 元件 SDK 能够帮助元件录入元件市场,目前还只是服务本地研发,4月会进行线上元件开发的能力补齐。元件的表单配置项借助 form-render 实现了可视化的表单搭建,很好用,点赞。
基于底层设施,上层实现了统一的元件市场,会按照应用、标签等进行隔离,所有业务的元件在元件市场中收敛,方便以后统一落入优酷物料中心。
元件的一生
一个元件从被生产到被编排,最后在页面被运行,贯穿了逻辑编排的始末,也是“逻辑是核心”的论证,下面我们就以元件的视角来看看整个逻辑编排过程中它都经历了啥。
元件被发布后,进入元件市场,在编排界面,被拖拽生成元件实例,构建完整的业务逻辑。画布中,branch.json 被用于阐述各个分支,input.json 中的 json schema 通过 form-render 渲染为可配置的表单。编排实例创建完后,与UI编排进行关联,在UI搭建平台中创建的坑位,会需要元件执行后的返回值渲染UI,UI搭建平台会解析元件的 output.json ,将所有数据平铺展示。业务模块搭建完成后,跟随页面发布上线,runtime经过 启动 —> DSL 解析 —> 元件加载&注册 —> 元件检查 —> 元件执行 链路执行业务相关动作,在 元件执行 这一步,runtime 会进行 helper 和 payload 注入,提供帮助函数以及配置数据。
两个问题
这里有两个问题是我们落地过程中争论过的点,也希望各位看官提出自己的见解。
- 先是业务元件 还是 结束元件?
- 元件的 JSON Schema可以被修改吗?
三、前端特色的逻辑编排
集团内目前还是服务编排和业务编排比较多一点,也有服务端的逻辑编排,前端侧的逻辑编排却不多见。如果你对逻辑编排、业务编排、服务编排不是很清楚,推荐这篇文章 —— 《业务编排、服务编排、逻辑编排的区别》。在落地可视化搭建系统的过程中,结合搭建技术我们做了一点比较前端质的实现。
我们的模块搭建系统,每个模块都会生成对应的 CDN 资源,页面组装的时候,会将页面用到的模块资源在服务端进行拼接 —— JS Combo。在逻辑编排落地的过程中,我们一不希望入侵现有构建服务,增加开发适配工作,第二也不希望逻辑编排增大页面 JS Bundle。所以在 runtime 的 dslWillInterpret 生命周期里,我们进行逻辑元件的资源按需动态加载。元件都很小,一个完整的流程对应的 JS 资源只有几 KB,这一点加载时间暂可忽略。配合上对已加载元件 JS 资源进行缓存重复利用,需要加载的资源体积会进一步减少。
这个方案帮助我们解决了另一个问题,就是同一个业务模块中,我会创建多个编排实例,两个编排实例可能会在维护过程中出现使用同一个元件的不同版本,这种场景下,如果我们不进行按需动态加载,页面的 JS Bundle 可能会不可控地急速扩大。此时回头看,按需动态加载逻辑元件,帮助我们将页面的逻辑从 JS Bundle 中抽离了出去,替换成了简洁的 DSL,页面的 JS Bundle 也变小了,对于性能还有提升。
四、YOHO平台
YOHO平台的结构如下。
在图的最右侧,有我们对 YOHO 的定位,它是一个平台。我们不希望做完全中心化的平台,我们只想约束协议、规范,基于这套编排协议做统一的 node 服务,对外提供 OpenApi;在前端交互侧,不奢求一套交互可以满足所有业务,编排器的外框架只会基于编排协议实现各个板块的适配器,你可以选择用或者不用我们提供的面板或者画布,或者选择性的用,编排器同样会基于这套协议去实现自动检查、编排快照等能力。
求同存异是我们的选择。
五、未来规划
FBP,业务逻辑编排最后一公里
这就需要提到我们当初做YOHO平台,要服务营销搭建平台的愿景了。将业务模块从易到难进行排列,前面75%的模块将由逻辑编排来覆盖。剩下的25%包含了我们的玩法类的或者定制性极强的模块,它们共同特点的逻辑要求高,内部状态多,想支持,也能支持,但元件调试发布、编排调试发布显得极为麻烦,在我们的预期中,这部分复杂模块是由研发来承接的。
可视化搭建中逻辑编排的最后一公里还有解法吗?是可解的,基于现有能力去扩展 FBP 能力,让研发能够基于流来编程,在线编写 —> 在线测试 —> 流程纠错,你不仅可以更快的开发,而且你的代码会更加安全。
六、总结
逻辑编排的价值:
- 可视化:简化了业务逻辑的生产
- UI与逻辑解耦:提高业务模块的可维护性
- 逻辑复用:逻辑可以通过连接为多种不同的方式来产生不同的行为,会更好的代码复用能力
- 天然跨端、跨容器:因为是纯 JS 执行,天然适配各端、各容器内。它的更深远意义是,如果UI + 逻辑能够覆盖所有模块开发,对于rax0.6 升 1.0、Weex 切 Kraken这种场景,迁移成本可以降低 90+%
- 在线测试、自动纠错:让你的代码更安全
YOHO的价值:
- 统一大团队内的逻辑编排规范约束
- 便捷的工具/库便于逻辑编排在业务中落地
【四】编排通用性方向探索
一、导言
之前的文章讲过YOHO平台是因营销搭建平台的需求而诞生的,而后我们有了让它能够服务到更多平台的想法。三月底的时候,直播业务的同学找到我们,希望能够借助我们的平台实现编排。彼时的YOHO还不足以承接外来业务,所以开始了通用性的改造。
二、G6到X6
YOHO早前是基于G6开发的,逻辑编排对图形编辑的要求比较高,而G6更加侧重于图形展示、数据可视化,以及基于canvas的高性能。团队的小伙伴最初通过G6实现了一版编排,能力都没问题。后来想要不断优化画布能力的时候,就比较累心了。
无论是对齐还是连接线路径计算,都需要自己实现。对图形可以自由编辑也是有诉求的,这些都是开发量,需要人力与时间。X6则不同,它的强点就在图形编辑上,可以说图形操作的一切辅助能力你都不需要去关心,因为它都内置了能力支持。我们愿意花费一点时间从G6切到X6,因为他会为我们后期节省更多的时间。
另一方面,G6 绘制图形只能通过 G6 提供的 Shape 属性绘制或者类 JSX 语法来自定义节点,对于会有丰富图形的场景,让接入的同学去绘制,还是需要一定的成本的。而X6你可以通过 HTML 或者 React 或者 Vue 进行渲染,这个对于我们来说是更加熟悉的。
所以,从G6向X6的转换正是基于以下考虑:
- X6精于图形编辑,G6精于数据可视化
- X6比G6更加容易上手开发
- 编排场景对性能要求不高,X6可以满足需求
三、通用性
从G6到X6的转变只能保障在短时间内画布能力的完备,但是如何让一个画布能够更加通用呢?这就要说一下YOHO的定位了——让用户一站式接入编排。一站式是因为我们希望用户能够在线定制他们的编排业务,有这个想法是基于两点考虑,一是前端同学可以快速接入,二是非前端同学不了解前端开发也有能力接入。YOHO目前没有想去做一个能够所有场景的平台,它会有它的能力边界。比如当前我们只支持流程图的编排,业务方想要一个积木图编排,这就是我们的能力范围之外了。
滴滴前段时间开源的 Logic-Flow,在他们内部有一些中后台在使用。看了一下基于X6进行了二次设计,并没有能力的增强,API的设计与X6比,也只是做了一个他们自己的梳理,还增加了学习成本。观察他们放出的一些案例,很多平台之间的使用区别并不大,无非是图的样式不太一样。
所以如果我们把编排进行拆解,抽离一些API,进行可视化配置,是可以快速接入编排的。
将编排根据各个区域进行划分,元件面板可以根据不同的诉求,开发四种版型,让接入者选择;画布进行图、节点、边的拆解,具体在下方介绍;属性面板我们基于Form Render进行可视化表单搭建,对于非表单类型的,比如表达式编辑器这种,可以提供源码开发方式。
四、编排器
画布的绘制
我们来看看如果代码开发初始化一个画布,我们需要如何描述:
const graph = new Graph(
{
container: document.getElementById('container'),
width: 800,
height: 600,
// 节点是否可旋转
rotating: false,
// 节点是否可调整大小
resizing: true,
// 背景配置
background: {
color: '#f8f9fa',
},
// 网格配置
grid: {
visible: true,
},.........
}
);
实际上X6有很多的配置项,也包括通过函数去灵活配置一些画布的能力。这部分都是一些配置项而已,如果我们针对它们进行可视化的配置,画布的这部分问题就解决了。
节点的绘制
绘制一个节点:
const rect = new Shape.Rect({
x: 100,
y: 40,
width: 100,
height: 40,
attrs: {
body: {
fill: '#2ECC71', // 背景颜色
stroke: '#000', // 边框颜色
},
label: {
text: 'rect', // 文本
fill: '#333', // 文字颜色
fontSize: 13, // 文字大小
},
},
})
const rect = new Shape.Rect({
x: 100,
y: 40,
width: 100,
height: 40,
attrs: {
body: {
fill: '#2ECC71', // 背景颜色
stroke: '#000', // 边框颜色
},
label: {
text: 'rect', // 文本
fill: '#333', // 文字颜色
fontSize: 13, // 文字大小
},
},
})
基于提供的属性我们还能够绘制更加复杂一点的节点,但是有两个问题,一个是svg学习的成本,第二个是基于现有的属性有一些定制的节点是绘制不出来的,比如:
如果我们像开发react组件一样,来开发这个节点,就简单多了。
在YOHO中,节点开发的构成如下:
- shape
|-- index.tsx
|-- node.tsx
|-- thumb.tsx
节点可以分为两个部分,一个是左边的元件面板中的缩略展示,一个是画布中的展示,分别在 thumb.tsx 和 node.tsx 中完成,由 index.tsx 导出,同时在 index.tsx 中需要定义节点的 port(连接点) 。
基于此,我们将画布与图形进行了解耦,拓展出了一个概念,图形库。这些节点开发后都会入库,图形可以进行共享,如果你的业务需要某个图形,可以将图形引入你的业务,开发元件的时候和图形进行关联。如果你所需要的图形在图形库中都已经有了,可能你真的一点开发量都没有了。
边的绘制
边的绘制没有太多特殊情况,基本是基于现有的属性去做可视化配置就行了。
edge.attr({
line: {
sourceMarker: 'block',
targetMarker: {
name: 'ellipse',
rx: 10, // 椭圆箭头的 x 半径
ry: 6, // 椭圆箭头的 y 半径
},
},
})
表单通用性
编排除了流程的绘制以外,还有每个元件的属性的配置。属性我们都是用 JSON 来进行描述,那想要能够配置这些属性,就是要有能够可视化配置的表单,也就是 schema2Form。这一点,我们是基于集团开源的 Form Render 来做的。Form Render 同时提供了一个可视化搭建表单的工具 fr-generator ,方便我们拖拽生成表单。
还有一种场景,就是不是普通的表单,拿Mendix的表达式编辑器来说,这就不是个表单,我们应该怎么办?目前给出的解法是,属性面板是可以源码开发的。
我们可以选择两种模式
- 一种是JSON Schema,也就是基于FR的那种;
- 另一种就是源码开发,发布后产出CDN文件,在编排时选中元件,加载对应的Component CDN,将其渲染。
五、触发器
逻辑编排完成后,编排引擎会生成 Graph JSON,这是 YOHO 中通用的。一来每个业务会有自己的 Runtime,也就是逻辑解释器,自然也就有自己的 DSL ,Graph JSON 到 DSL 的转换器,自然每个业务不尽相同。二来业务方可能需要将数据落到自己的数据库中。那自然,YOHO就不会在自己的库里存储业务的 DSL 了。
YOHO 设计了触发器机制,业务在YOHO上可以填写自己的触发器,当用户在YOHO平台进行了对应的操作时,触发器就会触发,YOHO会将相应的数据传递过去。
六、总结
这篇文章是想分享一下最近关于编排平台通用性的一点思考与改动,其中有一部分已经实现了,有一部分还需要投入精力开发。对于平台未来的方向在开发过程中也一直在思考,一直在调整。如果各位看官有看前面的文章,可以发现,YOHO的设计思路已经有了很大的转变。就像是一个行业中的某一垂直领域,YOHO做的是编排这个方向上的某一模式。