Vision,是在线教育已经打磨了两年多的一站式通用运营平台,旨在帮助产品运营同学高效地完成活动页面的搭建、发布等工作的同时,也帮助业务开发效率和体验的不断提升。Vision 跨过自己的一个个里程碑,截止到今日,除了在线教育的三大业务,也已经支持了部门外业务 18 个。
在近一年的开发迭代中,Vision 平台也针对活动页运营链路完善了一系列功能,诸如便捷的 AB 实验能力,开箱即用的线上数据透视能力等等。
而另一方面,更接近搭建核心能力的升级,像水平布局、容器无限嵌套、组件间数据访问 等能力却迟迟没有动工。背后的原因在哪?
一. 问题是什么?
"我们似乎不太知道该从何下手增加这些能力 ",团队里负责一起维护 Vision 的小伙伴会发出这样的疑问。
作为一个运营提效工具,它提效的方式是提供给运营同学便捷地可视化地搭建落地页的能力,不难发现,Vision 的核心能力就是 可视化搭建。
然而,系统的设计是否能直观体现我们的核心能力?能力的边界又在哪?我们是否能清晰地辨析出新能力该在 Vision 的哪个子模块扩展,同时还保持着系统复杂度增长的平稳趋势?我心里也有着这些疑问,而答案几乎都是不明了的。
可以看到,我们的模块划分是一个常规 web 应用的方式,可视化搭建能力的核心并没有一个直观的形态,模块化的描述与可视化搭建领域的模型之间并没有建立合适的关联。
在这样的设计下,由于缺乏对领域的抽象和基础模块的设计,很多工作都变得困难起来。比如我们想增加渲染协议检查相关的逻辑,我们需要仔细地阅读大量现有系统的代码,才能找到一个相对合适的切入点,完成功能的实现。
更让人受阻的是,协议检查这个 涉及渲染流程 的能力落地,甚至需要同时发生在系统前端和后台的两个涉及渲染流程的模块中,编写类似的代码。这为系统代码的维护带来了额外的成本。
我们需要一种更理想的设计方式,将 Vision 的核心能力直观地描述出来,将 Vision 系统的软件复杂度的增长压缩在一个可控的速度,从而帮助我们更稳固、高效地进行后续系统的开发与维护。
来自团队另一类场景的诉求
另一方面,团队内 B 侧需求量日渐庞大,持续有相当比例的人力投入到管理后台类系统开发中。
这部分系统的重复度很高,都是重表单场景,很多时候就是各类表单的排列组合。同时大部分这类系统是给内部使用,对页面的质量、性能各方面要求相对 C 侧页面还是要稍低一些。这样的重复性与开发要求,持续投入在其中的同学未免会觉得自己的技术成长受限,管理后台页面的开发就好像成了一件苦差,谁都不乐意接。
我们注意到,公司内其实已经存在用于解决管理后台场景的低代码方案,比如 XPage,但是它目前仅在微信支付内部使用。
我们很自然地考虑,是否能扩展 Vision 的系统能力,让其也能够支撑 B 侧场景。但几经推敲和讨论后我们放弃了这个思路。我们认为,运营场景与 B 侧场景在搭建页面的形态与搭建平台使用用户上存在着诸多差异,如果我们期望在一个搭建平台上解决两类问题,必然存在着一些折中的设计,为了同时满足两类场景的需求。
最高效好用的工具一定是专注于某垂直领域提供深度解决方案的。因此,我们希望在上层建设另一个专注于管理后台系统搭建的平台,在新的平台上针对 B 侧场景提供最适合的产品设计;同时将 Vision 的核心搭建能力下沉,构建一个可视化搭建引擎,在其基础上,扩展更丰富的基础能力以支撑上层多样的应用场景。
Vision 遇到的问题,是软件工程的问题,是控制软件复杂度的问题;而来自团队的 B 侧需求,是产品能力复用的问题,是大框架下系统分层的问题。然而,它们似乎都指引着 Vision 往同一个目的地迈进......
二. 新的思路
我们开始将 Vision 系统打碎,开始思考可视化搭建核心能力的边界在哪,可视化搭建核心的抽象可以是怎样的,换句话说,从 Vision 下沉的可视化搭建引擎的具体形态如何。
Vision 平台的产品视图,活动搭建、编辑页面,包含了活动页面编辑、保存、历史管理、权限控制、预览发布等等能力。这其中不少是更适合作为上层平台能力的,比如历史管理、活动发布等。这些都不是我们的引擎想要包含的内容。
如果尝试用一句话来描述核心的搭建能力,那么首先,我们组织页面的方式是基于素材的拼装,然后页面搭建的结果,其实就是对页面组成素材的协议化描述,而页面的渲染,则是对这份描述的解释与执行。
我们将上面这句话做一些拆解和抽象:
- 我们需要提供对协议的解析与渲染的能力,我们把它叫做渲染器;
- 协议从哪来呢,我们需要提供可视化组装协议的能力,就叫做编辑器;
- 协议渲染过程所需对应的素材,那便是素材库;
渲染器、编辑器、素材库,三个相对独立的基础库组成了我们的可视化搭建引擎。 它将作为承载可视化搭建领域核心能力的微内核,成为 Vision 以及上层其它各类贴合业务场景的搭建平台的基石,Gems 应运而生。
除了大模块的拆分,每个模块内部也进行了相当的抽象和设计,下图是一个总览:
其中值得一提的是,编辑器中所见即所得的操作画布(模拟器)所依赖的渲染逻辑,我们统一收敛到了 Renderer,然后通过编辑器中的 EditableEnhancer 为渲染组件提供编辑上下文特有的交互能力。 这带来了两个好处。
- 降低了渲染相关逻辑改动的成本,即前面提到的涉及渲染逻辑的改动需要发生在多处的问题不复存在;
- 减少了模拟器预览效果与最终发布效果的差异,因为渲染代码就是同一份。
其它子模块的设计细节这里暂时不展开,后续可能进行系列文章总结,有兴趣的同学可以保持关注。
三. 新架构下的能力扩展
在新架构的基础上,我们如愿进行了众多核心能力的扩展。这里从几个具体的场景切入,来看一看 Gems 具体能力项的设计与实现。
栅格布局
这里说的栅格布局,主要就是为了解决水平排列素材的场景,该场景在 PC 端页面中较为常见。
原有的能力中,我们拥有普通的容器能力,容器里面可以放子组件。而栅格布局实现的方式则是提供一个栅格布局容器。
可以看到,栅格容器看起来就是比普通容器多了几个“坑”,在添加组件时可以选择往哪个坑添加。那么,结合我们的整体架构进行需求分析:
- Renderer 侧,从渲染协议开始看,本质上,栅格容器只需要 一个额外的信息来描述子组件属于哪一个“坑” ;
- Materials 侧,从素材定义看,栅格容器则比普通容器需要多定义一个信息,那就是 容器需要有多少“坑”。
其实分析到这,围绕这个“坑”的设计,似乎已经不局限于栅格容器了。所有需要有多个“坑”的容器组件,都可以拥有类似的抽象,像 Tab 组件、Table 组件、模态窗等等。
我们进行了如下的抽象和设计:
- “坑” => 插槽 => slot;
- 一个额外的信息来描述子组件属于哪一个“坑” => 渲染协议扩展,组件描述中增加 slot 字段;
- 容器需要有多少“坑” => 组件定义中增加 slots() 配置项;
到这,我们已经具备了渲染一个多插槽容器的基本条件,剩下的问题来到 Editor,如何为这种携带插槽的容器赋予编辑态特有的一些 UI 和交互动作了,也就是让容器里的插槽可以被选中,指定往某个插槽添加子组件等等。
得益于我们 Editor 与 Renderer 的架构设计,我们只需要在 EditableEnhancer 中扩展处理插槽的能力即可,具体的实现形式即为一个高阶组件,大致逻辑如下:
- 将拥有插槽的容器的子组件按 slot 分组;
- 为分好组的子组件包裹插槽组件,为插槽提供相应的 UI 和交互。
const withSlots = (View) => {
return function WithSlots({ instance, children, ...otherProps }) {
const { slots } = instance;
return (
<View instance={instance} {...otherProps} >
{
slots(instance).map((slot) => (
<SlotContainer>
{children.filter((child) => slot === child.props.slot)}
</SlotContainer>
))
}
</View>
);
};
};
至此我们已经具备了提供栅格布局能力的所有关键要素。
素材数据联动
素材间的联动,其实 Vision 原有的设计里已经支持了,主要是借助事件系统实现的一套联动机制。这套联动模式在由明确 UI(事件) 驱动的场景下是很适合的,我们以点击按钮打开弹窗这个场景为例,简单回顾下 Vision 对此的描述方式:
视角转换到我们新面临的B侧场景,我们试一试使用这套模型对常见的表单场景进行描述。
我们假设有一个 Input 提供给用户输入,然后有另外一个 Table 依赖这个 Input 的值进行数据拉取,以及另一个 Text 需要展示 Input 的值。
那么按上述联动动作的思路来完成,我们需要以下几步:
- 首先在 Input 组件上提供一种自定义事件触发,比如 输入变化,同时携带上改动的值;
- 在 Table 上实现自定义动作(事件监听),比如 更新参数,接收新的参数值并更新组件内部维护的数据;
- 类似 2,在 Text 上实现自定义动作,比如 更新文本,接收新的文本内容并重新渲染;
- 在 Input 上配置 输入变化 分别触发 Table 和 Text 的 更新参数、更新文本 动作。
模型已经有些复杂了。这里的核心逻辑,Input 其实是作为一个数据的生产者,对应的, Table 和 Text 作为其数据的消费者。可以想象得到,我们需要为每一个拥有数据生产能力的素材,围绕其 UI 行为,实现类似上述步骤 1 的自定义事件触发,将数据发射出来;同时,为每一个拥有数据消费能力的素材(其实可以说是所有素材)实现类似上述步骤 2/3 的自定义动作。而且,当数据消费关系复杂时,上述步骤 4 的搭建配置,成本也是不容小觑的。
显然,联动动作设计背后事件驱动的方式在这个场景下变得不那么适用。我们需要一种数据驱动的模式,我们需要一种组件间能进行数据共享的能力。
我们为素材定义了新的接口能力 ——— 开放数据接口。简单的说,开放数据接口就是一个素材声明的可以被外部(其它素材)访问引用的数据接口声明。 区别于素材自身的静态数据,该开放数据是一个运行时的动态数据,并不固化于渲染协议中,数据也并不是由页面编辑者在搭建页面时产生。
如上图,我们实现上面的表单场景就只需要简单的两步:
- 在 Input 定义可供其它素材访问的数据项;
- 页面搭建时,在需要消费 Input 数据的地方(此例为 Table 和 Text 的数据配置)通过 Input 的组件 key(实例 id)进行数据引用。
整个具体的实现将由三个核心模块的密切配合共同完成:
- Materials 提供数据接口的定义能力;
- Editor 提供快捷配置数据变量及相关提示的组件配置表单能力;
- Renderer 提供运行时数据对象的创建、数据分发及变量解析能力。
每一个素材(包括组件、动作、插件)都可以定义自己拥有的可被其它素材访问的数据,同时素材数据的更新将会触发依赖侧的更新。最终的效果如下:
组件模板
在某一些场景下,我们希望一个组件被添加到画布中时能携带一些默认的样式或行为,比如模态窗组件。
首先我们希望它能默认拥有比较常见的 Footer(取消、确定按钮),但我们同样也希望模态窗的 Footer 部分可以相对自由的被定制,那么将 Footer 固化在模态窗实现内不会是一个好的方式。
我们考虑将 Footer 部分作为模态窗的插槽之一,可以自由地置入子组件。所以现在就回到了最初的命题,我们需要一种能力,能够描述组件进入画布时的默认表现。
可以分析得到,这个能力是 Materials 和 Editor 需要关注的。Materials 提供素材默认表现的定义,而 Editor 则负责解析执行该定义,并赋予素材进入画布时以相应的行为。
我们在 Materials 侧为每个组件提供了一种定义 templates 的能力,如下图:
针对模态窗的场景,通过 templates 配置,为其定义了两种模板,其中 弹窗(带默认 Footer) 的模板则自动携带了的“确认”、“取消”按钮。Editor 拿到这份组件配置时,则会在组件面板中给出相应的模板以供搭建用户选择。
组件的颗粒度与搭建灵活程度几乎是正相关的。组件颗粒度越细,那么搭建时可自由组合来支撑的页面多样性就越大。但与之而来的,是搭建成本的提高。反之组件粒度越粗,搭起来越简单,而灵活性又受限。
组件模板能力的设计,有效 缓解了搭建效率与搭建灵活性之间的矛盾 ,在维持细粒度、高灵活性的原子组件的同时,通过预设模板对原子组件进行组合的方式,为高频搭建场景提高了不少搭建效率。
四. 成果及未来展望
从 Vision 中诞生出的 Gems,现在已经成功替代了 Vision 原有的核心逻辑代码,也为 Vision 带来了新的栅格布局、数据联动等新能力。更重要的是,Gems 让 Vision 的软件复杂度得到了控制,为系统的开发维护工作带来了指引,让一切系统的变更更加有迹可循。
另一方面,基于 Gems,团队内 专注于管理后台类页面搭建的 Hulk 系统也成功问世并投入实际生产之中。截止笔者发文,团队内已经在开心鼠排课系统、课堂章鱼哥、课堂人社系统等多个管理系统的页面使用 Hulk 搭建,得到一众好评,节省了大量开发人力。
目前 Gems 仍在高速迭代与实践验证中,后续将开放给团队外使用,并逐步开源。届时欢迎大家前来使用、给予宝贵的意见!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!