自 2020 年来,网易数帆探索可视化低代码编程已两年有余,打造了轻舟低代码平台用于企业应用开发。然而,不少编程技术人员对这一领域还比较陌生。我们开设《低代码技术内幕》专栏,旨在讨论低代码编程领域中的困难、问题,以及高效的解决方案。本文为第二篇,结合我们的产品研发经验解读打造 web 端可视化代码编辑器需要权衡的因素以及技术实现的要点。 专栏内容回顾:低代码编程及其市场机遇剖析 | 低代码技术内幕
轻舟低代码平台是一款基于云服务的 web 端产品,面向零基础或者有一定编程基础的用户。用户不需要额外安装软件,就可以在任何有浏览器的电脑上编写和发布应用。可视化代码编辑器 是轻舟低代码平台的重要组成部分,用户通过可视化界面开发应用。然而,在 web 端构建一个拥有良好体验的可视化编程工具是一个很大的挑战:良好的体验要求良好的视觉效果、交互、性能;而视觉效果越花哨,交互越复杂,性能也就越低,过低的性能(卡顿)会影响体验,但如果单纯为了性能,减配视觉效果和交互,又会显得简陋,所以可视化代码编辑器需要掌握好这个平衡。
为了完成这个挑战,我们在 canvas 上模拟实现了浏览器的事件、容器、布局等功能。另外,为了兼顾团队本身的技术栈(Vue)和项目的可维护性,我们最终使用了与 Vue 框架结合,通过 Vue 模板来控制 canvas 渲染的方案。
下面我们从渲染、交互、数据与视图三个方面来介绍。其中渲染部分主要考虑了性能问题,交互部分介绍了如何模拟浏览器的事件机制,数据与视图部分说明了如何与支持双向绑定特性的 Vue 框架结合。
高性能渲染
低代码可视化编辑器保留了控制流的设计,所以在整体结构上类似于传统的 流程图。但其与流程图有两个明显的区别:
for
、条件语句 if
等,这些语句内部又包含很多条表达式(expression),如算数运算、函数调用等;表达式可以嵌套组合,因此每个语句结点可以异常复杂。为了处理好这些问题,我们调研了主流的渲染方式。目前浏览器提供的主流渲染方式有以下三种:
HTML + SVG + CSS 渲染
2D Canvas 渲染
在考虑到团队技术栈和开发周期的同时,为追求更好的用户体验,我们选择了使用 2D canvas API,并 实现了一个自研的框架 JFlow。JFlow 框架通过模拟浏览器的事件系统以及布局系统,可以无缝嵌入到我们的 Vue 工程下,让我们的前端工程师能够快速迭代业务需求。
细粒度交互
为了支持表达式级别的细粒度交互,我们在 JFlow 框架中实现了一整套可交互的基础设施,我们的交互设计是基于浏览器内部基本的事件,没有超出浏览器给出的事件范围。HTML 中 <canvas>
的事件都是从顶层触发的,如果要在画布内部,针对每个绘图单元来实现交互,得从 <canvas>
上的原始事件出发,通过构建画布内部的多级坐标系统,定位到具体的绘图单元(捕获),结合浏览器事件本身和定位到的绘图单元层级,再模拟事件抛出(冒泡)的过程,在画布内模拟出浏览器的原生事件系统,通过结合前端事件基础知识,从而实现业务中的交互设计。下面我们从定位、状态、事件三点来介绍:
在什么定位下、在什么状态下、在什么浏览器事件下才能触发什么交互?交互是单一确定的,还是个像事件列表那样有优先级?会冒泡到父对象的交互中处理吗?
节点的位置由具体的节点布局算法来确定,算法基于内部对象坐标系 B 来计算。
坐标系 B 上的节点及内部节点均设计为中心对称,即节点在其父级坐标系的坐标为图形中心,这种设计方便设置行列对齐。节点内部的子坐标系以图形中心 (cx, cy) 为原点,若子坐标为 (x, y),父坐标为 (x', y'),则内部的父子坐标系存在如下关系:
节点内部绘图单元的位置,由节点上具体布局算法来确定,算法基于父级坐标系来计算。
通过构建这些基础设施,我们模拟了事件机制并获得了对内部绘制对象操作的方法,但是 JFlow 作为语言的基础渲染框架,还需要增强对抽象语法树(abstract syntax trees,AST)的互操作能力,见下一小节。
数据与视图
我们上面的设计相当于在 canvas 上加了 DOM 的部分能力,使其能够更方便地接收事件和操作内部绘图对象。但是直接操作 DOM 的缺点在于视图操作和业务逻辑会耦合在了一起,在我们的场景下就是在操作 AST 的同时,还要同时操作渲染对象。为了解耦,以及减少业务代码出错造成的渲染问题,我们需要引入 MVC 或者 MVVM 之类的框架。
我们选择了与现有的一些 MVVM 的框架结合来解决这个问题。但是在结合之前,我们还需要补齐一些能力。
vue plugin
提供了响应数据变化渲染的能力,内部通过调用 scheduleRender
方法,在浏览器每次重绘之前触发一次性的渲染。现在我们可以引入 MVVM 框架了:
数据流向
数据流向
虚线左侧是 AST 的使用部分,右侧是框架控制的部分,原始数据决定了布局算法,布局算法控制渲染节点的位置,原始数据节点经过布局节点对应的模板,来控制渲染节点,框架再通过事件通知原始数据或模板更新,进而更新渲染节点。
vue数据流向
上图更具体说明了整体数据流向,Vue 起到了 MVVM 的作用。原始数据更新布局,相当于浏览器里面的重排。
一个例子
假设在某种语言里有
class A extends B mixin C, D {}class B implements E {}
class C {}class D {}interface E {}
这样的代码,我们将用 jFlow 来实现如下的展示效果:
代码展示
首先,我们构建描述 A、B、C、D、E 这几个类型之间的关系的 JSON:
[ { "name": "A" , "extends": "B", "mixins": ["C", "D"] }, { "name": "B" , "implements": "E" }, { "name": "C" }, { "name": "D" }, { "name": "E" }]
然后,我们根据视图和数据的关系来构建总布局。
总布局由布局节点和布局组成:
// DemoLayout.jsclass VirtualNode { constructor(source) { this.type = 'VirtualNode'; this.source = source; }
makeLink(callback) { const { extends: ext, mixins, implements: impl } = this.source; if(ext) { callback({ from: ext, to: this.source.name, part: 'extends', }) }
if(mixins) { mixins.forEach(t => { callback({ from: t, to: this.source.name, part: 'mixins', fontSize: '24px', lineDash: [5, 2] }) }) } if(impl) { callback({ from: impl, to: this.source.name, part: 'implements', }) } }}
class DemoLayout { constructor(source) { this.static = false; // 布局实例必须包含节点 (flowStack) 和边界 (flowLinkStack) this.flowStack = []; this.flowLinkStack = []; const nodeMap = {}; const nodes = source.map(s => { const node = new VirtualNode(s); nodeMap[s.name] = node return node; }); nodes.forEach(node => { this.flowStack.push({ type: node.type, source: node.source, layoutNode: node, }) node.makeLink((configs) => { const fromNode = nodeMap[configs.from]; const toNode = nodeMap[configs.to]; if(!fromNode) return; if(!toNode) return;
this.flowLinkStack.push({ ...configs, from: fromNode, to: toNode }) }) }); this.erNodes = nodes; }
reflow(jflow) { const nodes = this.erNodes; nodes.forEach((node, idx) => { // 计算 节点位置 const renderNode = jflow.getRenderNodeBySource(node.source) renderNode.anchor = [-idx * 220, (idx % 2) * 80]; }); }
}
export default DemoLayout;
接着,在全局引入 vue
插件
import Vue from 'vue'import { JFlowVuePlugin } from '@joskii/jflow';import App from './App.vue'
Vue.config.productionTip = falseVue.use(JFlowVuePlugin);new Vue({ render: h => h(App),}).$mount('#app')
其中导入的 App.vue
代码如下:
<template> <j-jflow style="width: 600px; height: 300px; border: 1px solid #000" :genVueComponentKey="genVueComponentKey" :configs="configs"> <template #VirtualNode="{ source }"> <virtual-node :node="source" ></virtual-node> </template> <template #plainlink="{ configs }"> <jBezierLink :configs="{ ...configs, content: configs.part, backgroundColor: '#EB6864', fontSize: '24px' }" :from="configs.from.source" :to="configs.to.source"> </jBezierLink> </template> </j-jflow></template><script>import { commonEventAdapter } from '@joskii/jflow';import DemoLayout from './demo-layout';import VirtualNode from './virtual-node.vue';import source from './data.json'const layout = new DemoLayout(source);export default { components: { VirtualNode, }, data() { return { configs: { allowDrop: false, layout, eventAdapter: commonEventAdapter } } }, methods: { genVueComponentKey(source){ return source.name; } }};</script>
其中引入的 virtual-node.vue
代码如下:
<template> <j-group :source="node" @click="onClick" :configs="configs"> <j-text :configs="{ textColor: this.coin ? '#60CFC4' : '#EB6864', content: node.name, }"> </j-text> </j-group></template><script>import { LinearLayout } from '@joskii/jflow';const layout = new LinearLayout({ direction: 'vertical', gap: 0,});export default { props: { node: Object, }, data() { return { configs: { layout, borderRadius: 8, borderColor: '#EB6864', borderWidth: 2, padding: 20, }, coin: false, } }, methods: { onClick() { this.coin = !this.coin; } }}</script>
运行这些代码,即可得到本小节开头的效果展示图。
JFlow 在低代码平台中的效果
我们用同一个 JFlow 框架,搭建了轻舟低代码平台的可视化编程引擎、流程图引擎、实体 - 联系图(ER 图)引擎,下面我们通过一组截图,分别展示其效果。
可视化编程:
逻辑
流程图:
流程
实体 - 关系图:
实体关系图
结 论
我们在框架设计实现中一直在寻求用户体验、可拓展性、性能、学习梯度、团队合作之间的平衡。这些框架设计可能跟具体特性不太一样,只有在缺失的时候才会被注意到,但是这些设计为我们的团队和产品带来了深远的影响和改变。
作为框架的设计和实现者,我们很开心轻舟低代码可视化编程能够为零基础或者有一定基础的开发者,带来良好的编程体验。未来,我们会继续探索可视化编程的可能性。
作者简介:
网易数帆编程语言实验室负责轻舟低代码平台核心编程能力的设计,包括类型系统、语义语法、声明式编程、可视化交互等 NASL 的语言设计,Language Server、可视化引擎等,以及后续演进方案的规划和预研,旨在创造低门槛高上限的低代码开发体验。
今日好文推荐
警方通报网传中电科加班事件调查结果;拼多多解散恶意功能团队;逼死程序员诈骗千万的“翟欣欣案”一审宣判 | Q资讯