逻辑可视化,人人能编程?

2,764 阅读14分钟

从可视化搭建说起

页面可视化搭建系统从16年开始如雨后春笋般涌现而出,从活动页搭建到中后台搭建,有开源有仅公司内部使用的,都致力于将前端从繁复的体力劳动中解脱出来,提高页面生产效率。优酷内部也有一套营销活动搭建系统,每年生产2K+活动页;能够满足这么多页面的需求,除了沉淀了大量可复用的组件外,围绕着搭建系统的前端研发每天都在不停地维护升级老的组件,同时生产新的组件。

痛点

页面生产能力上去了,研发还是一直埋头在组件开发需求中。这些需要都是从哪里来的呢?其实上面也有提到,就是两点:

  • 老的组件需要添加新的能力,可能是UI改动,可能是逻辑变更
  • 新的业务组件开发

需求是永远也做不完的,为了提高开发效率,研发侧不断地沉淀通用的基础库,与服务端商定标准化的接口,以此来减少维护成本,但基于现有模式锦上添花的优化远远不够。说白了,现有的可视化搭建效率和研发效率都已经达到瓶颈了,我们急需一种新的生产模式,给我们带来生产效率的突破性提升。

解法

我们思考一下,页面可视化搭建是如何解放生产效率的。它将完整的页面进行拆解,拆分为可以复用的组件,研发负责组件生产,产品运营负责组件配置,形成了一种简单的流水线作业的模式,这种模式的好处在于:

  • 业务组件复用,避免重复开发,研发只需要专注于单个组件
  • 产品运营可以介入页面生产过程,减少沟通损耗的同时分担了部分先前研发承担的工作

现在的瓶颈已从页面开发效率转变到组件开发效率上,我们做一个设想,让非研发角色也能介入到组件生产过程中,进一步提高生产效率。在此之前,我们需要先基于现有模式进一步拆分组件结构:

module-devide

组件可以拆解为 UI + 逻辑。UI 可以通过细粒度的文字、图片、slider等组件搭建出来;逻辑主要涉及到接口请求、数据处理、能力调用,我们将一些常用的api调用比如跳转、常用的接口请求比如查询登录态等进行封装沉淀,加上语义化的描述,非研发人员可以将他们拖拽绘制成流程图,完成业务逻辑的编排。两者结合,就可以生产出完整的业务组件。而对于业务逻辑的组合编排,我们称之为逻辑编排。

举个栗子

我现在有个需求,需要实现一个简单的抽奖活动。页面分为两块,第一块是奖池,五个奖品横铺开,第二块是抽奖按钮,点击按钮,如果用户没登录,拉起登录面板,如果用户已登录,则进行抽奖并高亮展示对应的奖品。

我现在把UI撘出来了,就差逻辑来让对应的坑位展示了,我们把欠缺的逻辑部分拆成两段:

  • 进入页面,也就是 componentDidMount 阶段,查询奖池
  • 点击抽奖按钮时,查询是否登录,未登录拉起登录面板,已登录则调用抽奖接口

logic-example

除了开始和结束以外的这种逻辑片段都是我们已经沉淀下来的,可以直接拖拽进来,绘制流程图。绘制完了后,我们选择模块的 页面主动触发(设计给非研发用的,他们肯定不理解didMount),将它和 查询奖品 关联上;选择抽奖按钮的 点击事件,关联上 抽奖 逻辑,这样模块就能在不同的时机做正确的事情了。可能你还有点小疑惑,我查询了奖品后,数据是如何映射到UI展示上的呢?要想知道如何映射,你得先知道逻辑是怎么运行的。

下图是我们在寒假战役中的一个应用场景:

demo

Logic is Core

扯一点远的,不愿意看可以直接跳到“逻辑如何运行”小节。前端的逻辑编排相比服务编排要更复杂一点,并不是说前端逻辑编排更难,而是因为:场景更加多样化,C端模块搭建和中后台搭建的编排差异就很大;人员更加多样化,除了服务研发人员还要服务非研发人员;除此以外,还需要和UI进行结合,可能性就更多了。

这段时间比较火的 iMove ,初始被设计服务优酷活动搭建平台的逻辑编排,后面服务imgcook后,与优酷的逻辑编排在形态上已经截然不同,所以在前端比较难像服务编排一样,平台化、中心化然后去服务多种多样的业务。如果你真的很想去服务多样化的场景及业务,提供一个轻量级的库,让它足够灵活、可定制,这也是 iMove 正在走的路。

前面讲了很多,都是在说,逻辑编排需要follow不同的平台做对应的定制,但是不管形态怎么变,它的核心是不会变的,我们一定是在围绕着 “逻辑” 去做包装,将之打造为不同的产品形态。所以,逻辑才是核心!

逻辑如何运行

说了半天逻辑多重要,假使我现在抽离了一个函数出来,把它发布了,我又用它拖了个流程图,它怎么才能执行呢?

逻辑最后想要运行,无论你是出码(to code) 还是在线执行,你都需要一个“逻辑编排解释器”,这个解释器可以读懂编排后的逻辑并且去执行逻辑,这个解释器我们称之为 Runtime。代码是冰冷的,解释器是没有智商的,它并不能真的读懂逻辑,只是因为我们约定了一个规则,定好了编排后的逻辑可能有几种情况,每种情况应该如何做,Runtime 只是在依章办事,而这个规则就是 DSL 。所以逻辑想要运行,它依赖于 DSL + Runtime ,要做一个逻辑编排平台,也一定是先定好 DSL,实现 runtime ,后面才是去做平台。

runtime-graph

每个沉淀下来的逻辑,我们将其定义为逻辑元件,加上语义化的元件名称及详细的描述,然后发布到元件市场。使用者只需要根据元件名称来挑选需要的逻辑,通过连接线将他们连接起来,就可以组合成一个流程图,这个流程图也就是一段完整的逻辑。

可是流程图导出来的 graph json 都是点和线的集合:

graph-json

这种 json 如果直接用 runtime 执行有两个问题:

  • 流程走向不直观,每个元件执行完后需要浪费时间去查找下一个元件是哪个
  • 无效信息太多导致json体积太大进而影响加载性能,比如坐标信息(x/y)、标签信息(label) ...

所以我们需要先去约定 DSL ,保证足够直观且只包含必要信息,所以我们设计了一个转换器将 graph json 转换为 DSL 。每个逻辑流程图对应一个 DSL 产物,借助 runtime 的 interpret 方法,它可能会运行在 useEffect 中,也有可能运行在某个元素的点击事件中,或者是页面的滚动事件中。

how-to-work

逻辑元件生产与消费的分工

逻辑元件从被编排到被运行的过程,也是它被消费的过程。文章到这里,相信大家可以感受到这个消费的过程,非研发同学确实是可以进入的,因为我们设计的足够简单。我们把整个页面的生产当做一条流水线的话,以前是 模块生产 --> 模块市场 --> 页面搭建,有了逻辑编排后,我们的流水线比之前划分的更细了。

flow-line

这张图是产品进入到中期的一个形态,前期的时候产品的角色更多是由研发来承担的,然后逐渐过渡到产品,到了后期呢,产研联合给运营同学做培训,中间这部分会逐渐得过渡到运营同学那边。

因为整条线上大家需要关注的事情越来越细了,页面生产线出错的几率也会低一些。

逻辑编排

上面一直在从业务角度去聊,接下来就要深入到逻辑编排里去了,讲一讲逻辑编排的设计思想。逻辑编排最主要是分为三块,元件、编排器、runtime,我将它们称作逻辑编排三板斧。那问题就转换成了:

  • 元件该怎么做?
  • 编排器该怎么做?
  • runtime该怎么做?

这三块各自拆解出基本要素,用基本要素来描述它们,互相之间建立起连接关系,这些构成了逻辑编排的规范协议。这样不管什么业务进来,只要遵循这套规范,它们的底层就是一致的,各业务也不会显得散乱。

specification

DSL

在探讨我们的 DSL 之前,一定要先聊一聊DAG(有向无环图),因为我们的 DSL 设计本质上就是 DAG 。

DAG

DAG 的 G 指 Graph(图),图是数据结构中最为复杂的一种,我们在了解 DAG 之前,回顾图的几个要点:

  1. 顶点(vertices):图中的一个点
  2. 边(edge): 连接两个顶点的线段
  3. 度数(degree): 从一个顶点出发有几条边,这个顶点的度数就是几

图就是由一些顶点和边组成,边就是顶点间的关联关系。DAG 中的 D 是 Directed,代表是有方向的,就是说顶点之间的边带箭头,常见的比如食物链,就是有向的;A 是 Acyclic,代表无环,从某个顶点出发,无论走那条路,都不会回到那个顶点。

DAG

基于有向无环图来约定我们的 DSL 正合适不过:

  • 我们需要有向边来告诉我们逻辑的走向
  • 流程从开始节点出发一定要遇到结束节点才结束
  • 我们的逻辑元件可能有多个出口,就像顶点可能会有好几个度,比如说判断是否登录,就算是2度
  • 逻辑编排中我们没法要求流程图一定绘制成树(一个顶点到另一个顶点,只有一条路径)那样

基于DAG的DSL

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 中间架一座桥。

runtime-ui

将逻辑内部生成的数据全部存储在内部 context 中,利用发布订阅模式,每当 context 有更新时,通知 UI 组件执行 setState,state 更新后,React 自动更新 UI。

runtime.subscribe('context', (val) => {
  // handle data

  this.setState({ data });
})

轻量的Runtime

YOHO 的 runtime 很小,不到 10k,麻雀虽小,五脏俱全,接下里我们看看这小麻雀是如何支撑起流程编排的最后一公里 - “执行逻辑” ,又是如何和业务打交道的😊。

runtime

逻辑元件本质上就是基于一段代码,给它加了一些描述信息,可是我的页面中是不会内置这一段段代码的,runtime 也不可能内置这些代码,也就是说我们的执行上下文中是没有元件对应的函数的;我们的业务是 No Code 形式,不出码,所以 Runtime 内部实现了一个元件管理器,你可以通过他进行元件的注册,在 DSL 执行过程中,它也会帮你进行元件检查。

Runtime 对每一次编排实例(流程)的调用都是在沙箱中进行,实例的执行是互不干扰的,而执行过程中每个元件输出的结果我们也做了隔离;其实我们还给元件之间互相通信提供了方法,但是目前不建议使用,因为元件之间随意通信有很大的副作用,我们目前还没有足够的产品形态去约束它。

在 runtime 设计过程中,我们预见业务会不断地有各种需求进来,可是我们不希望 runtime 过于业务定制化,导致将来积重难返,所以设计了生命周期。在编排实例执行的各个阶段,业务都可以进行干预。

使用发布订阅模式,而不是观察者模式的原因,也是我们希望 runtime 足够灵活。举例来说,上下文因为数据隔离,我们把它拆解为各个子上下文,你可以把每一个 childContext 当做一个 topic 来订阅,其他 childContext 更新并不会附带影响当前的。 childContext 。

除了以上,runtime 只做了一个事情 —— 逻辑走向的调度,就是有向无环图中的 “有向”。正是基于以上的设计,我们的 runtime 足够小,但是又足够灵活,便于定制。

三板斧在本文中就先讲这第一斧😄

后话

在做逻辑编排平台的过程中,方案做过好几版——从一开始两周搞出 iMove 初版给老板去展示,然后想要拥抱集团,和优秀的编排平台 Logic Force 想着共建前端编排,因为我们最终的产品形态差异过大,LF想要支持也需要投入很大的精力,同时我们的业务想要快速验证,所以又重新自研了一个轻量的逻辑编排平台。期间有很多感悟,我们也都在逻辑编排的设计中添加进去了。

逻辑编排讲到这里还没有结束,我们后面还会有系列文章,元件和编排器是如何设计的呀,和UI进行联动,UI侧又是怎么设计的呀,我们在逻辑编排方面又做了哪些前端特色的东西呀?如果你也感到好奇,敬请关注~