NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

基于 Vue 和 Canvas,轻舟低代码 Web 端可视化编辑器设计解析 | 低代码技术内幕

  • 2023-04-10
    北京
  • 本文字数:6351 字

    阅读完需:约 21 分钟

基于 Vue 和 Canvas,轻舟低代码 Web 端可视化编辑器设计解析 | 低代码技术内幕

自 2020 年来,网易数帆探索可视化低代码编程已两年有余,打造了轻舟低代码平台用于企业应用开发。然而,不少编程技术人员对这一领域还比较陌生。我们开设《低代码技术内幕》专栏,旨在讨论低代码编程领域中的困难、问题,以及高效的解决方案。本文为第二篇,结合我们的产品研发经验解读打造 Web 端可视化代码编辑器需要权衡的因素以及技术实现的要点。


专栏第一篇:低代码编程及其市场机遇剖析 | 低代码技术内幕  


轻舟低代码平台是一款基于云服务的 web 端产品,面向零基础或者有一定编程基础的用户。用户不需要额外安装软件,就可以在任何有浏览器的电脑上编写和发布应用。可视化代码编辑器是轻舟低代码平台的重要组成部分,用户通过可视化界面开发应用。然而,在 web 端构建一个拥有良好体验的可视化编程工具是一个很大的挑战:良好的体验要求良好的视觉效果、交互、性能;而视觉效果越花哨,交互越复杂,性能也就越低,过低的性能(卡顿)会影响体验,但如果单纯为了性能,减配视觉效果和交互,又会显得简陋,所以可视化代码编辑器需要掌握好这个平衡。


为了完成这个挑战,我们在 canvas 上模拟实现了浏览器的事件、容器、布局等功能。另外,为了兼顾团队本身的技术栈(Vue)和项目的可维护性,我们最终使用了与 Vue 框架结合,通过 Vue 模板来控制 canvas 渲染的方案。


下面我们从渲染、交互、数据与视图三个方面来介绍。其中渲染部分主要考虑了性能问题,交互部分介绍了如何模拟浏览器的事件机制,数据与视图部分说明了如何与支持双向绑定特性的 Vue 框架结合。

高性能渲染


低代码可视化编辑器保留了控制流的设计,所以在整体结构上类似于传统的流程图。但其与流程图有两个明显的区别:


  1. 流程图的节点相对简单且布局自由,而轻舟低代码的可视化代码编辑器的节点多且复杂(超一个量级)且布局严格。这种复杂性来自于编程语言本身:我们的低代码编辑器的结点相当于编程语言中的语句(statement),如循环语句 for、条件语句 if 等,这些语句内部又包含很多条表达式(expression),如算数运算、函数调用等;表达式可以嵌套组合,因此每个语句结点可以异常复杂。

  2. 流程图的交互操作简单,而可视化代码编辑器的交互复杂。我们提供给用户的交互粒度是表达式级别的,它包括了鼠标悬停、点击、拖拽和键盘输入等,我们需要处理好语句中嵌套的、表达式中嵌套的每一个表达式,这些交互的复杂性都是流程图所不具有的。


为了处理好这些问题,我们调研了主流的渲染方式。目前浏览器提供的主流渲染方式有以下三种:


  1. HTML + SVG + CSS 这是一种成熟的渲染方式,它提供的 API 中包含事件以及对于内部绘制对象操作的方法。但这种渲染方式有很多历史包袱,要保持不同浏览器的渲染一致性比较困难。其次由于它要使用 DOM 来操作节点,会比下面提到的 2D canvas 要慢。并且这种方式只适合于布局相对稳定的整体交互,因为布局变化会触发 DOM 重排,而频繁的 DOM 重排会成为性能瓶颈。另外这种方式在高分辨率的屏幕上有时无法做到抗锯齿,渲染效果无法保证。


  1. 2D Canvas 2D canvas 是一种高性能的相对高度抽象的立即绘制模式的 API,其渲染依赖于一个 Frame 内执行的指令。但是 API 中并不包含事件以及对于内部绘制对象操作的方法,需要额外设计框架实现。

  1. WebGL WebGL 是一种更为底层的渲染技术,它通过基于 OpenGL ES 的 API 控制着色器渲染 2D 或 3D 场景,性能比 2D canvas 更强,但是技术门槛较高。它提供的 API 中也不包含事件以及对于内部绘制对象操作的方法,需要额外引入框架来实现。


在考虑到团队技术栈和开发周期的同时,为追求更好的用户体验,我们选择了使用 2D canvas API,并实现了一个自研的框架 JFlow。JFlow 框架通过模拟浏览器的事件系统以及布局系统,可以无缝嵌入到我们的 Vue 工程下,让我们的前端工程师能够快速迭代业务需求。

细粒度交互


为了支持表达式级别的细粒度交互,我们在 JFlow 框架中实现了一整套可交互的基础设施,我们的交互设计是基于浏览器内部基本的事件,没有超出浏览器给出的事件范围。HTML 中 <canvas> 的事件都是从顶层触发的,如果要在画布内部,针对每个绘图单元来实现交互,得从 <canvas> 上的原始事件出发,通过构建画布内部的多级坐标系统,定位到具体的绘图单元(捕获),结合浏览器事件本身和定位到的绘图单元层级,再模拟事件抛出(冒泡)的过程,在画布内模拟出浏览器的原生事件系统,通过结合前端事件基础知识,从而实现业务中的交互设计。下面我们从定位、状态、事件三点来介绍:


在什么定位下、在什么状态下、在什么浏览器事件下才能触发什么交互?交互是单一确定的,还是个像事件列表那样有优先级?会冒泡到父对象的交互中处理吗?


定位 定位的基础在于坐标系统,canvas 的坐标系 是固定的,而 canvas 内部对象的坐标系 也是相对固定的,坐标系 之间存在以下关系:


节点的位置由具体的节点布局算法来确定,算法基于内部对象坐标系来计算。


坐标系 上的节点及内部节点均设计为中心对称,即节点在其父级坐标系的坐标为图形中心,这种设计方便设置行列对齐。节点内部的子坐标系以图形中心 为原点,若子坐标为 ,父坐标为 ,则内部的父子坐标系存在如下关系:



节点内部绘图单元的位置,由节点上具体布局算法来确定,算法基于父级坐标系来计算。


状态 判断状态的最常见的方法是碰撞检测,鼠标交互的实现仅需要判断交互点与具体几何图形的关系,即接触到图形或未接触到图形。通过捕捉这个状态的变化,来判断当前正在交互的对象。计算是判断的意思?


事件 Canvas 内部本身不存在事件,要模拟内部事件,需要从顶层分析 canvas 的原始事件,模拟浏览器事件捕获和冒泡的过程来实现。在有了定位的坐标系和逐层的状态判断之后,就能够从顶层的坐标计算出当前交互的对象,并向上抛出事件。


通过构建这些基础设施,我们模拟了事件机制并获得了对内部绘制对象操作的方法,但是 JFlow 作为语言的基础渲染框架,还需要增强对抽象语法树(abstract syntax trees,AST)的互操作能力,见下一小节。

数据与视图


我们上面的设计相当于在 canvas 上加了 DOM 的部分能力,使其能够更方便地接收事件和操作内部绘图对象。但是直接操作 DOM 的缺点在于视图操作和业务逻辑会耦合在了一起,在我们的场景下就是在操作 AST 的同时,还要同时操作渲染对象。为了解耦,以及减少业务代码出错造成的渲染问题,我们需要引入 MVC 或者 MVVM 之类的框架。


我们选择了与现有的一些 MVVM 的框架结合来解决这个问题。但是在结合之前,我们还需要补齐一些能力。


  1. 数据对象与渲染节点:在浏览器内部,布局是由 CSS 来控制的,而在 JFlow 内部,布局由布局算法来确定,布局算法建立了数据和布局之间的关系。在编辑器场景下,顶层的布局需要根据 AST 来绘制出基于控制流的固定布局,为了控制 AST 上节点的位置,需要根据具体 AST 上的节点来查找到具体的渲染节点。JFlow 通过引入 WeakMap 来解决这个问题:在节点被添加时,建立从 AST 节点到具体渲染节点的映射关系。

  2. 数据改变与渲染变更:MVVM 框架的模板会响应数据的变化,数据变化会引起视图上的变化。JFlow 通过 vue plugin 提供了响应数据变化渲染的能力,内部通过调用 scheduleRender 方法,在浏览器每次重绘之前触发一次性的渲染。


现在我们可以引入 MVVM 框架了:

数据流向


虚线左侧是 AST 的使用部分,右侧是框架控制的部分,原始数据决定了布局算法,布局算法控制渲染节点的位置,原始数据节点经过布局节点对应的模板,来控制渲染节点,框架再通过事件通知原始数据或模板更新,进而更新渲染节点。



上图更具体说明了整体数据流向,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、可视化引擎等,以及后续演进方案的规划和预研,旨在创造低门槛高上限的低代码开发体验。

2023-04-10 15:006719
用户头像
蔡芳芳 InfoQ主编

发布了 781 篇内容, 共 495.6 次阅读, 收获喜欢 2748 次。

关注

评论 1 条评论

发布
用户头像
JFlow计划开源吗?
2023-05-12 15:13 · 北京
回复
没有更多了
发现更多内容

等待时间的忍受度

箭上有毒

8月日更

当一只「无头苍蝇」又何妨?

非著名程序员

读书笔记 提升认知 个人提升 8月日更

【网络安全】渗透工程师面试题总结大全

网络安全学海

面试 网络安全 信息安全 渗透测试 漏洞

Vue进阶(五):与 Vuex 的第一次接触

No Silver Bullet

Vue vuex 8月日更

【翻译】数据包的旅程 - 主机之间通信

luojiahu

计算机网络 OSI模型

前端之数据结构(一)

Augus

数据结构 8月日更

【设计模式】单例模式

Andy阿辉

编程 程序员 后端 设计模式 8月日更

数据缓存历险记(二)--被过期键经理上了一课

卢卡多多

redis Redis键过期监听 8月日更

使用 Sequelize 快速构建 PostgreSQL 数据的 CRUD 操作

devpoint

node.js postgresql API 8月日更

graphql计算指令之@skipBy和@includeBy:使用表达式实现简单控制流

杜艮魁

开源 后端 低代码 graphql

在线手机号码上标生成工具

入门小站

工具

12道Java高级面试题:瞧一瞧

Geek_f90455

Java 程序员 面试 后端

为了进阿里,我通宵达旦三个月,学了这些技术点(附Java思维导图)

Java 编程 架构 面试 架构师

Kong-To-APISIX:减轻你的迁移压力

API7.ai 技术团队

开源 网关 kong APISIX

小技巧 | Get 到一个 Web 自动化方案,绝了!

星安果

chrome 自动化 Web 插件 chrome扩展

Dremio 推出在 AWS 云上运行的数据湖服务

水滴

数据湖 8月日更 Dremio

SpringBoot 整合 Drools

LeifChen

drools springboot 规则引擎 8月日更 业务规则

阿里人,五年心血汇聚而成这份Spring Cloud Alibaba开发手册

Java架构师迁哥

刷完这套200+大厂Java真题手册,成功拿到阿里,京东,美团的offer

Java 编程 架构 面试 架构师

15个经典面试问题,如何设计一个百万级用户的抽奖系统?

Geek_f90455

Java 程序员 面试 后端

「SQL数据分析系列」16. 分析函数

数据与智能

sql 函数 分析

藏在煤箱中的文明:一个会说话的箱子能告诉我们什么?

脑极体

Linux之at命令

入门小站

Linux

Gin 介绍

xcbeyond

Go 语言 gin 8月日更

趁着课余时间学点Python(三)变量,基本数据类型,运算符

ベ布小禅

8月日更

Linux 环境如何使用 kill 命令优雅停止 Java 服务

陈皮的JavaLib

Java Linux 面试 springboot 8月日更

IDEA下载及新建第一个Java项目(Helloworld)

Bob

8月日更

Error SHA256 mismatch

一个大红包

8月日更

试错,然后成长

escray

学习 极客时间 朱赟的技术管理课 8月日更

面试官:JVM的运行时数据区了解不?

程序员阿杜

Java JVM JVM、 8月日更

NLP随笔(四)

毛显新

自然语言处理 神经网络 深度学习

基于 Vue 和 Canvas,轻舟低代码 Web 端可视化编辑器设计解析 | 低代码技术内幕_大前端_网易数帆编程语言实验室_InfoQ精选文章