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

基于 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 重排会成为性能瓶颈。另外这种方式在高分辨率的屏幕上有时无法做到抗锯齿,渲染效果无法保证。

    HTML + SVG + CSS 渲染

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

    2D Canvas 渲染

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

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

细粒度交互

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

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

  • 定位    定位的基础在于坐标系统,canvas 的坐标系 A (x',  y') 是固定的,而 canvas 内部对象的坐标系 B (x,  y) 也是相对固定的,坐标系 A 与 B 之间存在以下关系:


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

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

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

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

  • 事件    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数据流向

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

今日好文推荐

如何防止架构师PM化

警方通报网传中电科加班事件调查结果;拼多多解散恶意功能团队;逼死程序员诈骗千万的“翟欣欣案”一审宣判 | Q资讯

谷歌正式发布WebGPU!90多位贡献者研发6年,浏览器终于可以利用底层硬件了

新手用ChatGPT仅需数小时轻松构建零日漏洞,69家专业公司都检测不出来:“不仅能调用开源库,还能彻底重写源代码”

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
京东开源:一款高效的企业级表格可视化搭建解决方案!强生旗下Kenvue纽交所上市:上涨22% 市值502亿美元首日大涨22%!美股今年最大IPO诞生,强生消费品业务Kenvue成功登陆纽交所,市值超500亿美元数据可视化:基于 Echarts + SpringBoot 的动态实时大屏银行监管系统【源码】还在手动配置Nginx?太LOW了,这个超强大的 Nginx 可视化管理工具太牛逼了!(待会删)yyds!网易内部PPT数据可视化学习资源,请低调使用!人生算数:好的数据可视化什么样?港理工教授:电动汽车高级速度控制器设计仿真研究|收获一作论文与导师推荐信!美女们, LV Victorine 钱包 , leather or canvas 好用?IDE + ChatGPT,这款编辑器真的做到可以自动写代码了!高端的数据可视化如何做?限时薅羊毛课程来了!使用这些 Python 工具可视化地探索数据 | Linux 中国人人都能做UE5游戏?《堡垒之夜》升级UGC工具、推出UE编辑器猫头鹰介绍新款 NH-U14S 散热器:为 56 核至强 W9 处理器设计,解热 700 W可视化液体“精油仓”驱蚊手环!不挑蚊子品种,持久有效240天!新一代基线资料表R包,10min学会精美可视化结果!最新数据!澳洲巨头们都在高通胀下赚大了!Qantas,WWS,Coles,Kmart,Bunnings...全部利润狂飙!将 Gedit 作为代码编辑器的 10 项增强调整 | Linux 中国nexttrace: 一款开源的全能可视化网络路由追踪工具超强大的 Nginx 可视化管理平台 Nginx-Proxy-Manager 中文入门指南Webflow:40亿美金估值的无代码建站SaaS,能否跨越下个技术周期?MyBatis-Plus 可视化代码生成器来啦,让你的开发效率大大提速!!气球事件闹大了,麻烦来了低阶到高阶的数据分析图表如何做?内含100套数据可视化模板…颠覆性的忆阻器设计邓上台文革批的丑陋几何级复活【手撕代码】医学影像报告自动生成(2) 数据探索性分析及可视化与防火长城斗智斗勇,RDP安全翻墙秘密花园根茎生长可视化套装,全景透明容器,直观感受大自然神奇变化绑架不能违反质押基本原理 ——笑谈《狂飙》之三减肥瘦身成功,Trader Joe's 的健康食品面向数字化提质提效的低代码架构设计 | 低代码技术内幕Bellevue测试无绳遛狗临时站点:Crossroads,Wilburton Hill,Wildwood各有一个柯维新:为超低延时编解码技术夯实“芯”基础树莓派基金会推出网页端代码编辑器低延时直播技术优化实践,画质增强技术应用实践,云剪辑架构设计与演进,Web端音视频通话技术探索
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。