网易CodeWave“在线智囊团”上新

从2025年起,网易CodeWave陆续邀请网易产品、技术、客户成功、解决方案等专家,聊聊智能开发在企业端落地的前前后后,旨在为大家在技术解析、行业实践、团队建设等方面提供实用参考,提升软件研发和协作效率。

第二期,我们邀请网易CodeWave技术专家,聊聊CodeWave的技术架构演进实践过程。

图片

2014年,Forrester Research首次提出了“低代码/无代码”平台的概念,预测该技术将改变应用程序的开发和交付模式。2019年,Gartner公布了低代码领域的魔力象限,国内低代码厂商也纷纷进入这一领域。

网易CodeWave作为国内最早一批推出低代码产品的公司之一,于2020年左右签订了某国有商业银行的第一个商单。经过五年的持续迭代,产品迎来了成熟期。本文旨在介绍CodeWave的技术架构演进实践过程

01 做一门编程语言

低代码/无代码的技术路径有两条:一条路径是定义一套schema配置(一般是JSON或者XML),然后自己选定一门语言写程序去解释执行这段配置,需要迭代新的产品功能,去设计研发这套配置以及配套的解释器程序。

另一条路径是做一门编程语言,设计一套DSL,从更好的产品体验来看,首选是静态类型,用动态灵活性换程序安全性,通过友好的产品界面去提升开发者开发体验。

思路:基于图数据库

任何一种技术路线,应用的描述数据都具备某一全局数据视图。项目初期,团队选取Graph来描述应用的数据外部模型,考虑到一张图中的点(Nodes)和边(Edges)足够表达一个应用的方方面面,比如修改其信息可以直接反馈到图的顶点、边、属性,并且可以完成级联更新,而且图还可以有版本,所以选择把应用数据放到图数据库中,并且在API层补充了基础Type Check的能力。

但随着语言不断迭代,一个应用的表达所需的数据量逐渐变大,百万级的点边已经使得图数据库的性能问题逐渐暴露,即便我们对当时选取的开源图数据库打了N个性能相关的补丁(分布式缓存,流控,节点间调度问题等),一个巨大的应用读写I/O甚至会超过10s,这几乎无法接受。

早期的存储模型如下图:

0

思路:“存算分离”

事实证明图数据库不是最优解,对于应用数据的存储和引用关系的拓扑计算应该分离,计算层无状态,存储层有状态,需要一个语言服务器和一个存储引擎。

随后CodeWave在短时间内自研了一版语言服务器,重新设计数据结构和拓扑分析算法,支持了Type Check和查找引用分析,彻底放弃了图数据库,选取了关系型数据库来存储应用数据,通过关键外键应用id来分别持久化各个子节点,性能得到了数十倍的提升,一个同等规模的大应用一次读写I/O可以在秒级完成。

随着基础语言能力的不断丰富,挑战也随之而来,自研的语言服务器的迭代任务难度也越来越高,比如类型推导,需要去实现Hindley-Milner或者最优通用,类型表达能力也欠缺,比如Union等等,此外一个应用上千条记录,随着产品规模越来越大,关系型数据存储也面临着技术风险。

早期的类型校验如下图所示:

0

思路:基于一门宿主语言实现eDSL

从低代码形态采用的可视化编程本质来看,就是实现一套DSL,从业界实践来看,DSL两大设计思路:独立于任何特定语言的外部DSL和建立在宿主语言之上eDSL。eDSL的成功案例有谷歌的DI框架Guice的API,.NET实现的LINQ,包括Scala开源的很多eDSL实现demo,基于单一宿主语言的eDSL路线论:

0

该方案的好处是不要再去操心类型系统的设计和研发,在人力成本有限的情况下是首选。

常见的外部DSL实现有解析器组合子和解析器生成器两种,下图左边描述了解析器组合子的DSL设计架构,本质上就是定义了多个Parser,每个Parser都可以解析一类场景,本质上就是定义了多个Parser,每个Parser都可以解析一类场景,比如Scala就提供组合字库scala.util.parsing.combinator,以此去构建更加复杂更加高级语言的Parser,感兴趣可以参考https://github.com/scala/scala-parser-combinators,有些语言没有这类特性,比如Java,就得去写递归下降解析器。

右图则是广泛使用的解析器生成器的DSL架构,区别于定义的语法规则是否在宿主的基础设施之内,解析器生成器流派代表是Antlr,该种技术在DSL解析的生产实践中使用更加广泛。外部DSL自己去定义语法规则和解析规则,而基于一门宿主语言去实现,只需要去完成DSL Schema到某一宿主语言的方法链接和构建,而后就可以使用该宿主语言的类型系统了,其实现和维护成本更低。

0

奠定基调:语言基础架构成型

网易CodeWave最终选取的是eDSL的实践路线,致力做一门编程语言并实现其技术架构。

从商业的角度,语言也会成为代价昂贵的技术壁垒,从质量的角度,一款软件还须从性能、可用性、安全等角度去度量;从功能需求的角度,需要满足用户产品功能的需求迭代。

基础语言架构演进成下图所示:

0

宿主语言选择——Typescript

📌 类型安全

基于TypeScript实现eDSL,其一因为它类型安全,是一门强类型语言。为什么要做一门强类型语言?因为能够在开发早期就能发现问题,进而提示开发者完成对应的编辑修改。

甚至很多学术界研究在将动态语言静态化,牺牲语言的灵活性去保证程序安全,解释器+Just In Time模式变成编译器模式,AOT,运行性能也更好。

📌 类型系统表达能力强

TypeScript作为JavaScript的超集,具有丰富的类型系统,类型推断,类型兼容性,泛型,柯里化,协逆边,联合类型交叉类型等等(Java程序员感受不到的类型系统),社区活跃。在项目前中期,比自研一个类型系统要少踩N多坑。

📌 内置浏览器和可移植性

低代码产品有C/S和B/S两种架构,其中C/S架构在国外比较主流,如OutSystems、Mendix厂商。CodeWave采用的是B/S架构,可移植性更好,维护成本比客户端模式更低,其中将语言服务器内置于浏览器运行,不存在额外的网络通信,可以极快的给出编程反馈。TypeScript可以编译成纯JavaScript,跨浏览器、跨操作系统运行。

📌 团队技术栈

前端技术栈JavaScript,TypeScript由前端团队维护。

📌 其它

TypeScript的语言服务器还提供了一些查找引用,补全,可以考虑复用,由于团队有拓扑分析,符号邻接表的一些算法和数据结构积累,这点属于选用。

WIP: 这套系统运行至今证实了这套理论的可行性,但typescript的language server毕竟是一个ts程序诊断工具,实践过程中发现不仅存在诊断信息含义模糊的返回,存在bug而导致诊断假死,corner case的未知问题等,而且也缺乏比较好的上层扩展能力,会导致语义等价层演变成胶水代码层,变成了“千层饼”,在产品的表现卓越期会碰到诸多阻碍。

此外浏览器中内置该语言服务的同时,也内置了IDE的前端业务,在应用达到一定规模量级后,对浏览器的CPU、内存也提出了更高的要求。接下来团队会致力对语言服务架构进行重点优化,追求更流畅更友好提示的开发体验

可视化编程语言的代码仓库

代码仓库最著名和成功的产品是Git,具备的能力就是存储代码、协作、版本控制。对于可视化编程语言,从数据库逻辑模式来看,描述应用的数据静态特征很明显,一个app是一棵树,且存储本身并不需要计算语言能力,需要的是对树上任一节点快速的存取。

CodeWave存储引擎选用的是基于文档数据库MongoDB做数据库存储,其BSON的数据模型以及基于节点path的细粒度更新能力完美贴合当前的场景,需要注意的点是MongoDB的BSON有单页16M的限制,所以存储方面需要做Page Splitting,也就是页面切分,切分之后子文档的查询需要做Aggregation。

0

📌 B/S架构下的数据一致性

浏览器客户端用户的每一次拖拽操作都会产生一次代码更新,技术上要保证用户在IDE操作中的流畅度的同时,保证数据不丢失。

  1. 定义IDE操作流畅度:正常拖拽操作400ms 内结束,用户可进行下一步操作(多尔蒂阈值400ms是可以让用户保持专注的延迟)。

  2. 用户的每一次拖拽指令:ADD、UPDATE、DELETE

  3. 数据不丢失:指令按照FIFO顺序进行持久化。

  4. 整体设计方向:前(后)端队列严格控制顺序,前端将用户的每一次拖拽指令入队,保证IDE操作的流畅度,选择对应的指令打包发送到服务端,不需要等服务端返回,前端可继续入队操作,保证操作流畅。若服务端持久化发生异常,会进行正向重试保证成功,若仍然失败,会从服务端进行一次查询,清空任务队列,保证数据一致性。

0

目前该模式下平台可以轻松支持百人同时在线高流畅地开发,且云端API的并发能力几乎不会成为瓶颈,只要符合公式RT ≤ 200ms × Q即可,其中200ms假设用户流畅地拖拽编辑时间(按照多尔蒂阈值的一半计算,实际上大部分情况应该是1s以上),Q为队列的长度。若按照队列Q=10个指令,API在100并发的情况下RT延迟只要小于2s即符合要求。

WIP:该方案目前需要完善一套拥塞和重传机制来进一步增强B/S通信鲁棒,主要问题在于一旦浏览器出现异常(bug,网络,多页面开发)打破FIFO,会导致指令错乱失序,破坏数据一致性,若加上IDE没做好容错甚至会导致IDE无法打开,解决方案是提供重传和恢复机制。

服务端收到失序请求后,需发起重传指令,并取消该次请求入库执行。浏览器需要在所有请求中带上seq,发出后放入Map中留着重传,服务端会对每一个seq进行ack,浏览器需要对下一次请求进行seq+1,若收到seq的重传指令,则重新获取seq请求进行重传,ack之后即可丢弃,重传算法本身并不复杂,在数据结构的设计方面需要考虑粒度,比如针对某一个页面或者逻辑编辑,从而去兼容支持多页面/多tab的用户共同开发场景。这里有两个常见的异常case:

  1. 浏览器收不到ack,seq+N拥塞。要么请求发不出来(链接bug,丢包),要么长时间收不到ack(网络延迟,服务端处理慢),此类问题需要根据N的配置抛出拥塞控制指令,控制用户的操作,比如使用一个长度为N的队列。

  2. 浏览器seq重传包丢失。从重传Map中无法找到seq数据包,此时就直接弹出队列中的所有seq+N的数据,强制撤销IDE到seq的数据状态来保证数据一致性。

02 可视化编程语言的IDE

专业的低代码开发工具一定要有一个集成开发环境,类似于Intellij IDEA或者vscode,使其具备软件工程中常见的一些能力,比如debug、运行、协作与版本控制、开放集成等。

  调试  

低代码开发者通过可视化拖拉拽形式来搭建应用,搭建的过程实际上为表达应用编程语义的过程,区别于传统的java、js这类通用文本型编程语言表达编程语义,低代码产品一般是通过的可视化编程语言来表达应用编程语义。

相较于通用编程语言能通过IntelliJ、vscode等IDE debug功能辅助开发与排障,低代码开发者在开发过程中,如果逻辑编程语义无法及时验证,潜在问题难以及时发现,会严重影响编程效率,软件质量也无法得到保障,在一些业务复杂、逻辑众多的场景下尤为突出。

📌 低代码编程中的程序诊断

对于程序员而言,无论你使用哪种编程语言,哪种编程开发环境,程序错误、诊断不外乎以下五种类型:

  1. 词语法错误,无法编译通过

  2. 语义错误,无法编译通过

  3. 程序异常中止

  4. 程序错误退出

  5. 程序运行正确,运行结果错误

对于1,2,静态类型的语言配合开发环境甚至不需要通过编译就能发现并解决。

对于3,这里一般分成两大类,运行时异常和自定义异常(CheckedException,部分语言没有,这里不展开),但不管是哪种,本质就是未处理异常(Unhandled Exception)而导致当前程序中止,这类问题可以通过防御性编程解决,比如NPE的处理方式。

对于4,更多地是指程序资源溢出,比如内存或栈溢出,这类问题往往需要程序员对语言的内存模型有一定的理解,包括可以自动GC和手动GC的语言。

对于5,很容易就能想到解决方案,那就是调试(Debugging)。

低代码开发者同样面临上述问题,那么低代码开发平台需要如何帮助开发者做程序诊断?

  1. 设计成静态类型语言,并提供配套的开发环境。

  2. 提供AI辅助编程,"一定程度"可以辅助增加程序鲁棒性。

  3. 使用静态程序分析技术,发现程序设计中的bug以及潜在问题,比如sonar。

  4. 提供调试技术。

把低代码编程当成一门语言来建设的产品,基本都具备1/2的能力,AI辅助编程随着GPT、DeepSeek的火热在低代码产品中逐渐普及,静态程序分析技术在现代高级语言如Java、JS、Python中已经有非常优秀的工具了,在1的基础上,词语法分析后能够构建出AST,加上对控制流,数据流分析可以自研语言的静态程序工具(CodeWave的IDE也已经有可视化的SAST插件),这里主要介绍调试技术。

📌 调试技术

程序员发现程序没有得到预期的结果,都会进行调试,而现代调试技术早在上个世纪80年代就有了,大家所熟悉的GDB调试就是在那个年代。常用的调试技术:

  1. 打印变量到控制台 print(),println(),printf(),console.log(),...

  2. 打印日志 log4j,logback,logging,...

  3. 代码断言 assert

  4. 调试器 debugger(GDB,PDB,Intellij IDEA, VS Code,各浏览器支持JavaScript调试...)

上面这些调试技术都或多或少有些弊端:

  1. 除了调试器,都需要修改代码

  2. 除了调试器,上下文都不够完整

  3. 性能考虑,比如JVM因为怕影响性能默认关闭assert

  4. 日志配置复杂,代码侵入

  5. 调试器又具备一定学习门槛

  6. ...

因此大家对于各类调试技术也是各有钟爱。调试器断点调试是广大程序员最常用的一种调试技术,低代码是可视化编程体验,需要设计一套 “全栈可视化调试” 的方案,全栈调试支持在前后端整个调用链路过程中设置断点进行调试,Java虚拟机提供了Java调试接口JDI和标准调试协议JWDP,所以要实现一个Java语言的调试器并不困难,大多数IDE产品的调试器就是基于JDI和JWDP实现的,但低代码的调试器有两个难点:

难点一:JDI返回的是调试代码的行号,如何体现到可视化组件上?

难点二:JDI的断点和执行事件也是基于行,但可视化组件的一个组件块跟代码行并不具备一一对应的关系。

📌 难点一:SourceMap

SourceMap本质上是一个保存源代码转换前后位置信息的数据结构,在web devtools领域使用较多。常见的SourceMap的数据结构设计:

  1. sources: 转换前的文件。该项是一个数组,可能存在多个文件合并成一个文件

  2. names: 转换前的所有变量名和属性名

  3. mappings: 记录位置信息的字符串

  4. sourceContent: 原始内容

  5. version: source map 的版本号

低代码的数据结构需要做一些调整:

  1. jsonPaths: Map类型,节点对象和节点路径的map,持有原始可视化内容

  2. lineNumberTable: 对象类型,源码位置表,持有源代码内容和位置信息

  3. version: SourceMap 的版本号

SourceMap的算法实现一般有两种方案:

方案一:随机翻译,反复订正。这类方案常用于基于模版进行的代码翻译拼接模式,弊端是需要不断地修改SourceMap对象,翻译不独立带来行号不对齐,自适应困难,并且会有额外的修改开销。

方案二:顺序翻译,Visit后代理统一处理。这类方案适用于有比较好的语法树操纵框架,在翻译过程中完成,自适应能力强。目前我们选用的该方法。

📌 难点二:压栈组件内部执行过程

低代码编程中的最小粒度是一个个组件,但低代码的导出源码是具备可读性高的代码,所以多个组件往往有时只需要一段简洁的源代码就可以表达出来,但断点事件是基于源代码行,此时比较好的做法是将这段组件的执行顺序模拟后压栈,执行后从栈中弹出,基于JDI事情做不同组件的切割。

0

WIP:Smalltalk语言系统以及OOP编程的创始人Dan Ingalls在《The LIVELY Project》提出了“time-travel debugging”,通过浏览器,编辑器,调试器,REPLs等调试方式都是相对孤立的,应该能基于时间线串联起来调试所有正在编辑的上下文,并能够redo/undo所有变更效果。

《Draggable Code》的原型中比较形象地描述了这个场景。live programming是可视化编程的一个趋势,下图是CyberBrain的一种调试形态(基于Python):

0

  发布  

低代码产品中的发布一般分为预览和正式发布,属于低代码产品最热点的功能,根据投产的数据统计,一个平均活跃度的应用平均每个月会产生120次以上的预览或正式发布。所以发布的成功率和性能直接关系到用户对产品易用性的体验评分。

低代码产品的发布技术架构差异非常大,差异来源于两大低代码流派——引擎式和转译式,前者发布生效快,万物皆配置。后者发布链路长,而且依赖目标语言的运行

发布引擎的最佳实践就是状态机设计模式(State Machine Pattern),编排和扩展灵活,状态规则和流转能力强,用户的每一次发布对应着每一个长短不一的状态机实例,也是IDE内核化的体现点之一,配合每一个具体执行阶段的增量技术,可以去尝试追求引擎式的产品体验。

0

📌 难点一:发布成功率如何保证?

引擎式的低代码产品发布几乎不可能失败,因为其技术原理是配置加载,这样配置有问题也暴露在运行时。但转译式的低代码产品,尤其是有一门编程语言,往往存在着诸多会导致失败的阶段。

上文提到的状态机设计还有个好处是可观测性能力强,下图是目前对实际环境的各接入阶段的实时监控示例,可以很明显发现哪些具体状态的执行发生了异常,进而分析解决问题。

0

当然,(有数据证明)导致失败的原因大部分其实是开发者自己导致的问题,比如类型不匹配,不规范扩展,资源问题,运维变更,配置错误等等,还有一小部分失败属于软件bug,好的产品实现一定可以快速识别出是用户的姿势问题还是软件自身的bug,即使后者的比例非常小

这两类问题往往技术解决途径有两种,一类是前置校验,另一类是后置容错。语言侧前置校验的基础设施一般就是Language Server(包含各类checker),一切不符合语法语义(也有可能是程序员实现bug导致的)的错误都应该前置给于开发者提示,一种比较常规的实现类似于TDD的研发模式,先有规范约束,并完成测试用例,该test/checker设施一般可以左移进Language Server,以此来给与开发者更加友好的交互体验。

下面重点讲一下容错机制的实现。容错分为两大类:

  1. 目标语言对可视化语言DSL的容错

第一类问题比较典型的就是变量shadow问题。目标语言是Java,受限于语言,典型的场景就是方法内使用foreach或者lambda,跟方法内的变量由于在同一个scope下而错误,此时就需要对这些变量进行shadow,具体的实现需要对每一个方法在翻译期间做变量分析和搜集,递归向下,并进行同层级同名变量压栈,递归出口对栈内同名变量进行rename并弹出。算法随着操纵语法树的过程并不会增加额外的时间复杂度。

第二类问题就是Java lambda的闭包限制。在可视化语言中可以支持在lambda中对方法栈上实际变量的修改,但Java为了线程安全,用闭包思想来设计了lambda的实现,lambda引用了外部的局部变量,是引用了变量的值,只可以访问不可以修改,此时只能对该变量进行wrap,将该变量变成一个可变对象,这样才允许进行修改。

  1. 框架容错

开发者大部分时间对面向框架在做应用开发,比如配置一个权限点,定义一个工作流,创建一个定时器,发起一次接口调用,这类问题往往脱离了基础语言可以管辖的语义范畴。

上面提到的Language Server大部分情况下解决不了权限资源点冲突,工作流定义、定时器、调用配置错误这类问题,需要由各类框架在框架引擎内完成容错,并给于用户正确的提示,可以发生在发布阶段,也可以发生在运行时。

还有一种统一的技术实践方案是,纳入Language Server能力范畴,支持框架类的自行扩展。

📌 难点二:发布性能如何保证?

服务端的目标语言的Java,转译耗时,Java编译耗时,Java启动同样耗时,需引入全链路增量技术

可视化语言转译成Java的增量的技术核心就是快速判断代码有没有变更。有三个技术方案,各有利弊,详见下面翻译器增量翻译的章节描述。

Java编译启动耗时我们也采取了两套技术方案。一套方案是利用gradle的增量编译技术,然后基于热部署工具做热部署,每次发布不需要启动应用,JVM中基础框架的classloader不进行反复装载启动,只reload用户侧的classloader。

另一套方案是不进行打包,直接编译后,将二进制文件丢进Java虚拟机用agent做hotswap,这种方式可以真正做到秒级reload。Hotspot JVM的字节码交换技术是基于Instrument的API实现,其只支持方法体变更的hotswap,甚至新增一个方法或者属性都无法支持,我们打上DynamicCode Evolution的补丁后,可以支持任何类结构变更/新增的hotswap,完美贴合低代码的场景。

这里还涉及到一些技术细节,比如这些修改过的class文件如何正确加载到classloader中,需要基于双亲委派和使用一些插桩技术来增强类加载器。

从测试的数据看,一个30w节点的大型应用,会产生约3600个Java文件,在同等(4c8g虚机)硬件资源下,修改编辑一个逻辑,就转译->编译->启动的耗时从1m30s+降低到了15s内完成(文件的传输还未增量的前提下,全量文件的传输有3s甚至更多的网络开销)。

WIP:我们针对提升发布性能还有个思路的大方向是框架引擎化,这一点就跟引擎式的产品类似了,配置下发,解释执行,也是发布的最快模式,该模式在某一类的场景上还有一个数量级的提升。

  多人协作  

低代码的开发者并不一定具备专业的开发技能,甚至有些开发者并未使用过Git这类产品,低代码的多人协作主要还是直击用户最原始的需求原型--小团体同时开发同一个应用,可以独立调试,互不影响,调试完进行应用合并,同步后继续开发直至全部完成第一次发布上线。所以,低代码中的多人协作其中最主要的功能便可分解为如下几点:

  1. 分布式开发同一个应用

  2. 需要小团体(10人内)范围内同时开发

  3. 有相对独立的开发环境,互不影响

  4. 相互拉取合并

  5. 可以发现组件的合并冲突情况,提供解决冲突的方式

低代码的协作最难点是可视化的组件而非文本粒度的冲突检测和合并,git的diff基于文件摘要相似度以及文本行比对完成,但可视化要比这个复杂得多,两点:

  1. 代码的载体不是文件,不能简单地做文件相似度比较,是一棵语法树,语法树层级非常深。

  2. 直接把树的层级内容返回给使用者,没有实际意义。

Diff算法对编辑距离算法Myers做了一些改良,因为原始的Myers只能输出距离,但我们需要输出编辑路径,如下图所示:

0

这里又引入两个技术难点:

  1. 即使IDE交互和语言语义规定name必须全局唯一,但页面的结构树层级深,布局广,分布式开发的环境下,不同的开发者编辑出来的name仍然相同,一旦代码的合并后,会合入大量同名的组件的问题,合并者会发现IDE上成千上万的报错信息?

  2. 冲突基准是什么?

问题1的直观解决方案应该是IDE的修复能力需要做得更智能,代码的合并不应该去耦合语言的语法语义范畴。目前我们的解决方法是在算法后增加后置业务处理的钩子处理器,去扩展一些业务语义的产品需求,比如上述问题我们通过将不同层级的节点形成级联关系,让跨层级的同名也给形成冲突对,并且要求用户必须重新进行命名,否则无法合入的方式解决。

问题2,如果只有两路,那么上图中C也会被识别成冲突,这种使用体验不太好,因为大部分合并代码的需求是一个source合入target的诉求,大部分情况下并不应该去产生冲突,那么如何去优化合并算法——三路合并算法,三路合并算法的灵活性比较大,比如git merge就使用的三路合并,可以给使用者很大的选择空间,能够自定义合并驱动策略,缺点是使用者学习门槛也比较高,技术实现时间和空间复杂度都不低,因为需要额外的一路存储代码快照,通过DAG找source和target最近的公共祖先,然后做合并冲突检测。

我们使用了操作记录辅助合并,有点类似于指针索引,通过操作记录是创建create,编辑update,删除delete来回放代码的指针移动过程,根据产品预设定的冲突规则表给于用户冲突展示,比如上述update,update会识别成冲突。

WIP:目前仍在完善代码合并层面的一些审计信息,希望同样给使用者呈现merge graph以及更完善地可视化组件层面的annation with blame,帮助使用者更好地追溯代码变更

此外在跟一些实际企业中的使用用户一些深入交流过程中,用户反馈当项目处于赶交付节点,集中研发的模式中,更倾向于借助于浏览器网页多开的模式,利用一些管理手段进行共同开发一个应用,目前并不支持这种产品模式,该种产品模式的终极形态是在线编辑协同(市面上大部分在线协同的编辑器都是基于OT和CRDT算法的变种实现),但在代码编辑器中其实不太使用,反而会造成程序不完整的发布,要配合一些语音室聊天功能,所以我们从技术的手段做了一些粗粒度的锁来保证数据一致性,也能够work。

  开放集成  

低代码产品面临的挑战是开发者画像过于广泛,并且tob的软件还受到企业内部组织架构,研发模式的约束,大多数企业已经具备了成熟的技术栈,要帮助这类企业成功需要多种模式的开放能力。

图片

📌 连接器

最常见的低代码产品集成的产品形态叫做连接器,连接器顾名思义就是链接到某些数据或者中间件的那些即插即用的驱动,比如可以快速制作一个DeepSeek或者coze连接器,从而快速应用到低代码开发的应用中。

但连接器的实现很容易出现一个误区,会变成不符合Framework IoC的要求,出现连接器资产繁荣后的软件兼容性和安全性问题,web框架从请求入口,认证,访问控制流入到业务逻辑,再到底层的文件、数据库等操作每一层的框架扩展点都必须提供严格的"链接"规范。

  1. 框架模块化。框架内核需要支持模块化设计,使各项功能能够独立开发、编译和链接,使得新模块可以快速插入或替换而不影响其他部分。

  2. 解析与版本控制。连接器开发者和内核开发者要进行契约式编程(Contract Programming),否则无法即插即用。

  3. 安全性。连接器在加载和链接模块时,内核必须完成自检,保证数据完整性和安全性。

  4. 高效的错误处理和调试。为了实现即插即用,需要提供详细的错误信息和调试支持。这样开发者可以快速定位和解决模块集成中的问题,提升系统的稳定性和可靠性。

WIP:资产繁荣的后背更多就是安全隐患,有技术、政策、政治多方面安全,这点跟大模型产品面临着同样的问题,我们在优化资产接入流程的同时,需要在连接器加载过程中提供security manager来保证资产安全

📌 SDK / 扩展库

除了连接器,还有一类常用的开发扩展能力是sdk扩展,sdk扩展大部分属于无状态的纯函数,这里涉及到的主要难点是sdk的兼容问题,比如后端的SDK大多数是Java SDK,lib冲突在Java项目中较为常见,比较常见的技术实现是分析依赖链后用不同的类加载器进行隔离加载,解决掉用户侧冲突困难。

WIP:要最大化SDK的作用范围,比如支持在SDK中定义数据/业务对象并托管Spring纳管,此时会涉及到类加载器间对象的通信问题。

这里如果没有对象冲突在同一个Spring容器中没什么问题,一旦涉及到对象冲突,有两种实现途径,一种是直接失败,要求修改,这类对于sdk的使用者不友好,因为使用者跟开发者并不是同一个人。

另一种是要求开发者按照指定的API进行对象使用,比如按照指定的方式进行依赖注入,技术层面内核劫持对象实例化,通过父子容器隔离的方式保证兼容。

📌 高代码

诸多低代码产品还提供了一种通过高代码扩展的方案,可以很严肃地讲,这是一种Non Compliant的扩展模式。

Java,js等等程序开发在专业的工具中开发效率和程序安全性远远高于一个低代码产品,尤其是一个还拥有一门自己编程语言的低代码产品,连最基本的类型可能都很难抹平。要通过这种“hack”、“unsafe”的模式进行扩展一般出现在产品缺乏框架设计的初期,比如开发者想在应用中写一个拦截器来做一些认证逻辑,或者想在ServletRequest上做些头设置解决一个网站CORS的问题。

03 最正确的事情——可扩展的翻译器

CodeWave并没有做语言的运行时,去做解释执行类的“引擎式”产品,因为解释执行最大的商业化阻碍是捆绑,尤其是对于金融银行和数科类有技术规范的企业,这几乎是不可接受的。所以项目初期我们就选择了一个方向——做转译(Transpiling)而不是做解释(Interpreting)

前端转译成JavaScript,后端转译成Java,这里有两点明显的优势:一是源代码透明,客户不存在任何代码可维护性焦虑,并且支持导出源代码,自行编译部署;二是运行时本身不产生任何性能损耗,以下主要讲述服务端代码转译的实现机制,前端类似。

服务端转译的初衷是打造一款通用的从DSL转换通用语言的编译引擎,目前默认内置转译目标语言是大生态语言Java,要考虑的技术要素如下:

  1. 设计一套的调度框架来完成语法树操纵。

  2. 性能保障。并行,增量技术。

  3. 生成的代码,可读性好,符合开发工程结构规范,并满足各厂标准。

项目思路和方案  

实现一个翻译器内核,利用插件去解决各厂标准不一的问题, 整体架构思路如下:

0

📌 技术难点一:增强Java动态派遣

在翻译节点时,除去处理该节点本身的元信息外,节点内也会包含其他的AST节点(树状)。按照计算机语言业界标准,会对此语言预先生成一套节点访问工具,即节点Visitor,在整套Visitor体系内对具体节点的翻译做手工路由,使其能精准导航到语言节点的翻译方法。

Java语言本身支持静态和动态两种模式,静态模式最大的优势就是所有的方法绑定发生在编译器,性能极高,内敛简单,但维护,一旦语言发生变动就需要调整,在语言在不断迭代过程中不是首选方案。

单纯的动态派遣可以将方法绑定后移到运行期,JVM根据虚方法表和接口方法表进行派遣,即使JIT一些内联优化,在调用频率高,结构复杂层次深的AST访问中性能损耗的风险会被放大,此外查表跳转的时间复杂度,在未命中缓存的情况下最坏情况需要遍历,时间复杂度是O(n)。从测试数据来看,一次静态派遣1ns不到,一次动态派遣需要3~5ns。

综上,我们翻译器在可维护性和性能上做了tradeoff,整体思路改为:在加载整个AST及Visitor时,对AST节点及其Visitor方法进行预分析,生成AST节点对应的Processor(节点处理器)并将其编排成Processor路由表,然后在翻译某节点时使用Processor管理器查询此路由表即可获取对应AST节点的Processor进行翻译调用。

这种模式的好处是解耦待转译语言的迭代约束,并且最大化的在编译器内联的同时,利用路由表提升动态派遣的性能,路由表的维护完全跟翻译器业务相关。

📌 技术难点二:并行和增量技术

大部分语言编译器都对编译性能有机制要求,比如javac中M1芯片上并发8线程编译500个中等规模的Java类,大概需要2s左右。在一般体量应用(约10W语言节点以下)与大体量应用(30W语言节点以上)翻译时,若单线程运行上述的翻译时间会扩大到半分钟以上,所以是否拥有并行编译能力,是对一款语言编译器性能好坏的考量

在翻译AST之前,节点之间相互引用、依赖关系,生成引用符号表,以此符号表为翻译上下文基础,独立模块进行多线程并行翻译。

核心数据结构如下图所示,算法的实现就是递归AST的过程中填充各层次节点的NodeInfo。有了符号表再建立依赖图,依赖图是有向无环图DAG,图上节点就是nodeInfo,边就是依赖,数据结构选取邻接表,比如A引用了B和C,B引用了D,D没有引用。此时输出领接表如下图所示:

0

后面通过Kahn算法进行拓扑排序,输出批次划分即可,比如上述第一批次是A,第二批次是B和C,第三批次是D,那么B和C是可以并行翻译的。

并行编译的模式的时间复杂度和空间复杂度是客观存在的,瓶颈一定在CPU,内存,甚至IO其中某一点。那么问题来了,如果用户只给你1c2g呢?

在发布的章节中,提到了增量编译技术。增量编译技术和并行并不冲突,而且可以极大弥补并行的硬件超模要求,增量的核心难点就是分析应用的编辑改动范围,只编译出有变动的逻辑源代码,交由上游的编译服务完成Java源文件的增量编译,这里的难点技术就是如何正确识别哪些代码需要重新进行编译。有三个方案:

1.AST的Diff。有现成的API可用,毫秒级的RT,但翻译器需要缓存上一次的完整代码,就跟gradle的增量编译实现机制类似,但我们的翻译器在云端服务器上,每天有上百个应用发布,需要超大内存。如果硬件资源充足,是首选方案。

2.基于AST做哈希。这种模式的好处的只需要存几个哈希值,但是计算哈希复杂度高,即使StringBuilder底层是基于数组的实现,提前做好容量设置,JVM执行一次append的时间是50ns左右,但是AST上会有上亿的属性值,计算时间会恶化。如果AST的杂音大,情况会更坏。

3.代码AST上附加Last-Modified Time。该方案性能和开销应该是最小,实现成本上,NASL存储模块已经实现了这些打标信息,可以直接复用。

目前我们软件实现上的方案均支持,比如逻辑节点AST巨大,大应用一个逻辑有上亿的节点,去做摘要太耗时,使用编辑时间来做,实体数据结构AST在万级别,耗时几乎忽略不计,而且不需要依赖其它信息输入,直接做哈希对比。

有了操纵语法树的框架,就可以提供翻译器插件机制,通过自行写插件的方式去解决工程规范的问题了。

WIP: 生成的代码可读性和规范是一个更高阶的要求,否则在tob的过程中会遇到代码审计层面的挑战。我们接下来会重点对生成的代码,工程结构做进一步规范约束,并划分基础语言和语言框架的代码组织形式,框架引擎类下沉为SDK,增强源码程序的封装性,安全性的同时,扩展性也更好。

0

04 “技术变现”——商业化架构

大多数低代码产品初期都是按照SaaS的模式呈现给用户,但在实现商业化的过程中,低代码产品需要不断适应市场的需求变化,这不仅要求低代码平台能够支持SaaS模式和私有化部署(On-Premises)模式,还要求平台能够集成更多的服务和功能,以满足不同客户的需求,OutSystems和Microsoft Power Apps这类先驱SaaS产品是较好的佐证。这对低代码平台的技术架构的灵活性和扩展性也提出了挑战。

 多模式支持的挑战与应对策略  

支持多种部署模式意味着低代码平台需要有足够的灵活性和配置能力。为了应对这一挑战,CodeWave采用模块化架构,将平台拆分成多个独立的模块内核,每个模块单元负责的一个特定功能。这样可以根据自己的需要选择部署哪些服务,实现定制化的部署方案,比如SaaS运营模式需要订单,支付,资源包等业务模块,以及运营配置的下发。

在低代码产品探索更多的商业模型过程中,如基于使用量的计费模式、市场化的资产交易平台等也可以进行模块扩展而不需要迭代原有内核功能

此外,平台也是沿用了微服务架构,提高平台的可维护性和可扩展性,便于引入新的服务和功能灰度,以确保软件更新的平滑过渡和最小化对用户的影响。

高度集成的服务体系--模块化/内核化  

为了满足不同客户的需求,低代码平台需要能够集成多种服务和功能,如AI工程套件、云上RDS服务、容器服务等。这要求低代码平台具有高度的可扩展性和兼容性。

一种有效的方法是通过插件或模块化的方式来实现功能的扩展,CodeWave的技术架构一路演进而来,内核便由上述IDE服务和语言工程构成,其中IDE内置三大核心引擎,存储引擎、发布引擎和数据执行引擎以及一个应用生命周期管理系统,这些核心模块组成了一个低代码生产工具的MVP,足以支撑应用创建,编辑,发布运行。当然,要达到一个完备的产品,需要更多模块共同完成,如下图。

0

  1. IDE服务的模块化体现在项目打包的过程中也支持根据商业售卖情况支持选择性构建和混淆交付,如下图所示:

    0

  2. 语言工程的模块化扩展体现两大类:

    翻译器的源码扩展,通过翻译器提供的插件进行自定义扩展,涵盖基础语言和工程架构;框架功能的扩展,通过Framework提供的插件进行自定义扩展。比如不同的企业对于Process工作流的功能要求、组织架构等方面有不同的需求。

  数字化转型的助力--研发基座  

在企业级市场,低代码平台不仅是工具,更是企业数字化转型的助力。通过提供易于使用的开发环境和丰富的服务集成,低代码平台可以帮助企业快速构建和迭代应用,加速创新过程

在这类企业中,低代码平台变成了推进企业生产模式,组织模式,服务意识重构的一员,企业级的产品体验往往不如消费级产品,企业级的研发人员并不清楚实际业务的真实需求,所以想通过引入低代码产品来输出业务,然后集中精力解决数据链接和业务流的组装,构建一个能够解决企业内痛点问题的解决方案产品。

这里的问题就变成了基于现有的低代码平台怎么实现这套解决方案产品。OpenAPI集成、各模块的插件扩展、应用资产共享共同支撑了这条切实可行的路径,更复杂的产品能力则需要购买更多的定开服务费用来实现。

对网易CodeWave感兴趣的友友,欢迎私聊小码哥,可进低代码开发者社群获取更多学习材料和源码~

Logo

低代码爱好者的网上家园

更多推荐