写点什么

基于 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:007919
用户头像
蔡芳芳 InfoQ 总编辑

发布了 818 篇内容, 共 607.0 次阅读, 收获喜欢 2822 次。

关注

评论 1 条评论

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

Hi3861编译烧录更快捷

HarmonyOS开发者

HarmonyOS

如何用Apipost预执行脚本动态修改Query、Body、Header参数

不想敲代码

Postman 接口调试 API apipost

云安全之浅谈密钥泄露

HummerCloud

云安全 密钥

不常用但却常问的迭代器

虎妞先生

前端 ES6

Flomesh Ingress 使用实践(四)TLS 透传

Flomesh

Kubernetes 服务网格 ingress Pipy 流量管理

git中patch的用法

ModStart

干货|PCBA丝印位号与极性符号的组装性设计

华秋电子

PCB dfm

给webpack提了一个pr之后......

虎妞先生

前端 webpack #开源

众生皆苦,我选pnpm

虎妞先生

npm 原理 前端工程化 pnpm

十分钟用vitepress搭建项目文档

虎妞先生

前端 vite Vue 3

谈谈干前端三年的几点感受

虎妞先生

前端 成长 代码人生

前端包管理工具 npm yarn cnpm npx

虎妞先生

前端 包管理工具 #面试

Flink CEP 在抖音电商的业务实践

Apache Flink

大数据 flink 实时计算

看海泰方圆类ChatGPT技术模型!

电子信息发烧客

图片竟能直接生成逼真音效?这AI模型也太神奇了吧!

人称T客

畅销10年的数据库技术图书,当之无愧的霸主!还有谁?

博文视点Broadview

微信小程序底层框架实现原理|万字长文

虎妞先生

微信小程序 前端 原理 架构、

应用部署初探:微服务的3大部署模式

SEAL安全

微服务 企业号 2 月 PK 榜

对话 ChatGPT:现象级 AI 应用,将如何阐释「研发效能管理」?

LigaAI

人工智能 研发效能 openai ChatGPT 企业号 2 月 PK 榜

JVM性能调优,分享些好用的内存分析神器

Steven

探讨MySQL事务特性和实现原理

小小怪下士

Java MySQL 程序员 事务

Vue3项目框架搭建封装,一次学习,终身受益【万字长文,满满干货】

虎妞先生

前端 前端架构 Vue 3 vue cli

银行零售如何更贴近客户?是时候升级你的客户旅程平台了

Kyligence

数据分析 客户旅程

前端如何实现将多页数据合并导出到Excel单Sheet页解决方案|内附代码

葡萄城技术团队

数据库 前端 架构分布式

从零开始学习BOM&DOM

虎妞先生

前端 DOM

2023年低代码发展新趋势

力软低代码开发平台

C++到Python全搞定,教你如何为FastDeploy贡献代码

飞桨PaddlePaddle

c++ paddle 飞桨

我的2022,从紫竹院到通惠河畔

虎妞先生

学习 前端 成长 年终总结

非代码的贡献也能成为Committer,我与DolphinScheduler社区的故事

白鲸开源

开源 开源社区 开源文化 开源软件 大数据 开源

Python从0到1丨图像增强及运算:形态学开运算、闭运算和梯度运算

华为云开发者联盟

Python 人工智能 华为云 企业号 2 月 PK 榜 华为云开发者联盟

BSN-DDC基础网络详解(二):快速接入指南

BSN研习社

BSN-DDC

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