first commit
This commit is contained in:
143
专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md
Normal file
143
专栏/说透低代码/00开篇词|抛开争论,先来看看真正的低代码.md
Normal file
@ -0,0 +1,143 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词|抛开争论,先来看看真正的低代码
|
||||
欢迎和我一同展开低代码的学习卷轴。
|
||||
|
||||
有人说我很“多情”,毕业至今 17 年,我“勾搭”过多种计算机语言和技术,有后台类的 Java、C、C++,有前台类的 TypeScript、JavaScript、HTML 和 CSS,还有不前不后的 Node.js;甚至还差点“误入歧途”转岗做 UX,2015 年我和团队还受邀组织了多次 UX 实战讲座。现在呢,我在中兴通讯担任软件研发资深专家。
|
||||
|
||||
看到这,你一定很好奇为什么是我来带你学习低代码?这就要谈及我“专情”的一面了。在低代码领域,我有幸成为了国内早期“吃螃蟹”的那批人,一直持续到现在,也算小有成就。
|
||||
|
||||
2018 年初,我收到一封闭关研讨的邮件,正式开启了我的低代码平台的架构和实现之旅,这个低代码平台叫 Awade。现在看来,我们的低代码平台的起步时间比国内绝大多数同行,包括各大互联网巨头都要早。甚至,如果将构建低代码平台的前序工作Web 组件集 Jigsaw的开发作为起点,那我们启动的时间就可以追溯到 2017 年 4 月甚至更早了。如果你感兴趣,可以点开链接了解一下这套专为低代码可视化开发打造的组件集。
|
||||
|
||||
经过 4~5 年的持续演进和积累,现在,由我主导的低代码平台 Awade,已经非常成熟了,成了中兴通讯事实上的标准实现。2021 年,Awade 更是获得了公司级的 CTO 专项奖,以鼓励我们在研发提效方面的贡献。
|
||||
|
||||
在低代码平台的应用方面,我们也有了满满的收获。到目前为止,我们主要是对内推广应用,采用低代码平台交付了 150+ 商用功能,主要客户是通讯运营商,全面覆盖了国内各大运营商,以及其他国家的知名运营商。
|
||||
|
||||
在我写这篇文稿时,2022 年巴塞罗那世界移动通信大会(MWC2022)正在进行。其中,中兴通讯展台里就有一组 App 是采用这个低代码平台开发的。这组 App 有酷炫的展示效果、丰富密集的交互功能、流畅的运行性能,不仅打破了低代码平台只能开发出又丑、又难用的 App 的刻板印象。更重要的是,它们定义了低代码平台能开发出高质量 App 的新高度,为低代码的支持者注入了信心。
|
||||
|
||||
你可以看看公开宣传资料里展出的这组 App 的 UI 效果图:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
从 2019 年开始,我逐渐在国内各大行业大会上分享我在低代码平台研发方面的各种经验,由此也结识了业内许多专家,深入了解了低代码在不同公司的多样实现方式,以及良莠不一的应用效果。
|
||||
|
||||
这次,我将我这些年在低代码架构、实现和应用方面的积累,整理成这个专栏分享给你,希望能帮你拨开迷雾,对低代码有更客观、更深入的理解。
|
||||
|
||||
争论不休:银弹 v.s 毒瘤
|
||||
|
||||
纵观我整个职业生涯,我从来没有见过哪个技术会长时间受到如此两极分化的评价,支持者将低代码奉为“银弹”,反对者称之为“行业毒瘤”。这两种极端评价的存在,充分说明目前低代码在各个企业中的实现效果良莠不齐,方式方法也各不一样。
|
||||
|
||||
做得比较好的企业,确实利用低代码技术获得了显著的收益:或是降低成本,或是提升效率,又或是兼而有之。因此,这些企业往往会把低代码技术奉为银弹,大力推广,持续获利。相反的,那些未能帮助企业解决实际问题的低代码实现方式,不仅无法降低成本、提升效率,反而起到了相反的效果,这个情况下,低代码不免就被贬损为毒瘤。
|
||||
|
||||
其实,无论是银弹也好,毒瘤也罢,两种评论共同说明了一个问题,那就是传统 Pro Code(纯代码)的开发模式与高速增长的业务需求之间产生的矛盾越来越尖锐,我们急需一种新的模式来消除这对矛盾,低代码就是业界共同给出的新模式。
|
||||
|
||||
不过,低代码模式目前仍处于探索期,甚至到现在都还没有一个已达成共识的定义,它就像是一个大框,啥东西都可以往里装。
|
||||
|
||||
在这个时间点上,用语言去争论低代码到底是银弹还是毒瘤,其实并没有太大的意义。沉浸在争论的迷雾中,你就无法客观、理性地看待这件事情。不如我们回归技术人的处理方式,看看目前一线低代码平台真正的架构和思路,用你的技术理性做判断。毕竟,在这个话题下,没有谁比低代码的一线开发者更有发言权了(将自己代入其中也不失为一个好方法)。
|
||||
|
||||
但要拨开迷雾,深入了解、学习低代码,确实不怎么容易。
|
||||
|
||||
学习低代码难在哪儿?
|
||||
|
||||
低代码平台是一种非常复杂的综合系统,它的实现过程涉及到大量的通用技术、架构设计方法,需要开发大量的功能模块,代码量动辄达到数十万乃至百万行的级别,需要使用和无缝集成数以千计的开源技术。
|
||||
|
||||
这也就导致了学习如何开发低代码平台,与学习使用任何一门具体的技术都不一样。当前世界上并没有一个公认的低代码实现技术标准,哪怕是技术白皮书,简单地说,就是没有一个清晰的学习目标。
|
||||
|
||||
举个例子,4G/5G 通信协议是极其复杂的协议,学习起来显然非常不容易,但毕竟通信技术是有公认标准的,只要有恒心和信心,不停地攻克协议中的各个章节,总有一天能完成协议的学习,成为专家。但低代码不一样,虽然它的复杂度远没有通信协议那么高,但是它没有标准,学习它就意味着:没有起点也没有终点,没有正确也没有错误,没有考试也没有答案。
|
||||
|
||||
这样的状况对于在校学生来说是最舒服的,特别好“混”,但如果你要成为一位低代码的架构师或负责人,对你来说却是噩梦:
|
||||
|
||||
我学到的知识真的就是低代码所需要的吗?
|
||||
|
||||
有没有更好的架构思路和实现方法?
|
||||
|
||||
为什么业务团队总是提出平台能力之外的需求?是我错了还是他们错了?
|
||||
|
||||
我们常说,鞋子好不好只有脚知道,同理,低代码平台好不好,只有业务才有发言权。所以,面对这样一种知识,最合适的学习方式就是倾听他人的经验,听听别人是怎么成功的,也听听别人是怎么失败的。
|
||||
|
||||
虽然现在行业大会多数都有低代码专题,但以演讲形式分享低代码的实现经验实在太有限,也不成体系。对于低代码这样复杂、综合的系统来说,50 分钟左右的分享实在是杯水车薪,只能展示一些碎片化的知识内容,学习成本很高。而且不同业务背景对应不同的实施策略,有时甚至是矛盾的,不明就里只会越听越迷糊。
|
||||
|
||||
而专栏是一种系统展示低代码知识的极佳形式。从架构设计到演进策略,从细到代码级别的技术要点说明,到总体的技术选型思路等,我都会通过这个专栏,将我的经验充分、系统地展示给你。
|
||||
|
||||
如果你是一位一线开发人员,你不仅能知道当前大热的低代码到底是怎么一回事,也可以从专栏中学习到低代码编辑器各主要功能模块的具体架构方法,从而帮你提升架构能力,为未来独立架构一个功能模块做好准备,缩短从一线研发岗转型为架构岗的周期。
|
||||
|
||||
如果你是一位架构师,你可以从中学习到如何恰当地设计低代码编辑器和编译器之间的关系和抽象,从而架构出一套具有高度通用性的低代码编辑器,你也能知道如何围绕编译器提供扩展能力,设计出比较完备的低代码插件系统,实现通用与效率兼得。
|
||||
|
||||
如果你是一位决策者,那你可以从这个专栏中了解到实现低代码平台过程中的各个阶段的特点,以及采取什么样的策略可以确保平台始终朝着高通用性的方向演进,同时你还可以了解到采用哪些方法可以让平台兼具较高的开发效率和尽可能广的适用范围。
|
||||
|
||||
我会给你讲什么
|
||||
|
||||
在这个专栏中,我主要为你提供了低代码平台的核心模块,包括低代码编辑器主要功能的技术要点,以及实现思路和具体方法。
|
||||
|
||||
除了编辑器的实现技术要点之外,你可以从这个专栏中了解到低代码平台的架构策略和思路、从零开始打造一个低代码平台需要经历的阶段以及特点,甚至还包括低代码模式对应用全生命周期的支持,插件系统和生态圈的打造等内容。
|
||||
|
||||
我把这些内容整合成了下面这张知识地图:
|
||||
|
||||
|
||||
|
||||
你可以从这张图中看到,居中的低代码编辑器是低代码平台的核心功能模块,它的能力基本决定了低代码平台的能力。之所以说它是核心,不仅因为它需要提供各种基础编辑功能、所见即所得的效果,更是因为它是整个平台所有功能的锚点,低代码平台上任何内置功能、扩展功能都是以它做为入口。
|
||||
|
||||
同时,多数锚在编辑器上的功能,其本身也具有非常高的复杂度,任何一个功能点都有相对独立的演进线路。比如代码生成器,它与编辑器之间的关系甚至可以决定平台的长期演进策略。插件系统则是给应用团队开放的扩展和定制的能力,用于解决通用性低代码平台在具体业务落地时的各种个性化问题。基础设施则是低代码平台的基石,它的特殊在于逆向性,它的研发不得不先于低代码编辑器,而集成时却必须完全融入低代码编辑器。
|
||||
|
||||
不过,出于对学习梯度的考虑,我并没有完全按照这张知识地图排布内容,而是将整个专栏分成了三个部分:
|
||||
|
||||
|
||||
|
||||
你可以发现,这三部分覆盖了以低代码编辑器为核心,同时包括代码生成器及策略、基础设施、插件系统及周边等三大编辑器的主要研发支线延伸。其中低代码编辑器的内容占据了专栏的绝大部分,三大延伸内容也都覆盖了关键内容。
|
||||
|
||||
第一部分是认知基础与架构策略篇。
|
||||
|
||||
这部分中,我们不会涉及具体技术,而是主要从架构设计和演进策略等角度来学习低代码。所谓磨刀不误砍柴工,在启动低代码的研发之前,你肯定要对低代码有个大致的了解,同时也需要先有一个清晰的系统架构思路,确保各个模块有序开发和相互依赖。有明确的演进策略之后,我们才能确保演进过程能让好钢都用到刀刃上,资源不发散、不做无用功。
|
||||
|
||||
不仅如此,这部分还详细给出了在低代码平台启动研发之前,我们需要准备好的“家底”,哪些是必备的,哪些是可选的,哪些必须要自主掌握,哪些可以借开源社区的力,等等,帮你做到心里有数、有备无患,更好地规划好研发计划。
|
||||
|
||||
第二部分是核心模块开发篇。
|
||||
|
||||
低代码编辑器是低代码平台的核心模块,也是我们这个专栏的重点内容,占据了专栏的大部分篇幅,这部分我们会详细说明一个通用型低代码编辑器的技术实现要点。
|
||||
|
||||
我们整体以应用 App 开发三部曲(布局、交互、数据)为线索设计内容,我会从技术实现角度详细给出低代码编辑器的布局编辑器、属性编辑器、可视化编程编排、业务数据获取可能要用到途径等的实现方法。同时,这部分还覆盖了低代码编辑器的一些重要但容易被忽略的能力,包括多人协同编辑的支持、编辑历史管理、分支管理,甚至还包括如支持 Low Code(低代码)和 Pro Code(纯代码)混合开发等内容。
|
||||
|
||||
你会从具体的目录中看到,这一部分我会以低代码平台的代码生成器作为开始。这样安排,不仅是因为代码生成器是几乎任何一个低代码平台启动伊始就需要实现的功能,更是因为,多数人在开始实现代码生成器之前,不先考虑清楚它与低代码编辑器之间的关系就贸然动手,导致整个低代码平台的长期演进空间和拓展能力大大受限。
|
||||
|
||||
如果代码生成器与编辑器之间没有一个良好的松耦合关系,我们后面要提供插件扩展能力就很难了,而失去了插件的扩展性和定制性,会进一步导致通用型低代码平台在具体业务场景中的效率无法保证。失去了效率,低代码平台的效能等于打了半折。
|
||||
|
||||
第三部分是平台功能拓展篇。
|
||||
|
||||
这部分我们关注的是低代码平台开发能力之外的内容,主要包含了低代码在业务开发全生命周期各个环节中应该起到的作用,以及技术性、方向性建议。而且我们也会从技术实现方向,详细说明如何实现低代码平台的插件系统,从而实现低代码平台在具体业务场景中的定制、扩展,进而与业务团队一起形成一个低代码生态圈。
|
||||
|
||||
具体你可以看这张目录图:
|
||||
|
||||
|
||||
|
||||
到这里你可能会发现,似乎这三部分并不是这个专栏的全部。确实,这是动态更新的专栏,第一阶段更新完后的四年之内,我会以每年 5 讲的频率,继续更新,带你去看最新、最前沿的低代码技术动态。内容主要有这些方面:
|
||||
|
||||
增加低代码平台在 UX、需求端的能力的技术实现要点,以及在交付端的测试、运行能力的技术实现要点;
|
||||
|
||||
Awade 的新技术、新场景、新应用,我会精选参考价值较高的部分更新到专栏中,分享给你;
|
||||
|
||||
新业态剖析、相关开源技术实践与解析、新的调查机构报告解读等行业性内容。
|
||||
|
||||
|
||||
|
||||
在这漫长的征途中,我希望你可以和我、还有正在学习这门课的其他同学保持交流,分享你的学习心得和实践经验,并为课程未来的内容提供建议。
|
||||
|
||||
写在最后
|
||||
|
||||
我特别想说的一句话就是,探索低代码之路,我们才刚刚开始。
|
||||
|
||||
低代码是一个饱受两极化争议的技术方向,一方面大家对它有种种殷切期望,希望低代码能成为消除传统 Pro Code 的开发模式与高速增长的业务需求之间的矛盾,另一方面,低代码落地过程中出现的大大小小问题又很容易归咎于低代码,甚至怀疑低代码这个方向到底是对是错。
|
||||
|
||||
这里我想再引用前人的一句话,与你共勉:“虽然未来藏在迷雾中,叫人看来胆怯。但当你踏足其中,就会云开雾散。”
|
||||
|
||||
如果你依然对低代码抱有疑虑,我想请你踏进来,看看低代码平台真正的样子,理性地作出判断。如果你已经身处其中,是一线低代码架构者和践行者,我希望你有坚定的信念继续坚持下去,同时我也希望这个专栏的内容,能帮助你在低代码探索之路上少走弯路。并且,我将在未来至少 4 年的时间里坚持更新,在低代码的探索之路上,你,并不孤单!
|
||||
|
||||
如果你已经准备好踏上低代码的探索之旅,那么我们正文见!
|
||||
|
||||
|
||||
|
||||
|
210
专栏/说透低代码/01|低代码平台到底是什么样的?.md
Normal file
210
专栏/说透低代码/01|低代码平台到底是什么样的?.md
Normal file
@ -0,0 +1,210 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01|低代码平台到底是什么样的?
|
||||
今天我们正式开始了对低代码的学习。与某种具体技术不同,对于低代码的概念,业界至今没有达成一致意见(我估计以后也不会,这是低代码的职能所决定的)。
|
||||
|
||||
但作为低代码的学习者,甚至是架构者,我们需要对低代码平台到底是什么有一个清晰且深入的了解。这也就是我们第一节课的任务。这节课里,我会通过对低代码平台进行归类带你厘清低代码的概念,并带你分析当前低代码的发展现状,让你在脑海里建立起对低代码的直观印象。
|
||||
|
||||
正如开篇词所说,我们这门课的所有内容都侧重于低代码的架构、策略和技术的实现。所以,对低代码是啥理解得越清楚,相应地,你就越容易理解我所作出的架构和策略选择,以及为啥要采用特定的技术实现选型。反之,在概念理解有误的情况下,后续的内容有可能使你陷入目标与执行相互矛盾的困境,难以自拔。
|
||||
|
||||
什么是低代码
|
||||
|
||||
要讲清楚一个模糊的概念,一个有效的手段就是先应该尝试对它,以及相关的概念进行归类,然后比对,从比对中得出关键差异。
|
||||
|
||||
但要对低代码做分类,并不容易。由于低代码概念和内涵未达成一致,业界对它进行归类的方式也多种多样。这里,我以我理解的低代码的几个重要特征作为维度,对低代码进行归类,同时你也能通过这些分析,了解我们这门课要实现的低代码平台到底是啥样的。
|
||||
|
||||
按代码量的维度来分类
|
||||
|
||||
这个维度下,App 的开发模式可以分为三种:纯代码(Pro Code)、低代码(Low Code)、无代码(No Code)。
|
||||
|
||||
这三者有着巨大的差别,我们需要非常准确地将它们分开。纯代码是这个维度下的一个基准概念,它指的是用传统的手工编码的模式开发应用。而低代码和无代码比较容易搞混。
|
||||
|
||||
从中英文字面上说,无代码意味着 App 的开发过程没有代码参与。但是这样的理解比较粗浅,为了获取更加权威的理解,我尝试从头部分析机构 Forrester 和 Gartner 发布的报告中,查找与无代码相关的调查报告,但一无所获,不知道是不是这些头部机构并不认可无代码这个概念。
|
||||
|
||||
低代码模式下的 App 开发过程需要有代码参与,特别是面对一些复杂的业务逻辑的时候,通过表达式或者直接编码的方式来表达,反而更加清晰。而无代码模式开发 App 的全过程,没有任何代码的参与,不仅是从开发者角度看是这样的,从无代码内部的实现方式看,也是这样的。
|
||||
|
||||
严格来说,把采用无代码模式生成 App 的过程称为开发是不恰当的,因为它只是对已有原子业务能力进行二次组合,形成具有特定功能的新业务而已。因此从这个角度来说,低代码和无代码完全不是一种东西,切不可将这两者混为一谈。
|
||||
|
||||
但有一个情况非常容易混淆低代码和无代码。当低代码的成熟度到一定高度时,在某些细分场合下也可以实现零代码开发。在这个情况下,从 App 开发过程的表现看,这二者差异微小,此时最容易将两者混淆。当然,我们也不排除一些低代码解决方案提供商为了夸大其低代码的效果,故意将二者混为一谈,把无代码当作一个噱头来宣传。实际上,低代码模式要将一个场景做到零代码,难度非常大,并且有诸多业务前提。
|
||||
|
||||
在代码量这个维度下,我们专栏所说的低代码是指这 3 个分类中的“低代码(Low Code)”这一类。
|
||||
|
||||
按适用范围的维度来分类
|
||||
|
||||
这个维度下,低代码平台可以分为专用型和通用型两种。
|
||||
|
||||
所谓通用,指的是开发平台不事先假设自身只能应用在特定的场景、业务、行业,而是具有广泛的适用范围。
|
||||
|
||||
具有这样特征的开发平台往往需要有一个通用的底座。这个底座是纯技术性的,它不依赖于特定的业务功能,而只与业界广泛使用的标准协议、技术标准产生耦合。不过,这个时候,我们只有深入平台架构实现的细节,才能判断平台到底是低代码还是无代码,这就导致平台的使用者难以甄别(注意,我这里的目的不是想告诉你如何甄别,而是为了告诉你这门课所说的低代码平台具有的特征)。
|
||||
|
||||
但是,通用是有代价的,越通用就往往意味着在特定业务场景下的效率越低,越通用就意味着默认配置里的个性化信息越少,为形成某个具体场景所需的配置量就越大,从这个具体场景的角度看,效率相应也就越低。
|
||||
|
||||
所以通用型的低代码平台往往伴生着这个特征:有相对完善的有插件(或类似)机制。这一点相对来说比较好识别,相对高通用性的技术底座来说,插件是廉价的,因此通用性低代码平台往往会有数量众多的插件。这些插件可以定制出各式各样具体的业务场景,通过插件的定制化和扩展性来解决效率问题。
|
||||
|
||||
这个维度下,这门课所说的低代码指的是通用型开发平台,它具有一个通用性非常高的底座,和一个相对完善的插件机制。
|
||||
|
||||
按输出的 App 的类型来分类
|
||||
|
||||
其实,在一个具有较高通用适用范围的低代码平台来说,按照输出 App 类型分类几乎是没有意义的。之所以不得不按输出 App 类型分类,是因为开发平台的通用性不足,而在有了足够高的通用适用性之后,支持开发各种类型 App 的问题,就不在于能不能了,而只是时间问题。
|
||||
|
||||
尽管我们这门课所说的低代码指的是“通用型”这一类,但这并不影响我们看看现在业界其他低代码平台都可以输出哪些类型的 App,大概有流程驱动型、表单驱动型、模型驱动(ORM)型、BI 分析类型这几种,具体你可以看看这张表格(5 星为满分):
|
||||
|
||||
|
||||
|
||||
这里,我主要给你区分一下表单驱动型和模型驱动型这两个类型,因为它们比较容易混淆。
|
||||
|
||||
所谓模型驱动型 App,它的模型指的是数据模型,或是数据关系。而这里所说的关系,指的就是符合三范式的关系型数据库的关系,也就是你数据库中各个数据表之间的关系,比如表 1 的 a 字段和表 2 的 a 字段是相同的,但与表 3 中的 a 字段没有关系。在正确配置了各种数据关系之后(这个过程一般称为数据建模),页面上就可以很容易创建各种 CRUD(增删改查)类 App 了。
|
||||
|
||||
表单类 App 则是仅以数据为中心,创造各种表单来收集或呈现数据。这里的关键点在于,这类 App 并不关注数据之间的关系。所以表单类的 App 非常容易形成数据孤岛,并存在大量冗余数据,以及大量数据不一致性等问题。如果我们将表单类 App 做得比较完善的话,实际上它就会逐渐转型成模型驱动类 App 了。在完成数据建模之后,我们就分不清楚它到底是模型驱动还是表单驱动了,差异只是前端是用表单展示,还是表格展示而已。
|
||||
|
||||
按使用者的类型来分类
|
||||
|
||||
如果按照使用者的类型进行分类,我们可以将开发平台的使用者分为 3 类:专业技术人员,业务技术员,相关无专业技能人员。
|
||||
|
||||
这里所说的业务技术员是一种正在兴起的角色,它是指构建供内部和外部业务使用的技术或分析功能的非 IT 部门员工。他们担任着装备和赋能非 IT 资源以构建数字化能力的战略角色。
|
||||
|
||||
|
||||
根据 Gartner 的研究:41% 的员工可以被称为业务技术人员,不过这一比例在不同行业可能存在很大差异。例如在政府部门等技术密集度较低的行业,这一比例接近 25%,但在能源等 IT 密集型行业,这一比例接近 50%。
|
||||
|
||||
|
||||
多数的无代码开发平台将业务技术员作为主要的用户群,为他们提供对已有业务的二次组合为主的基础开发能力,一般具有专业技能的开发人员是不会使用无代码开发平台的,因为专业技能者要面对的问题域已经大大超出了无代码平台的能力范围。
|
||||
|
||||
而低代码开发平台一般会将专业技术人员和业务技术员同时作为他们的客户群,并以专业技术人员为主要用户群,业务技术员为次要用户群。
|
||||
|
||||
随着低代码开发平台的成熟度上升,业务技术员用户群的占比会有所上升。因为成熟度高的低代码平台,不仅有各式各样的可视化工具来降低业务研发的难度和代码量,同时对业务研发生命周期各个环节的覆盖也会越来越完整。从开发到测试,从测试到上线,再到高容错运行时自动化部署 / 恢复、运行时自动化运维等各个环节的可视化、自动化完成,这为无 IT 技能的业务技术员独立开发提供了可能性。同时,越发完善的可视化自动化能力不仅会牢牢抓住已有的专业技能用户,还会吸引更多的专业技能用户的加入。
|
||||
|
||||
这个维度下,这门课所说的低代码是以专业技术人员为主要用户群的一类平台。不过,在写这篇文稿的时候,我负责的低代码平台正在努力将业务技术员纳入到它的用户群中,但是这项工作才刚起步不久,当前尚没有特别成熟的经验可以分享。但这是一个动态专栏,在未来几年还会保持更新,我会在合适时机,及时把我在拓展更多用户群方面的经验分享给你。
|
||||
|
||||
|
||||
|
||||
现在我们来总结一下,我们这门课要实现的低代码平台到底是怎么样的。它是一个以专业技术人员为主要用户群的通用型低代码平台,它会有一个通用性非常高的底座,和一个相对完善的插件机制。
|
||||
|
||||
我这里还要再解释一下,在后续的内容中,我可能还会提到低代码工具和低代码平台,对于这两个概念,我所指的内涵是一致的,区别就在于规模和成熟度。低代码工具指代规模较小、成熟度较低的低代码实现,而低代码平台则指代规模较大、功能较完善、程度较高的低代码实现。
|
||||
|
||||
了解了行业内对低代码的几个分类,以及我们这门课的低代码平台的定义后,我们再来简单看看低代码的历史演进和现状,让你对低代码和低代码行业有更进一步的理解。
|
||||
|
||||
低代码的发展
|
||||
|
||||
在低代码的发展上,我们可以从基础设施的演进、时间和地域,以及中台的演进这三个方面一探究竟。
|
||||
|
||||
我们先从基础设施演进看低代码的发展,你可以先看看下面这张图:
|
||||
|
||||
|
||||
|
||||
长久以来,软件的基础设施都是纯物理设备,当虚拟技术引进后,IaaS(基础设施即服务)时代就开始了。紧随着虚拟技术继续蓬勃发展,没过多久,软件技术便历经了 PaaS(平台即服务)时代、SaaS(软件即服务)时代。关于这几个概念更具体的解释,你可以看下补充材料的内容。
|
||||
|
||||
SaaS 类产品高度封装的软件服务为行业提供了巨大便利的同时,人们也渐渐发现这种形式的短板:定制性太弱。因此在 SaaS 的基础上,又演进出了一种被称为 aPaaS 的软件服务体系。
|
||||
|
||||
根据 Gartner 的说法,aPaaS 是应用程序平台即服务的缩写,它是一种云服务,可为应用程序服务提供开发和部署环境。aPaaS 平台提供的功能包括:迭代构建应用程序、即时提供应用软件、按需扩展应用程序,以及集成应用程序与其他服务。
|
||||
|
||||
很明显,Gartner 把这里的 a 作为 application 理解了。但我个人认为,这里的 a 当做 ability 来理解更为恰当,借用文言文的使动用法,将它翻译为赋能。因为很明显的,相比其他架构,aPaaS 体系多出了开发和部署应用程序的能力,也就是说,aPaaS 赋予了原来的软件服务体系开发和部署的能力。
|
||||
|
||||
我们再换个角度,从时间和地域来看低代码的发展。下面这张来自艾瑞咨询的图片总结了这个过程,图中信息量比较大,你可以点开仔细阅读。
|
||||
|
||||
|
||||
|
||||
数据来艾瑞咨询2021年低代码行业研究报告
|
||||
|
||||
我们可以从这组比对数据中明显看到,国内的低代码平台要落后于美国一个时代。现在低代码头部解决方案中已经有类似 OutSystem、Microsoft 这样的通用型低代码巨无霸,而国内多数提供商还在探索如何有效地为某个垂直行业、细分领域提供低代码服务。但这对你我这样的低代码人来说,实际上是一个好事,这仍是一片蓝海,大有可为。
|
||||
|
||||
第三种角度就是从中台演进来看低代码的发展。这里你可能会觉得很奇怪,为啥低代码又和中台扯到一起了呢?
|
||||
|
||||
这是因为,低代码可以将多个“烟囱系统”归整为一个集大成者,更灵活敏捷地创建中台架构。在传统的企业系统中,每个部门有不同的系统需求,于是会各自采购自己的系统。但这些系统彼此孤立,独立运作,导致企业采购的软件系统冗杂。而低代码平台能让绝大部分部门的业务系统都能在一个平台里搭建,彼此联系,打破信息系统孤岛,同时降本增效,提升内部生产力。
|
||||
|
||||
低代码有助于横向打破传统企业的烟囱系统,将它们串联到一起,这与中台的目标不约而同。此外,低代码对外赋能的职能,也是中台建设目标之一。因此中台的发展过程,有相当一部分线路与低代码是重合的,二者可以起到相互促进,良性共生的关系。所以,如果你所在的企业同时在架构中台和低代码,不妨尝试将它们放到一起来考虑。
|
||||
|
||||
行业状态速读
|
||||
|
||||
了解了低代码的发展和演进之后,作为低代码的研究者,我们总得关心下当前低代码的行业现状吧?
|
||||
|
||||
不过,网上这方面的信息实在太多了,多数说的有鼻子有眼,但不知道真假,所以我只看专业调查机构输出的报告。其中我主要关注 Forrester 和 Gartner,以及国内的艾瑞咨询,相关的报告链接我都统一附在了文末的补充材料中。
|
||||
|
||||
在这么多报告里面,我首先要向你推荐的就是 Gartner 绘制的关于低代码的魔力四象限报告,关键部分就是下面这张图,概括性非常强。
|
||||
|
||||
|
||||
|
||||
作为低代码的实现者,一般看这种报告都是以竞品调研为目的的,因此我们一般只研究 Leader 象限里的提供商就可以了。Leaders 这个象限显示的是技术能力较强、对未来的规划很清晰的厂商,其产品被市场广泛认可,对我们有极强的参考价值。
|
||||
|
||||
其次我想向你推荐的是 Forrester 的 Forrester Wave™ 报告。与分析 Garter 的魔力四象限相似,我们仍以 Leader 这一波里的厂家作为我们的调研对象。与魔力四象限的结果比对,你发现了啥?
|
||||
|
||||
|
||||
|
||||
两家机构对低代码的 Leaders 给出了几乎一样的结论,对吧?在 Leaders 里,头部机构取得了一致意见。这两份报告为我们低代码平台的竞品调研给出了一个非常明确的指引,所以如果你现在还在头疼不知道如何下手做调研的话,他们就是极佳的研究和参考对象。
|
||||
|
||||
那么国内的厂商是啥样的状态呢?
|
||||
|
||||
我同样有两份报告可以推荐给你:一个来自 Forrester 的报告《The State Of Low-Code Platforms In China》(下文简称中国报告),另一个来自艾瑞咨询的《艾瑞咨询 -2021 年低代码行业研究报告》(下文简称艾瑞报告),你可以在这一讲的补充材料中找到原文。
|
||||
|
||||
在《中国报告》中,Forrester 第一次将视角聚焦在中国,它认为,低代码目前在国内主要应用于银行、保险、零售、医疗、政府、制造、电信和建筑行业。Forrester 认为,国内低代码目前主要集中在如下 9 个领域,分别有:
|
||||
|
||||
|
||||
|
||||
而《艾瑞报告》的信息量就更大了,主要包含了概念界定、应用场景、竞争要素、市场规模、趋势洞察四大块的内容。下图是《艾瑞报告》绘制的低代码厂商图谱,非常概要地整理出了国内外低代码厂商的分类。
|
||||
|
||||
|
||||
|
||||
大体上,《艾瑞报告》把低代码厂商分成了通用型和垂直型两种,垂直型和我前文所说的专用型是类似的,均指只能应用在某个业务领域的低代码解决方案,无法运用到其他领域。
|
||||
|
||||
无论你是要做竞调,还是打算采购,这个图都可以提供不错的指引。
|
||||
|
||||
大小厂商这么多,也从一个侧面反映了低代码在国内的发展仍处于早期的状态,按照“惯例”,风口褪去后,各个厂商会快速聚集,要么大鱼吃小鱼、要么抱团取暖,形成寡头化的局面,当前还处于“百花齐放”的状态,说明低代码仍处于投资风口,风投时不时来“奶”上一口,所以大家都还能坚持得住。
|
||||
|
||||
不过,这份《艾瑞报告》是 2021 年 3 月出的,有点老了。目前我和负责竞调的团队还没找到新版,一旦获得一手信息,在商业合规的前提下,我会在这门课的动态更新部分中第一时间分享给你。
|
||||
|
||||
小结
|
||||
|
||||
这一讲从多个维度对低代码做了分类,并简要讨论了各个分类的低代码所具有的特征,这些分类方法和特征的讨论,对帮助你理解和总结网上对低代码的各种讨论,会有莫大的帮助:
|
||||
|
||||
|
||||
按代码量的维度,App 的开发模式可分为纯代码、低代码、无代码,其中低代码主要特征是 App 开发过程,平台按需开放表达式和编码等编辑入口,无代码则是对已有业务做二次组合;
|
||||
按适用范围,低代码平台可以分为专用型和通用型,通用型平台有通用低代码底座,可满足大多数业务开发需要,通用型平台一般提供插件方式来提升其在特定场景下的效率和易用性;
|
||||
目前各低代码平台可以输出的 App 类型汇总大概有:流程驱动型、表单驱动型、模型驱动(ORM)型、BI 分析类型,通用型低代码平台不局限输出某种类型的 App,而专用型低代码平台则一般专注其中的一种;
|
||||
低代码平台的使用者可以分为有专业技术能力者和业务技术员,有的低代码平台同时支持两种用户同时使用,有的则专注于为其一提供服务。
|
||||
|
||||
|
||||
更具体的你可以看下这张脑图:
|
||||
|
||||
|
||||
|
||||
现在的状况是大家把低代码当做一只大框,啥业务开发只要能少写两行代码的,都往这个框里扔,都说是低代码。这造成了不同的人带着不同的业务背景来理解低代码,进而得到差异很大的结论,甚至连啥不能算作低代码都说不清楚。我希望通过不同维度分类的方式,来帮助你对低代码的能力、职能、目标等有一定的认知。
|
||||
|
||||
最后的两个小节,主要是结合了我的经验,对低代码的发展和行业状态做了一些总结。当了一名数据搬运工,把我认为可信度较高、参考价值较高的几份机构调查报告汇总出来,便于你研读和参考。这部分的篇幅不长,但信息量很大,相信对你会有帮助。
|
||||
|
||||
思考题
|
||||
|
||||
除了代码量、使用范围、输出应用类型、使用者等维度之外,你认为还可以从其他哪些维度对低代码做分类?可以分为哪些类型?各有啥特点?
|
||||
|
||||
欢迎在留言区分享你的看法。我们下节课见。
|
||||
|
||||
补充材料
|
||||
|
||||
关于基础设施演进的几个概念:
|
||||
|
||||
IaaS:Infrastructure as a Service,是提供消费者处理、储存、网络以及各种基础运算资源,以部署与执行操作系统或应用程序等各种软件。
|
||||
|
||||
PaaS:Platform as a Service,平台即服务,将软件研发的平台做为一种服务提供给消费者。
|
||||
|
||||
SaaS:Software as a Service 软件即服务,也可称为“按需即用软件”(即“一经要求,即可使用”),它是一种软件交付模式。在这种交付模式中,软件仅需通过网络,不须经过传统的安装步骤即可使用,软件及其相关的数据集中托管于云端服务。用户通常使用精简客户端,一般即经由网页浏览器来访问、访问软件即服务。
|
||||
|
||||
aPaaS:application Platform as a Service 应用程序平台即服务的缩写,它是一种云服务,可为应用程序服务提供开发和部署环境。
|
||||
|
||||
关于 Gartner 的魔力四象限:把研究对象分为四类,分别是领导者,有远见者,挑战者,利基企业,通过归类可以快速了解被研究对象所在行业的状态,更多信息可以看这个文章。
|
||||
|
||||
关于 Forrester Wave 报告:和 Gartner 的魔力四象限相似,也是对被研究对象分成若干类,更多信息可以看这个文章。
|
||||
|
||||
《The Forrester Wave™: Low-Code Development Platforms For Professional Developers, Q2 2021》原文在这里。
|
||||
|
||||
《The Forrester Wave™: Low-Code Development Platforms For Professional Developers, Q4 2021》原文在这里。
|
||||
|
||||
《The State Of Low-Code Platforms In China》原文在这里。
|
||||
|
||||
《艾瑞咨询 -2021 年低代码行业研究报告:化繁为简》原文在这里。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
137
专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md
Normal file
137
专栏/说透低代码/02|低代码到底是银弹,还是行业毒瘤?.md
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02|低代码到底是银弹,还是行业毒瘤?
|
||||
说到低代码,有人说它是毒瘤,也有人说它是银弹。那到底应该怎么看呢?这就是我们今天要解决的核心问题。
|
||||
|
||||
先上结论:存在即合理。
|
||||
|
||||
这里的“存在”包括两个角度:一是银弹论,二是毒瘤论,无论从哪个角度看,既然存在这样的论调,就有它们的合理性。
|
||||
|
||||
我们暂时不介入这两个言论的细节,而是先把关注点移到低代码本身,先回答这个问题:低代码到底是要革程序员的命?还是成为程序员工具箱里的另一个工具?
|
||||
|
||||
如果你觉得低代码的目的是为了革程序员的命,是要把程序员的手脚给捆住,是要束缚程序员的创造性,是要把复杂的现实世界强制机械化、特例化、流程化,那么大概率你会接受毒瘤论。你甚至会毫无保留地否定低代码,认为 Low Code 除了 Low 以外,不会有 Code。
|
||||
|
||||
毕竟,变革是有成本的,而且往往成本巨大,无论是一个人还是一个团队,保持惯性,留恋舒适区才是正常的。我们几十年来用的软件从来都是双手敲代码创造出来,抛弃这个已被无数次证实可行的方法,拥抱一个“饱受争议”的新方法,这本身就需要有莫大的勇气,也需要承受风险。
|
||||
|
||||
虽然会有很多勇于探索、期望尝试新方法的人和团队,也有很多受困于 Pro Code(手敲代码的方式)的各种痛点被迫自我变革的人和团队,但是更多的人和团队是倾向于保持现状的,即使嘴上不说,身体却很诚实。每次回顾会都能复盘出一堆的代码问题,然后一而再、再而三地立 flag 说要改进,但多数会给出各种借口说明问题无解,然后继续这样的循环。有惯性就有排斥,有排斥就有各种负面论调,毒瘤论是其中典型的代表。
|
||||
|
||||
Pro Code 创造的软件更“香”吗?
|
||||
|
||||
如果能换个角度,把低代码当做程序员工具箱里的另一个工具,我们再次审视银弹论和毒瘤论的冲突时,会发现一些不同点。工具不会革谁的命,只会让大家的日子更好过。工具有好有坏,不好用时就丢弃,好用时就多用用。
|
||||
|
||||
条条大路通罗马,我们前往罗马除了传统的步行、马车外,还能开汽车、坐飞机,甚至也可以在路况好的时候开汽车,风景好的时候下车漫步抑或纵马驰骋,只要能按时到达罗马,我相信没人会太关注过程,所以在去罗马的旅途只要你开心就好。
|
||||
|
||||
开发软件写 bug 也是类似的,Pro Code 方式创造的软件不会比其他方式生产出来的软件更香、不会卖更多的米,因为软件的用户完全不关注软件是怎么被创造出来的。
|
||||
|
||||
当然,软件研发里的“罗马”不仅仅是一个目的地,所以这里带上双引号了。在软件业里,“罗马”除了指代交付的业务功能外,它的内涵还有:交付物可维护性、可扩展性、可测试性、交付过程的成本,甚至是团队的新陈代谢等等许多因素。同样地,我们审视到底是“骑马”好,还是“开车”好的时候,需要关注的不仅仅是心情,而需要关注到所采用的方式是否能在各个方面都有比较好的表现。这是一个极复杂的评判过程,且各个团队有各自的侧重,没有标准答案。
|
||||
|
||||
即使评判过程如此复杂,但是有一点是明确的(甚至早已是业界共识):Pro Code 不是软件开发的银弹,要不然就不会有低代码等多种新方式被提出来甚至展开实践了。
|
||||
|
||||
Pro Code 第一个问题是门槛高。虽然我们自贬为“码农”,但是根据 GitHub 的统计数据,去年国内只有大约 755 万多开发人员,一个人可能只会写 hello world 就被算进来,摊到各个细分研发领域后,人数就少得可怜了。我去年面试了几十人,但是最终入职的寥寥无几,招聘难成了我的痛点,我甚至将其写入年终报告。
|
||||
|
||||
Pro Code 第二个问题是跨界难。虽然都是写代码的,但是 Java 程序员可能很难玩得转 C/C++,前端程序员很难玩得转 Java/Scala 等后端技术,反之亦然。
|
||||
|
||||
一个典型例子是:全栈这个词是在在 Node.js 火热起来之后才被发明出来的,在这之前,前后端通吃的只能是极少数顶尖骨干的专属。但是,即使现在有了 node.js 实现前后端跨界,我们跨越到其他领域依然困难。总之,即使在具体业务场景下,要端到端交付一个完整业务,对一个人,甚至一个团队来说,都不是一件简单的事情。
|
||||
|
||||
第三,代码编写只是第一步,之后还有许多问题需要解决。像代码所依赖的第三方库的开源合规治理、第三方和己方的代码安全漏洞检测和治理,还有代码性能、代码测试、运行时运维等,这些工作不是难度大,就是繁琐。最后,为了对抗代码库的熵增,避免代码仓库越来越混乱,越来越难维护,还必须引入代码走查机制,让经验丰富的程序员来把关。
|
||||
|
||||
Pro Code 存在的问题显然不止这些,但这些已经足够说明问题了。
|
||||
|
||||
既然 Pro Code 有如此多的问题,而且许多问题是由于代码自身导致的,那么引入一些工具来降低代码量,许多问题也就可以缓解甚至解决了。代码本身并无直接价值,业务才具有直接价值,从来没听说过哪家公司是凭着一个百万千万行代码 repo 作为资产上市的。公司之所以能上市肯定是由于资本市场认可它的业务价值,代码只不过是用于实现业务的一种场常见方式而已,但绝对不是唯一的。
|
||||
|
||||
总之,不管是白猫黑猫,能抓老鼠的就是好猫,吃喝拉撒越少的猫则是更好的猫。只要能实现相似价值的业务,并且承载该业务的方式的副作用比代码要少,那么这就是一种更好的方式。
|
||||
|
||||
到这儿,我相信你已经有足够的理性来把低代码作为一种工具来看待,而不认为这是一种程序员自我革命的手段。
|
||||
|
||||
Low Code 银弹论合理吗?
|
||||
|
||||
既然 Pro Code 不是银弹,那 Low Code 是不是银弹呢?当然也不是银弹。
|
||||
|
||||
在理解这一点之前,我们先来搞清楚 Pro Code 和 Low Code 的区别。为了说清楚这两者的异同,我要引入“第三者”No Code,我们把这三个看起来很相似的概念放一起比较。
|
||||
|
||||
Pro Code 和 No Code 实际上都很好理解,一个是纯代码,一个是无代码。假设 Pro Code 的代码量是 100,那 No Code 就是 0,所以 Pro Code 和 No Code 是截然不同的,甚至你可以认为这两者毫无关系。No Code 的最典型形态莫过于 SaaS 类的产品了。
|
||||
|
||||
那 Low Code 应该摆在哪个位置呢?似乎应该介于 0 到 100 之间?50?80?其实都不是的,关键点不在于多少,而在于有没有!
|
||||
|
||||
Low Code 当然是有代码的,所以它和 Pro Code 是一路货色,从创建业务价值的最根本上说,它们是一样的,都是通过代码来创建业务价值。而 No Code 则不是,它只是对已有业务的二次组合来创建业务价值,No Code 创建业务的全过程都没有源码的参与。
|
||||
|
||||
那 Pro Code 和 Low Code 的差异在哪呢?我认为本质差异在于源码在这两者创造业务价值的过程中所扮演的角色。Pro Code 是把代码当作关键输入来创建业务的,Low Code 则不是,它的输入是一些结构化的数据。
|
||||
|
||||
Low Code 工具有能力将结构化数据生成为源码,然后再采用与 Pro Code 相同的方式将源码转为业务能力。很显然的一点是,Low Code 把源码当做中间产物,而 Pro Code 则将源码做为关键输入。相信现在你应该可以分清楚 Pro Code、Low Code、No Code 之间的差异了。
|
||||
|
||||
那么 Low Code 为啥也不是银弹呢?关键就在于,Low Code 是采用无逻辑的结构化数据来描述业务的,对于相同业务,Low Code 的描述能力要弱于有逻辑的 Pro Code。所以,Pro Code 都做不到的事情,Low Code 当然也做不到。
|
||||
|
||||
根据前面与 Pro Code 的比对,我们可以看出,Low Code 对业务的描述能力既弱于 Pro Code,也与 Pro Code 没有实质差异,看起来 Low Code 一无是处啊。我认为这可能是低代码毒瘤论的理论基础。别急,继续往下看。
|
||||
|
||||
Pro Code 开发方式是指令式的,它的开发过程是告诉计算机如何一步步实现某个业务。Low Code 则不然,它的开发过程是不停地在描述和细化这个业务最终的样子。
|
||||
|
||||
可以说,Low Code 的开发人员的思维方式与 Pro Code 的开发人员完全不一样,他们始终在思考这个业务应该是啥样的,而 Pro Code 的开发人员则是在开发过程中不停地思考如何将业务翻译成一条条指令。其中关键的一点是,对于 Pro Code 开发人员来说,这个业务最终的样子不是天上掉下来的,抑或大风刮来的,而是要在他们着手翻译成指令之前先想清楚的。
|
||||
|
||||
所以结论就是,Low Code 开发人员要比 Pro Code 开发人员少做一个事情:无需将业务翻译成指令,从而他们得以用更高的效率实现相同的业务,在单位时间内创造更多业务价值。
|
||||
|
||||
低代码模式除了效率外,还有另一个卖点,那就是低技术门槛。它可以赋能更多人,使其可以快速参与业务开发。没错,就是实现外卷!
|
||||
|
||||
根据我们前面说的,Low Code 依然有代码,但 70%~90% 甚至更多比例的代码是自动生成的。这里要特别注意的是,其中 100% 的框架性的代码都是自动生成的,这部分代码不但与业务无直接关系,即没有直接业务价值,还是最难开发的,一出问题都是大问题。而剩余的小部分需要开发者填写的代码,则基本上都是表达式,或者部分复杂业务逻辑。
|
||||
|
||||
表达式也好,强业务逻辑代码也好,都是在直接实现业务功能,所以即使在低代码平台上写代码,那也是在直接实现业务逻辑。那些只有业务能力但无开发能力的人、需要写 UI 的后端开发,或者是需要写业务的前端开发等人群,都可以在低代码平台上实现跨界开发,实现端到端的业务交付能力。赋予原本没有技术能力的人快速参与的能力,是低代码的另一个重要能力。
|
||||
|
||||
基于以上提效和赋能两点,我们有足够的理由来认为低代码银弹论是合理的,银弹论并非一群“不务正业”、“整天想着再造一个轮子”的人的自嗨。
|
||||
|
||||
Low Code 离银弹还有多远?
|
||||
|
||||
低代码毒瘤论者只看到表面,或者思考深度不足,没能从实质上看到 Pro Code 和 Low Code 的差异。即使如此,我也认为毒瘤论是有合理性的。毕竟很多实现 Low Code 的人自己也没想明白就匆匆动手,为了达到少代码(而非低代码)的目的而做出各种各样无理约束,你不能这个、不能那个,最终不但没能发挥出低代码的优势,反而降低了效率,被他人诟病。
|
||||
|
||||
即使抛开这类情形不说,Low Code 本身的成熟度也需要改进,一个重要改进点是:如何有效解决强逻辑场合下的可视化配置方法,这是 Low Code 采用无逻辑的结构化数据描述业务常见的重要短板。这个问题至今也在困扰着我,我认为我们目前给出的解决方案并非最优解,仍然需要孜孜不倦地探索出更合适的手段。
|
||||
|
||||
低代码要真正成为银弹,除了解决强逻辑的可视化配置外,要做的事情还有很多。有一部分我在前文已经有过描述了,这里再给你简单总结和拓展一下,一方面是为了用作毒瘤论非合理性的额外论据,另一方面也是为了给这个专栏后续的内容埋点伏笔。
|
||||
|
||||
首先,快速部署能力是要有的,最基本的需要一键导出所有运行时需要的文件,最好能做到一键部署和运行时,这样的能力将给业务的小伙伴提供巨大的便利,大幅减少他们的试错时间。
|
||||
|
||||
其次,也是最重要的,低代码平台还需要关注生成的 App 的可测试性。如果生成的 App 是一个黑盒,出错时无从下手,日志打印惜字如金,要是这时候平台再对业务团队的困境置之不理甚至推诿,那无论是谁都会厌恶这样的开发方式的。
|
||||
|
||||
可测试性是一个非常综合的能力,从最基本的部署运行开始,到丰富准确的日志,再到直接给予业务团队调试支持,甚至到直接提供自动化测试的能力,这里头涉及的不仅是技术,还有团队间如何协作的问题。
|
||||
|
||||
还有一个容易被无视的能力是,低代码平台需要支持多人协作开发,当业务越来越复杂,多人协作是刚需。这个问题往往在平台建设初期就容易被无视,在后期随着应用的复杂度上升后才被提出,这时候再去实现,成本会非常大。此时平台有可能会简单粗暴地采用互斥的方式来掩耳盗铃。
|
||||
|
||||
要知道,业务团队往往面临巨大的交付工期压力,如果小李无法在小王编辑时上手,一急之下他们可能将数据复制一份来同时编辑,结果发现无法手工合并结构化数据,这样的情形放谁身上都会抓狂。所以支持多人协作开发是低代码平台非常必要的一个能力,而这背后其实还隐藏着编辑历史管理、分支管理等潜在需求,这些都对业务团队多人协作效率有很大的影响。
|
||||
|
||||
再者,兜底能力也是很重要的。低代码平台不可能面面俱到,它总有能力边界,但这个能力边界不能束缚业务团队的探索。业务需要紧随市场甚至引领市场,而市场是千变万化的,任何公司都无法决定,所以要把“业务提出低代码平台能力之外的需求”当做一种常态。此时,低代码平台需要有一种策略帮助应用快速实现需求,哪怕直接上手编码乃至 Hack。这样的策略就是兜底策略。
|
||||
|
||||
此外还有一些增值功能,包括 UX 设计规范自动对齐、提供 UX 设计稿转代码(D2C)能力、App 的可维护性、App 的埋点 & 数据采集、App 的开源合规治理、App 的安全漏洞治理、App 的性能等等。简单来说,这些都是低代码平台的亮点能力,并且是拉开与 Pro Code 差距的重要能力。
|
||||
|
||||
讲到这里我们可以看到,低代码平台只关注开发能力是远远不够的。而毒瘤论的另一个重要基础就是来自于许多低代码平台只注重开发能力,而无视业务交付过程的其他能力,特别是对 App 可测试性的漠视。
|
||||
|
||||
作为低代码的坚定支持者和践行者,我当然不同意低代码是毒瘤这个论断,但是我不会轻易去喷这些言论,而是努力去理解和挖掘毒瘤论的言论基础,从中分析出那些容易被我们无视或者轻视的需求。只要我们常常能细心、谦虚地观察业务开发过程中的方方面面,并及时发现业务团队的痛点和需求并及时解决,以一种服务者的态度来为业务提供服务,我相信,毒瘤论会渐渐消散的。
|
||||
|
||||
总结
|
||||
|
||||
今天这一讲到这里就结束了。其实很少有一种技术像低代码这样同时被两种极端言论评价,今天我们从一个理性的角度审视了这两种言论,并尝试去理解它们的思路和痛点。
|
||||
|
||||
我们从 Pro Code、Low Code、No Code 的对比中了解到 Pro Code 与 Low Code 的关键差异不在于代码量的多少,而在于:
|
||||
|
||||
|
||||
代码在这两者创造业务价值的过程中所扮演的角色,对 Pro Code 来说,代码是关键输入,而对 Low Code 来说,代码仅仅是中间产物、是副产品;
|
||||
|
||||
使用 Low Code 开发虽然也需要少量编码,但是基本上都是在填写表达式,直接实现业务价值,与业务价值无关的代码则几乎全都被自动生成;
|
||||
|
||||
|
||||
使用 Low Code 开发业务的人几乎时时刻刻都在描述和细化业务最终的样子,使用 Pro Code 的开发人员不仅需要思考业务最终的样子,还要将其翻译成一条条计算机指令。
|
||||
|
||||
这些差异正是 Low Code 的比较优势,这也是为啥 Low Code 可以帮助业务提效和赋能的原因:无需长篇累牍地编码可以大幅降低使用的门槛,实际上就是对无编码技能者进行赋能;几乎所有框架性、非功能代码全部自动生成,以及无需将业务翻译成一条条计算机指令,可以大幅压缩开发周期,实际上就是在提效。
|
||||
|
||||
但即使如此,当下的 Low Code 也不是银弹,因为我们在分析毒瘤论的过程中意识到 Low Code 除了注重开发能力之外,还应该具有诸多其他能力以覆盖业务研发全生命周期,包括一键导出和部署、较高的可测试性甚至自动化测试能力、多人协作、兜底能力等等,若低代码平台不具备这些能力,则很难发挥低代码技术的优势。
|
||||
|
||||
当然,我们也发现了 Low Code 带来了许多增值功能,包括自动对齐 UX 设计规范、UX 设计稿转代码(D2C)能力、App 的埋点 & 数据采集、开源合规治理、安全漏洞治理等等,这些功能是拉开 Pro Code 差距的重要抓手。
|
||||
|
||||
思考题
|
||||
|
||||
低代码平台是否只需把开发能力发挥到极致就可以了?除了开发能力之外,低代码平台还需要注重哪些能力的建设?你认为其中最重要的是哪些?欢迎在评论区留言。
|
||||
|
||||
我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
122
专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md
Normal file
122
专栏/说透低代码/03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?.md
Normal file
@ -0,0 +1,122 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03|低代码的天花板:一个完备的低代码平台应该具备哪些条件?
|
||||
这一讲我们来探一探低代码的天花板到底在哪儿,也就是从多角度看看,一个完备的低代码平台到底是啥样的。
|
||||
|
||||
如果你已经在构建低代码的路上,且自认为干得不错,我想给你描绘一些新的目标,帮助你做得更好,或者至少帮助你了解接下来还能有哪些值得去做的事情。
|
||||
|
||||
如果你还未开始构建工作,或者刚刚上路,我想给你一个更加具体的目标,这样你可以更加精确地估计出,如果达成这样的目标,你需要投入多少资源(人力 / 时间)。当然,不是非要触及天花板的低代码平台 / 工具,才有生产力,即使你知悉了天花板在哪,也不一定非要去摸一摸,量力而行,适合自己的才是最好的。
|
||||
|
||||
这节课,我会从适用领域、适用人员、与基础设施和谐相处、全生命周期这几个角度来说明一个完备的低代码平台的模样。为了避免啰嗦,我们这一讲讨论的“低代码平台”都特指处于天花板中的低代码平台。
|
||||
|
||||
适用领域
|
||||
|
||||
低代码平台需要同时支持以数据为中心和以流程为中心的开发模式。多数企业的软件基本上都是围绕着数据或流程打造的,因此对低代码平台的适用性提出了极高的要求,要求它能够同时满足多数企业的业务需求的开发。
|
||||
|
||||
不同企业的业务复杂程度,显然是不一样的。有的企业的业务复杂度非常高,有的企业则相反,比较简单,但简单的业务往往伴随着数量庞大的特征。
|
||||
|
||||
那怎么支撑不同复杂度的业务呢?这一点上,我们要求低代码平台对于简单的业务,需要提供简洁的开发实现方式,并且需要有非常高效的开发效率,从而可以更好地应对简单业务在数量方面的需求。对于复杂度高的业务,低代码平台需要能提供良好的分步实施策略、自然的衔接方式,同时也要充分考虑并提供兜底策略。
|
||||
|
||||
在我看来,兜底策略是应对复杂业务的一个非常有用的方法。当业务的复杂度超过低代码平台的能力边界,或者业务复杂度过高,使得可视化模式带来的效率 / 易用性收益低于 Pro Code 模式的临界值的时候,低代码平台应该能提供一种方法,允许业务开发人员回退到传统 Pro Code 模式继续开发。并且,这个回退过程应该非常自然 / 内聚、同时尽可能地不影响其他功能的开发。
|
||||
|
||||
兜底策略不是一种通用的架构方法,而是一种思路一种策略,它应该在低代码平台各个功能点的架构和开发过程中因地制宜地被实现出来。如果更加极端地运用兜底策略,你会发现我们甚至可以要求低代码平台完全回退到 Pro Code 模式。
|
||||
|
||||
实现兜底策略看似难度非常高,但事实并非如此。得益于低代码的关键特征,也就是低代码是有代码的,低代码平台能很好地处理好 Pro Code 模式和可视化开发模式之间的关系。所以即使完全回退到 Pro Code 模式,对平台来说,不但问题不大,反而平台要做的事情会更少。因为在这个模式下,代码是人工产生的,而非低代码平台自动生成的!
|
||||
|
||||
当然了,能做到这点的前提是,低代码平台要有一个足够强大的编译器(代码生成器)。我会在第 6 讲、第 7 讲中,花整整两讲的篇幅,和你详细说明如何架构、实现这样的编译器,并帮助你理清编译器和编辑器之间的关系。
|
||||
|
||||
虽然一个良好的编译器能让低代码平台具有实现可视化模式到 Pro Code 模式切换回退的基础,但只有这点还不够。
|
||||
|
||||
前面我说了,回退的过程应该自然且内聚,这就给 UX 团队的交互设计师和编辑器的开发人员提出了考验。你的交互设计师需要能设计出自然流畅的回退过程,并且编辑器的开发人员要做得出来,并且做出来的性能还要足够好。
|
||||
|
||||
最后,在 Pro Code 模式下的低代码平台是一个事实上的 Web Online IDE。没错,也就是说,编码过程智能提示、智能补齐、重构、出错信息及定位等 Native IDE 的功能一个都不能少。
|
||||
|
||||
这方面概括地说,就是低代码平台必须具有充分的通用性。
|
||||
|
||||
适用人员
|
||||
|
||||
说完业务能力的适用性,我们再从低代码平台的用户的角度来看看天花板在哪儿。低代码平台要能支持各种技能水平的人同时使用,包括无软件开发技能的业务技术员,也包括掌握某种软件研发专业技能的人,以及水平介于两者之间的各种层次技能人员。
|
||||
|
||||
当然,不同层级的人的需求是不同的:
|
||||
|
||||
|
||||
业务技术员更需要傻瓜化的操作、更加简洁的操作流程设计,以及“说人话”,意思就是少用编程术语,不得不用时就地给出言简意赅(而非长篇累牍)的说明;
|
||||
专业软件研发人员往往要求对他们熟悉领域的底层代码,有更强的掌控能力甚至直接编码,对其他领域则需要有良好的可视化辅助。比如前端人员往往要求有可视化方式可以帮助他们获取业务数据,而后端人员则要求有可视化方式帮助他们画出 UI;
|
||||
水平不济的研发人员,特别是职场新手,往往更青睐可视化方式,他们更希望研发全流程都有可视化能力的辅助。
|
||||
|
||||
|
||||
另外,对软件研发人员这类人群,我们还有一个不得不考虑因素:如何消除他们的职业危机感。他们往往会非常担心在低代码的绑架下失去职业竞争优势、失去与公司的议价能力。特别是,技能较强的人不仅对这方面的担忧会更加强烈,还会认为低代码让他们失去了职业经验带来的比较优势,这种情况下,他们会更加抵触低代码技术。这个话题超出了这一讲的内容,但却极现实,影响团队稳定,有机会我们再专门聊聊。
|
||||
|
||||
除了个性化要求之外,低代码的各类用户也有共同诉求:
|
||||
|
||||
|
||||
平缓的学习曲线:完善的低代码平台往往可以覆盖业务研发全生命周期的各个环节,所以会包含数量众多的功能、流程。这些对使用人员来说都是知识,在开发流程的设计时,要管控好各种功能对知识的要求和传递,采用渐进式的方式设计开发流程;
|
||||
尽可能地自动化:没人能抵御自动化的魅力,这也是低代码平台的魔力所在。但在实现各种自动化封装的同时,我们要留出一手,在自动化流程不满足需求时可以切换为手动模式,这就是兜底策略的一种应用。另外,出错时还需给出准确的说明,不能惜字如金,比如出错时弹一个对话框只告知出错的结果,但不包含出错的原因,这样的方式是非常糟糕的;
|
||||
可以抄作业:也就是说需要提供数量众多的典型应用范例。低代码平台构建初期这个能力很容易被忽视,在平台逐渐成熟之后,我们必须恶补这方面的功课;
|
||||
提供视频形式的教材:多数人不乐意阅读文字,一份文档有数百字时就嫌烦。视频教材最好采用无音频的外挂字幕方式制作,一方面剪辑起来更加方便,一方面外挂字幕方便搜索和定位;
|
||||
出问题或者有疑问时能在第一时间得到帮助:建立客服群平台,开发者可以多与使用者互动,热情耐心地帮助他们解决问题,建立首问责任制。
|
||||
|
||||
|
||||
与基础设施和谐相处
|
||||
|
||||
这一点主要讨论的是低代码平台与基础设施之间的关系。我认为主要包含两个方面:一是对运行环境的要求;二是处理好与已有系统,特别是已有数据之间的关系。
|
||||
|
||||
对运行时的要求相对简单。有的低代码厂商可以直接提供基于公有云的运行时,这样就更加方便,基本可以做到开箱即用,但要照顾低代码客户在数据和信息安全方面的担忧。除此之外,我们还可以提供虚拟镜像,并支持在多数基于 PaaS 的平台上安装和运行。这个方式往往用于私有云的部署,这是应对客户信息安全担忧的有效方式。
|
||||
|
||||
我认为,低代码平台需要同时对这两种部署方式提供支持,特别是需要支持私有化部署方式。这样,无论是内部使用还是对外售卖,都可以做到更加灵活。当然,那些专注于内部使用且用户群明确的低代码平台来说,就不需要过多考虑这个问题了。
|
||||
|
||||
低代码平台与已有系统之间的关系,大致分为三种:一是被其他系统集成;二是将其他系统集成到平台中来;三是保持相互独立。
|
||||
|
||||
虽然,同时支持多种与存量系统融合部署,可以提升低代码平台的适用性,但是我认为没有必要支持所有方式,我们根据自身定位挑选以某种融合方式为主就可以了。
|
||||
|
||||
更多的情况下,优先发展被集成的能力是一个更好的选择。创造价值的肯定是业务能力,而低代码平台作为一种开发工具,对多数企业来说,是不具备直接创造价值的能力的,因此低代码平台也就很难冲到前面去了。即使对以售卖低代码能力为生的企业来说,低代码的开发能力是直接创造价值的业务,但一般买家购买后,也是将它作为自身的某个子系统,集成进买家自己的其他业务中。
|
||||
|
||||
既然低代码平台一般是被集成、作为子系统使用的,那么低代码平台与存量数据之间的关系也就比较明确了。低代码平台需要提供多种能力来获取外部数据,而不是将自身封闭成一个数据孤岛,要求外部将数据导入到平台中去。而外部数据的结构(关系)、存储介质、数据量、获取渠道等都是无法事先确定的。在这样的前提下,低代码平台要用好这些数据,可想而知难度是非常大的、甚至不具有可行性。
|
||||
|
||||
但如果是为确切场景下的数据提供定制化的融合方式,难度会低许多,但是代价也很明显,也就是几乎每种场景都需要提供一份定制。因此我们需要通过适当的架构设计,将定制部分与核心部分解耦,最好是能允许业务团队来自行定制。这正是插件系统需要解决的问题。我在第 15 讲里介绍这一点。
|
||||
|
||||
如何做到对单点登录的支持、如何考虑对接权限系统,也是被集成时需要着重考虑的能力。同时还需要在权限系统中区分开发态和运行态,开发态下往往要给予开发人员的账号更大的权限,而运行态下则需要做严格管控,避免数据滥用和渗透。开发态与运行态进行物理隔离,是杜绝开发人员利用开发账号越权访问数据的有效手段,这一点,我们会在接下来的全生命周期里简单阐述。
|
||||
|
||||
全生命周期
|
||||
|
||||
低代码平台不能只注重开发能力,开发能力当然是低代码平台的关键能力,但绝不是唯一的能力。低代码平台的能力必须能够覆盖从需求端到应用下线消亡的全过程。
|
||||
|
||||
这中间至少可以覆盖这些环节:
|
||||
|
||||
|
||||
D2C(Design to Code):业界已经有非常成熟、成功的 D2C 实践案例,对于低代码平台来说,从设计稿中识别出关键信息,再实现与低代码平台的对接与编辑,要比纯 D2C 解决方案容易得多;
|
||||
UX 设计即开发:有了 D2C 能力后,UX 设计师可以直接提供模板和业务组件。在这个意义下,UX 设计师起到了类似研发人员的作用;
|
||||
App 开发能力:这点自不必说,这是低代码的重要且基本作用;
|
||||
App 的自动化测试:包含两点,一是要能帮助 App 自动生成测试代码,二是提供一键式测试环境构建、测试执行、测试报告,乃至自动标注出错位置等;
|
||||
应用的版本管理:主要体现为要为应用构建相互独立的开发时环境、系统测试时环境、生产环境等,并能实现应用版本的测试、灰度发布、正式上线、紧急回退等能力;
|
||||
应用生产环境监控:这里包括两点,一是应用运行时基础信息(CPU/ 内存 / 磁盘空间)监控和告警,二是应用埋点数据的植入、采集、分析等。
|
||||
|
||||
|
||||
总之,虽然要做好其中的任何一条就已经很不容易了,但是这所有的功能都应该是低代码平台的“菜”,都可以实现低码化和自动化。第 15 讲我会挑重点对此进行部分讲解,比如 D2C 的实现、零代码生成自动化测试用例等,其他的内容有机会我们再聊聊如何实现。
|
||||
|
||||
总结
|
||||
|
||||
今天我从多个维度描绘了一个天花板级别的低代码平台必须具有的能力。
|
||||
|
||||
从适用领域角度看,低代码平台必须能同时支撑以数据为中心和以流程为中心的 App 的开发,这实际上等于需要覆盖大多数企业的开发业务能力。
|
||||
|
||||
同时,它还需要能支持不同复杂度业务的开发、兼顾效率。简单的业务开发要简单高效,而对复杂的业务则需要有良好的兜底策略,确保业务需求突破低代码平台能力边界时,可以有相应的应对措施,比如回退到传统 Pro Code 模式继续开发。这一点又给低代码平台提出了需要支持 Low Code 和 Pro Code 混合开发的方式的要求,而且混合的过程需要具有良好的自然过渡和内聚性。
|
||||
|
||||
从适用人员角度看,低代码平台需要能同时支持多种能力背景的人同时使用,比如业务专家群体多数无软件技术技能,需要为他们提供更多傻瓜化、可视化的操作。而且,既要照顾到有软件研发技能的群体在他们专业领域里的诉求,为他们提供更接近底层代码,甚至直接提供编码的开发方式,也要照顾这类群体软件技术能力之外的开发诉求,为他们提供可视化的方式,辅助他们完成业务的开发。
|
||||
|
||||
而且,低代码平台要有平缓的学习曲线、尽可能地自动化、提供大量的模板和素材,在帮助他们自学的同时,又能帮助他们解决实际问题。同时,低代码平台也不能只注重开发能力而忽视业务研发生命周期里的其他能力,应该积横向拓展,在需求端和交付端提供能力。
|
||||
|
||||
由此可见,要将低代码平台做到天花板上,无论哪个维度上显然难度都非常大,需要投入巨大的资源。但不是非要在触及天花板后,一个低代码平台 / 工具才能产生生产力。我们今天这讲的内容主要是给你描绘出一个完备的低代码平台可以做成啥样,希望能在某些方面对你有所启发。
|
||||
|
||||
思考题
|
||||
|
||||
在你的场景中,你更看重低代码平台在哪个领域中的表现呢?你希望主要有哪些人来使用你的低代码平台?除了开发能力之外,你还在业务开发过程中的哪些环节上对低代码有需求?
|
||||
|
||||
我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
188
专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md
Normal file
188
专栏/说透低代码/04|演进策略:先发展通用能力还是先满足业务需求?.md
Normal file
@ -0,0 +1,188 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04|演进策略:先发展通用能力还是先满足业务需求?
|
||||
今天我们来说说低代码平台在不同发展阶段的不同演进策略。我们可以将低代码平台的发展过程划分为 3 个主要阶段:MVP 阶段、成熟期、超越期。
|
||||
|
||||
MVP 阶段一般在 3 到 6 个月,时间比较短,主要目的是快速试错、快速闭环。这个目的之外的工作一般都“先放一放”,因此这个时候,备忘录里往往会留下许多待改进条目,但这些欠债在成熟期都要一一偿还。性能问题实际也是一种欠债,单独拎出来说是因为性能问题往往比较麻烦。它是慢性毒药,当毒性呈现出症状时,哪怕是轻微的症状,基本都已经很难搞了。而且,性能与功能是相生相克的,功能追加到一定程度就必然要停下来专门处理性能问题,两者呈现出一种螺旋式上升关系。
|
||||
|
||||
成熟期是实现低代码平台过程中的一个比较艰难的阶段。随着 MVP 阶段的需求免疫光环褪去、天使用户开始介入,实际业务需求紧跟着也就来了。此时平台团队往往面临这些直接压力:
|
||||
|
||||
|
||||
偿还 MVP 阶段的欠债;
|
||||
|
||||
彻底解决性能问题。
|
||||
|
||||
|
||||
功能欠债也好,性能问题也罢,始终只是技术问题。熬熬夜,牺牲一点发际线总是可以解决的。更麻烦的是,随着低代码平台的实际应用的推进,在 MVP 阶段中被有意无意忽视的业务场景逐渐显露出来,变得越来越具体。
|
||||
|
||||
这个状况会把低代码平台的发展道路的抉择推到风口浪尖上:先发展通用能力还是先满足业务需求?
|
||||
|
||||
其实,先发展通用能力也好,先满足业务需求也罢,最终目的是一致的,都是着眼于解决实际业务问题。这里的关键是优先发展哪种能力,是着眼于长远,还是着眼于眼下。
|
||||
|
||||
而到了超越期,我们的目标就非常明确了:解决具体业务问题,将业务问题梳理为各种场景,然后针对场景做针对性优化,使得在已覆盖的场景里的开发能力、效率等各方面全面超越 Pro Code 模式。
|
||||
|
||||
即使是在未覆盖的场景或者特殊场合,低代码平台也可以通过部分回退到 Pro Code 模式的手段、采用高低代码混合的方式,实现对业务开发需求的支持。并保持效率、交付能力相对 Pro Code 模式的优势,从而达到低代码平台将在各方面显现出对传统 Pro Code 模式的全面超越效果,这显然是振奋人心的一个阶段。
|
||||
|
||||
通过前面对各个阶段简单的分析可以看到,MVP 阶段和超越期的线路和目标非常明确,我们不需要过多讨论。但成熟期却有两条相对清晰且都很有说服力的发展路径:先发展通用能力还是先满足业务需求。这正是我们这一讲要解决的问题。
|
||||
|
||||
选择什么样的发展路径?
|
||||
|
||||
这个问题的答案实际上没有太多的悬念:优先发展通用能力。
|
||||
|
||||
“通用能力”指的是一种不与某种具体业务绑定的开发能力,是一种能够适用于各种形式业务的开发能力。在这里,我无法给通用能力画一个边界,或者给出一个精确的定义,因为它有很大的弹性,它的边界只存在于每位平台设计者对业务的理解中,或者说在他们的心中。可以说,你对通用能力的边界理解和想象,大概就是你的低代码平台未来的能力上限。
|
||||
|
||||
作为一个开发平台,哪怕只是一个开发工具,如果能力过于聚焦,带来的后果会是很难适应不断变化的业务需求,也很难搞定新形态的业务,从而失去拓展新应用领域的机会。最终的结果就是要么是推倒重来,要么就是被其他通用性更强的平台所替代。
|
||||
|
||||
毕竟,人人都希望有一个具有更多想象空间的平台,而不是只顾眼下的一亩三分地,一眼就能看到它的边界。想象空间越大表示它能做的事情越多,潜力也就越大,相应地,它能给你的回报也会越大。当你自己对这个平台的能力都没有想象空间的时候,就不可能让别人(上级或投资者)对它有多大的期待,这样就几乎不可能获得更多的资源。
|
||||
|
||||
所以,如果非要给“通用能力”画个边界,你对它的想象空间就是这个边界。而你对它的想象空间也决定了别人(上级或投资者)对它的想象空间,从而决定了它能给你带来多少资源和回报。
|
||||
|
||||
那么,不走优先发展通用能力的道路,还有其他发展道路可以走吗?
|
||||
|
||||
有的。我们可以选择优先聚焦于业务场景和业务痛点,优先发展能快速解决当前业务问题的开发能力、能切实解决业务痛点的能力。仔细想一想,这样的发展线路不但没有问题,而且会显得很务实。从实际问题出发,并且在短期内可以获得成效。
|
||||
|
||||
但是过于聚焦的实现往往意味着扩展性不足。一个显而易见的后果就是难以适应后期业务场景的变化,那当这样的状况出现时,要怎么办呢?
|
||||
|
||||
有人说:简单,再针对新的业务、新的痛点重新来一次就好了。但如此反复数次后,一个新情况马上就出现了:
|
||||
|
||||
|
||||
|
||||
这张图就表示了这个状况。你可以看到,每次都从头开始,在短时间解决具体问题的能力确实能迅速爬升,见效快。但由于没有通用性,在最初设定的业务问题都解决了后,自然就到达顶峰,之后基本就没下文了,每个工具都是这样。
|
||||
|
||||
如果你是一名管理者,你最容易想到的,可能是如何整合已有的这些工具。一般在评估之后,你会发现这很难!
|
||||
|
||||
因为这些工具是由不同的人在不同时间,采用不同方法(Shell、Native App、Web)聚焦在不同问题上开发出来的,简单地说,他们没有共同的基因。更要命的是,这种解决问题的思路形成习惯和传统之后,你会发现无论时间多长,能力的上限基本就在那,没有任何想象空间。显然谁都不希望看到这样的结果。
|
||||
|
||||
现在,我们可以来尝试回答一下这个小节标题中的问题了,你应该选择什么样的发展路径?很显然就是走优先选择发展通用能力的路。越通用,想象空间越大。
|
||||
|
||||
坚持优先发展通用能力不动摇
|
||||
|
||||
但凡事总有两面性。通用是有代价的,它的代价就是不能聚焦于具体业务,从而导致在具体业务上没有很好的表现。甚至,还有可能在开发特定业务的情况下,和传统 Pro Code 方式相比,不仅没有任何改进,甚至还倒退!
|
||||
|
||||
这与低代码模式四处标榜的高效、简易等标签相悖,很多低代码的反对者将这些案例收集起来作为毒瘤论的例证。
|
||||
|
||||
这里,我放了一张图,展现了优先发展通用能力的工具 / 平台的发展过程:
|
||||
|
||||
|
||||
|
||||
你可以看到,MVP 阶段与其他工具相似,只是周期很短,短时间内也有能力的迅速爬升。成熟期相对漫长,而且具体业务开发能力缓慢爬升,基本处于啥都差一点的状态。这个时期注重的是发展通用能力,为未来的各种场景做架构设计和打基础、解决性能问题。但这个阶段往往很容易夭折,不仅因为这个阶段难度巨大,而且漫长的持续投入与看到的收益不成比例。
|
||||
|
||||
如果你刚好是负责人或直接领导,在这个阶段里应该要不停地讲故事,把你的想象空间尽可能形象地描述给上级或投资者,建立他们的信心。能由此获得更多资源是最好的,但至少不要被压缩现有资源。同时也帮他们建立想象空间,让他们去影响他们的上级。
|
||||
|
||||
成熟期的一个显著特征是待实现的多数业务都会触及低代码平台的能力边界。所以几乎每面对一个新需求,你都要接受这样的灵魂拷问:要如何在确保平台的通用能力得到扩展的前提下顺便满足当前需求?即使通用能力无法得到拓展,至少也要避免为了实现某个业务团队需要的开发能力而将该具体业务耦合到平台中。
|
||||
|
||||
我的建议是,请按顺序评估如下的因素:
|
||||
|
||||
|
||||
评估手里有多少可用资源,这是根本;
|
||||
评估当前用户的友好程度;
|
||||
评估老板或者你自己有多强势,面临业务压力时,他能扛住多久。
|
||||
|
||||
|
||||
资源是一切的根本。资源充裕时,你基本上可以忽略其他任何因素。但资源总是有限的,需要着重考虑的是当各方扛不住业务压力时,有没有备用资源可以投入,以缓解矛盾、快速提供业务需要的开发能力。那些开发能力和效率都值得充分信赖的、又肯熬夜加班的技术骨干,就是一种王炸级别的备用资源。此外,你还要仔细避开那些与投入资源数量无关的困境,比如前面我们提到的性能问题,或者所用技术大大超出已有储备。
|
||||
|
||||
平台的用户(即业务开发人员)的友好程度也是一个重要因素。通用的能力往往无法在具体业务开发场景中提供良好的易用性或效率,而难用和低效是需要由他们来直接承受的。因此,如果用户群体的友好度很低,他们三天两头地发邮件,还在各个场合下不余遗力地抱怨,我相信你或者你老板很快就会妥协。在面对低友好度用户的时候,我们就要适当地聚焦在他们的痛点上,而不能一味追求通用能力,反之则可以更多地聚焦在通用能力的研发上。
|
||||
|
||||
你或者你老板有多强势是另一个因素。如果出问题时、业务团队不停抱怨时,总有人为你站台的话,你就可以更加专注于发展通用能力上。但这时我们要学会讲故事(画饼),不失一切时机地将通用能力所构筑的想象空间描绘出来。
|
||||
|
||||
坚持优先发展通用能力的道路的收获季,是在场景化阶段,也就是超越期。在一个通用的底座之上支持某种具体场景是很容易的,因为此时你考虑的已经不是能不能做到的问题(这个问题在成熟期已经基本解决了),而是要不要做、做成啥样的问题。即使做错了,付出的成本也只是把该场景推到重来而已,并不需要将整个平台推到重来。
|
||||
|
||||
这就好比我们国家坚持先发展重工业,后发展轻工业。在重工业阶段投入巨大、艰苦卓绝,但当各个工业门类基本建设齐全之后,再发展轻工业就相对容易得多了。结果有目共睹,近 20 年我国的 GDP 嗖嗖地往上涨。
|
||||
|
||||
一旦确定如何实现某个业务场景之后,实现的方法基本上都是一样的:具体化和自动化。
|
||||
|
||||
具体化和自动化实际上说的是同一个事情,场景越具体,配置内容也就越具体,越具体的配置内容就越容易实现自动化。这里所谓的自动化,指的就是在具体的业务场景下,基于通用能力自动化生成各种各样的配置。场景越具体,自动生成代码的比例就越高。而自动化完成几乎所有业务的开发,不就是低代码的魅力所在吗?
|
||||
|
||||
到了场景化阶段,低代码的魔力才开始显现,才能真正拉开与 Pro Code 模式的差距。这两种开发模式的能力终究会在某个点上出现交叉:
|
||||
|
||||
|
||||
|
||||
并且,一旦 Low Code 模式的能力超过 Pro Code 模式之后,这个趋势终将不再回头。因为此时的 Low Code 模式将依托于其强大的通用能力,将各个场景逐个纳入到其能力范围内。每个被支持的场景生成的代码都凝聚了技术专家、业务专家的智慧,因此,一旦低代码平台支持了某个场景,凝聚其中的专家经验将持续为高 / 低各种技能水平的使用者赋能和提效。这是 Pro Code 无法做到的。
|
||||
|
||||
而 Pro Code 模式,它固有的内秉性知识传递的弊端、以及语言自身能力的限制,导致它的能力上升将极其缓慢。这点你从身边的编码专家身上就可以看到,他们写了十几年代码,却依然摸不到这个领域的天花板,更别提有所突破了。Pro Code 模式的业务交付能力,之所以能随着时间推进持续缓慢爬升,是因为团队人员(不考虑流失)的编码经验在缓慢地提升,但显然靠个人经验的提升来提升业务交付能力的边际收益,必然是越来越低的。
|
||||
|
||||
如何保持优先发展通用能力呢?
|
||||
|
||||
前面说了,“通用能力”指的是一种不与某种具体业务绑定的开发能力,是一种能够适用于各种形式业务的开发能力。当低代码平台仍处于成熟期时,待实现的多数业务开发能力的需求,都会触及低代码平台的能力边界,我们需要创造出新的能力来拓展平台的能力边界。而且,用于拓展能力边界的功能都要是通用的,而不能只适用于当前的具体业务。
|
||||
|
||||
这样讲比较抽象,接下来我讲一个我自己的实际案例,帮你加深理解。
|
||||
|
||||
第一个例子是关于大场景的需求。在 MVP 阶段甚至更早的时候,我们的业务团队提出了两个比较典型的应用场景:一是低代码平台需要能支持表单的可视化开发;二是低代码平台需要能支持 Dashboard 的可视化开发。
|
||||
|
||||
这两个需求本身其实就已经具有很高的通用性了,直接照做也无可厚非。但深挖下去,你会发现这样的问题也需要一并解决:
|
||||
|
||||
|
||||
极少有表单单独成一个 App 的(否则就变成调查问卷了),表单与表单之间如何串联?
|
||||
Dashboard 里也有可能使用表单,有的 Dashboard 会有查询条件,查询条件部分就是一个表单(这在我所在的产品里很常见)。
|
||||
|
||||
|
||||
为了能同时解决这几个显式和隐式问题,低代码平台最起码需要提供一个能同时支持表单、Dashboard,以及它们相互引用的功能。实际上,能满足这样功能的编辑器的通用程度就已经很高了,再考虑到其他可能出现的应用场景,我们很容易想到需要打造一个不以任何具体场景为假设前提的场景编辑功能。后来我将这个场景称为通用场景。
|
||||
|
||||
第二个例子是关于图形的需求。应用团队提出需要提供柱状图、折线图、饼图、仪表盘等常见图形的可视化编辑功能。其他常见的图形还有散点图、漏斗图等不下 10 种图形。
|
||||
|
||||
|
||||
这里需要说明一下背景,我是 echarts 来绘制图形的。对它有一点了解的人都都知道,echarts 不仅可以绘制这 10 来种常见图形,也能绘制其他许许多多不常见的图形。
|
||||
|
||||
|
||||
每一种图的配置方式差距非常大,挨个去定制的话基本等于做 10 个独立的图形,但需求又非常多,我们不可能一个个去定制。于是我决定先提供一个支持任意图形的开发能力,而不提供可视化方式。当然,这事是先要和应用团队沟通的。这样做的话,短时间内可以满足业务团队对图形的任何需求,而我也可以将资源投入到其他功能的开发。
|
||||
|
||||
其实类似例子有很多,但这两个案例已经足以说明问题了,现在来总结一下我在这样的情况下的方法:
|
||||
|
||||
|
||||
对业务提交的任何功能需求,都按照最通用情形来考虑。即使对方只要一棵树,仍按照一座森林来考虑,我们不见得就要实际交付一座“森林”,但要为它预留足够的位置;
|
||||
充分考虑已知场景的共同特征,暂时把它们的个性化特征丢角落里。过多考虑个性化特征只会限制你的想象力,这会牺牲掉许多易用性,但这是权衡之下必要的取舍,这是通用的代价。
|
||||
|
||||
|
||||
还有第 3 点,是上面两个实例没体现出来的:
|
||||
|
||||
|
||||
任何业务需求,只要能采用已有的功能实现的,决不新增功能;只要保持已有功能通用性不变前提下稍微扩展就能实现的,也决不新增功能;好钢用在刀刃上,资源集中投放到开发新的通用能力上,不分散。
|
||||
|
||||
|
||||
还有第 4 点,是非技术性因素:
|
||||
|
||||
|
||||
之所以我能坚持这么做,是因为我的用户群友好程度较高,主要得益于我平时注重提升他们的满意度:倾听他们的痛点,主动热心协助解决问题(编码、出方案等),帮助他们脱困;一般只要他们的自身的需求能按时交付,他们就会很高兴;记住:放低姿态。
|
||||
|
||||
|
||||
采用上面总结的 4 点咬牙坚持一段时间,大约 12 到 18 个月后,基本就可以熬过成熟期了。此时你会发现你的低代码平台基本上啥业务需求都可以实现了(虽然采用的实现方法没有像传说中的低代码那样酷)。但这意味着你的平台已经活下来了,并且有了一定应用基础,可以考虑进入了超越期了。
|
||||
|
||||
现在我们就可以充分聚焦到各个场景的个性化需求上了。这时我们再回顾下前面那两个实例。
|
||||
|
||||
通用化场景下表单的开发非常繁琐,数据采集后的所有校验都需要应用自己捕捉事件,自己编写校验逻辑。小到文本是否过长过短,大到服务端异步校验,都非常繁琐。那么我们可以针对表单的特性设计出新的配置流程,把校验逻辑全部自动化实现。应用只要填写校验规则和出错提示文本就可以了。
|
||||
|
||||
通用化场景下的 Dashboard,无论是布局还是交互也都非常麻烦。现在我们可以针对它设计一个基于卡片的布局器,交互也可以简化成单传参和获取数据。这样,一个图表甚至都可以不用去关注其他图表,也能实现联动效果。
|
||||
|
||||
通用化的图形实现,实际上就是直接填写 echarts options。这个门槛很高,需要学习 echarts 的大量配置 API。那么现在我们就可以针对常见的图形,设计出针对性的可视化配置方式,屏蔽掉绝大部分 echarts 配置细节,做到只要会填表单就能开发出复杂图形。
|
||||
|
||||
那为啥一开始不这么做呢?资源!
|
||||
|
||||
当我们手里没那么多资源可以同时铺开时,我们既要保证新场景的支持不至于耽误应用团队的工期(这是最容易被投诉的因素),同时又要把已有场景做得很完美,这是需要大量资源的。实现一个通用能力,可以同时解决掉许多业务的开发需求,但如果我们把相同的时间投入到某个场景,即使做得尽善尽美,那也只能解决那一丢丢需求。我前面就说过了,在资源充裕的前提下,基本不需要考虑方式方法,干就完了。但现实的资源总是有限的,我只能这么做。
|
||||
|
||||
在超越期,场景化完善之后,是否通用场景就没用了呢?绝不是!
|
||||
|
||||
这些通用场景会华丽地转职为兜底策略的一部分。我说过,再完美的低代码平台也总有能力边界,总有业务团队提一些“奇葩”需求越过这条线。此时通用的功能就可以发挥兜底的作用,让应用能按期交付的同时,也给平台团队喘口气的时间。我们甚至可以根据这个需求的“奇葩”程度决定是否要无视它,即使这个需求下次再来,我还是有办法治它的。这就是兜底策略给的底气!
|
||||
|
||||
总结
|
||||
|
||||
今天我详细讨论了低代码平台演进策略,将演进过程分为三个主要阶段:MVP 阶段、成熟期、超越期。针对成熟期和超越期的发展策略,给出了不同的侧重。成熟期侧重于发展通用能力,而超越期侧重于发展场景化和提效的能力。
|
||||
|
||||
成熟期是一个相对漫长的,比较难熬的阶段。在资源不充裕的前提下,为了着眼于长期演进,必须坚持优先发展通用化能力的思路。对业务提交的任何功能需求,都按照最通用情形来考虑,同时充分考虑各个场景的共同特征,而暂时忽略他们的个性化特征。坚持采用已有功能来实现各种需求,通过这个方法倒逼已有功能的进一步通用化。同时我们也要放低姿态,注重培育和改善与应用团队之间的关系,还可以与 UX 团队保持良好通畅的沟通,要求他们不要设计出一些超过当前能力的应用出来。
|
||||
|
||||
到了超越期,我们的重心就要转移到各个功能的个性化需求上来了,此时要把个性化需求和易用性提升到最高优先级。必要的话,甚至可不惜重新设计各个具体场景的开发流程,以获得更高的自动化开发水平,尽可能高地提升代码的自动生成比例,从而最大化地发挥低代码的开发优势。而此时在成熟期留下来的诸多通用能力,就成了平台的兜底策略的一部分,可以兜住场景化所未能覆盖到的那 20% 的场景。
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
低代码平台发展过程中的成熟期非常关键,这个阶段的发展质量基本决定了低代码平台整体质量,你认可这个观点吗?为什么?
|
||||
除了这一讲提到的 Dashboard 场景和表单场景,你认为还有哪些场景与低代码技术是“天作之合”?完成了与低代码的结合之后,将对你现在的业务产生什么样的效果?
|
||||
|
||||
|
||||
期待在留言区看到你的想法。我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
213
专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md
Normal file
213
专栏/说透低代码/05|基础设施:启动低代码平台研发之前,你需要有什么家底?.md
Normal file
@ -0,0 +1,213 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05|基础设施 :启动低代码平台研发之前,你需要有什么家底?
|
||||
今天我们来谈谈建设低代码平台之前,必须准备好基础设施。
|
||||
|
||||
在过去的几年里,Web 技术得到了显著的发展,无论是功能还是性能方面,浏览器能够承载高度复杂的 Web 页面里。在这个情况下,低代码平台,如果要选择 B/S 和 C/S 的其中一种作为它的基本架构,我相信你会和我一样,毫不犹豫地选择 B/S 架构。
|
||||
|
||||
虽然低代码平台是一种非常复杂,综合要求很高的软件,但 Web 技术的长足发展、浏览器优秀的功能和性能,完全足以打消你对 B/S 能不能搞得定的各种疑虑。同时,几乎所有的 PC 端的业务、越来越多的移动端业务也都倾向于使用 B/S 架构(或其衍生架构),用 Web 技术来制造 Web 应用是一个非常自然而然的选择。即使需要同时输出安卓、iOS 等 Native App,利用 Web 技术也可以很好地在浏览器中模拟 Native App 效果,几乎不会在可视化开发方面造成麻烦。
|
||||
|
||||
Web 组件集是 Web 应用的最重要基础设施,没有之一。对于基于 B/S 架构的低代码平台来说,更是如此。而且,低代码平台的 Web 编辑器对组件集有着更多要求。Web 组件集主要在三个环节发挥作用,一是用于构筑低代码编辑器自身,二是用于构筑编辑器的开发能力,三是用于构筑业务应用。这三者基本覆盖了低代码平台 60%~80% 的功能,可见编辑器的质量和能力基本直接决定了低代码平台成败。而 Web 组件集是这一切的基石,组件集的能力彻底渗透到这三个环节的方方面面。
|
||||
|
||||
所以在开始打造低代码平台之前,请先确认你手里已经拥有一套值得托付的组件集。今天我会从多个角度说清楚什么样的组件集才值得你的托付。
|
||||
|
||||
自主可控
|
||||
|
||||
你可能会好奇,为啥不是一上来就提需要哪些功能。相比组件集的功能来说,我觉得自主可控更重要,主要是因为组件集的第二个职能的要求:我们需要利用组件集来构筑编辑器的开发能力。为了说清楚这一点,我需要先简单介绍一下低代码编辑器如何管控细节的。
|
||||
|
||||
我这里列出了一个表格,表示的是低代码编辑器在 App 开发过程中对细节的管控程度,我们主要围绕配置量、难易度、适用范围、定制能力这几个关键维度来分析。
|
||||
|
||||
|
||||
|
||||
这个表格给出了这样的结论:无论是过于严格还是过于宽松的细节管控,都对 App 的开发不利。也就是说,我们需要一种合适的细节管控手段,让 App 开发过程中的 4 个重要考量维度都有很好的表现。
|
||||
|
||||
我认为,对 App 常用功能做组件化封装就是这样一种非常合适的细节管控手段。组件化封装过程可以把大量的 HTML/CSS 细节屏蔽掉。如果再有一定的 UX 规范做约束,甚至可以做到几乎屏蔽所有的 HTML/CSS 细节,这也使得我们几乎可以在不需要 HTML/CSS 知识的前提下用到组件(当然这是站在低代码模式的角度说的)。至于定制能力和适用性,则是通过组件封装时暴露的 API 来说实现的。这里说的组件 API 是指组件的输入属性和输出事件,这是组件外部唯一用于影响组件行为和功能的通道。
|
||||
|
||||
在 2018 年和 2019 年的时候,我做过竞调。竞调显示那时候多数的低代码编辑器还在采用直接配置 HTML/CSS 的方式,把过多的细节暴露给开发者,导致这些编辑器使用起来非常啰嗦,所需的 HTML/CSS 知识一点都不能少。过于啰嗦繁复的配置过程也导致无法做出复杂的应用来。
|
||||
|
||||
但是现在几乎所有的低代码编辑器实现都采用组件化来管控细节了,这方面大家达成了一致意见。正因为低代码编辑器需要借助 Web 组件来管控 App 开发的细节,所以低代码编辑器的开发能力与组件就产生了关联。编辑器的编辑过程,实际上就是在收集到开发者的配置信息之后,为组件的各个 API 生成正确值的过程。这就是我在前文所说的组件集被“用于构筑编辑器的开发能力”的原因。
|
||||
|
||||
现在我们可以来回答为啥对组件集来说自主可控更重要的问题了。
|
||||
|
||||
由于编辑器需要委托组件集来管控开发的细节,所以低代码编辑器本身是没有开发能力的,它的开发能力来自于组件集,也就是说,组件集的能力直接决定了低代码编辑器的开发能力。
|
||||
|
||||
在这个约束下,你对组件集的定制需求是巨大的,而且有的需求是专为编辑器定制开发的,从 Pro Code 模式角度上看这种需求不可理喻。假设你把 GitHub 上一套 star 最多的组件集直接拿来用了,这种情况下即使它有着极活跃的开源社区可以快速响应你的需求,组件集的守护者们也不可能处处为你定制,因为他们从未考虑过他们的组件集会给人类以外的编辑器使用。即使你说服他们接受了定制需求,交付周期又是另一个大问题。一个小小的修改,可能需要等上数周,即使你给他们推送 PR,但 review、讨论和评估也需要时间。
|
||||
|
||||
你不可能会将你的研发进度与一个不可控的开源社区挂钩。最终的结果就是,要么是你 fork 一个仓库出来单干,要么自己做一个二次封装组件集,相对可控地实现定制化需求。这样做意味着你已经向“自主可控”屈服了,只是程度上多少而已。
|
||||
|
||||
说完技术面,我们再从另一个角度来说说:UX 规范。
|
||||
|
||||
当你开始讨论是否要打造一个低代码开发平台的时候,想必你已经有了一定规模的应用了,因此想必你也已经有了自己的 UX 规范了。
|
||||
|
||||
使用第三方组件集,就意味着要全盘接受它的 UX 规范。技术面的问题是“看不见”的、是藏在“面子”后的“里子”。而 UX 规范则是彻头彻尾的“面子”,它必须要展示在你、低代码上的业务开发者和 App 最终用户面前。
|
||||
|
||||
那么,涉及的各方能长期接受这样的 UX 规范吗?万一有人,特别是最终用户对此提出异议呢?这种情况下,即使是对 UX 规范的微调,局面也会非常尴尬。因为 UX 规范就像国家宪法一样,是一个国家法律的根基,即使 UX 规范做了很微小的改动,也会波及所有组件的外观。
|
||||
|
||||
一个工作量较小但可行的方法,是写出优先级更高的 CSS 覆盖掉原始组件的样式,但这些 CSS 样式开发难度很高且极其难以维护。更关键的是,这个做法直接侵入到了组件集的私有实现。这部分实现是没有兼容性保障的,守护团队几乎每个小版本的升级都有可能导致覆盖样式失效。
|
||||
|
||||
所以挑选组件集的时候,我们的第一要务是要选一套具有自主可控的组件集,即使它看起来没有那么强大。它最好是你自己或下级团队开发的,这样才具有完全的自主权。至少也要是兄弟单位开发的,而且你要有足够的权限修改它的源码。
|
||||
|
||||
我把给低代码编辑器使用的 Web 组件集称为可视化组件集,它和传统的 Pro Code 组件集有相似之处,也有差异。有机会我会再来详细阐述两者之间的异同点。
|
||||
|
||||
封装程度高
|
||||
|
||||
这里,我要再次提醒你所选的组件集是给编辑器使用的,而不是给人类使用的。
|
||||
|
||||
前面在讲自主可控时,我提到组件集的一个重要任务是用于解决低代码编辑器的细节管控问题。如果组件集的封装程度不高,就达不到细节管控的目的。比如下面这个例子,它来自于一套实际组件集,界面上显示 4 个 radio button:
|
||||
|
||||
|
||||
|
||||
这套组件集采用如下 API,HTML 部分是:
|
||||
|
||||
<label class="vx-radio-container">
|
||||
<input type="radio" class="vx-radio" [checked]="checkedFlg">
|
||||
<div class="radio-substitute"></div>
|
||||
<span>选中</span>
|
||||
</label>
|
||||
<label class="vx-radio-container">
|
||||
<input type="radio" class="vx-radio" [checked]="true" disabled>
|
||||
<div class="radio-substitute"></div>
|
||||
<span class="vx-radio-check-disabled">选中禁用</span>
|
||||
</label>
|
||||
<label class="vx-radio-container">
|
||||
<input type="radio" class="vx-radio" [checked]="!checkedFlg">
|
||||
<div class="radio-substitute"></div>
|
||||
<span>未选中</span>
|
||||
</label>
|
||||
<label class="vx-radio-container">
|
||||
<input type="radio" class="vx-radio" [checked]="false" disabled>
|
||||
<div class="radio-substitute"></div>
|
||||
<span class="vx-radio-check-disabled">未选中禁用</span>
|
||||
</label>
|
||||
|
||||
|
||||
这样的 API 问题很多,我们先不说 API 是否优雅,主要关注其相当混乱的配置方式:
|
||||
|
||||
|
||||
有的是通过变量配置,比如是否选中功能(Angular 采用类似 [checked]=“var” 的格式来引用变量);
|
||||
有的是通过样式控制,比如是否 disabled,用 vx-radio-check-disabled 样式配置 disabled 的状态;
|
||||
有的是通过 HTML 节点配置,比如单独使用 span 来配置 radio 的文本。
|
||||
|
||||
|
||||
你要注意,编辑器并不怕生成一大片代码,但害怕东一榔头西一棒,这会对代码生成器造成许多不必要的麻烦。前面就是一个非常典型的例子。一个 radio 无非就 3 个配置项:文本、状态和值,这个例子采用了各不同的方式来配置,有的用了变量,有的用了样式,有的用 HTML 节点。作为对比,还有一个比较好的方式是采用数据驱动的方式统一配置,比如:
|
||||
|
||||
// html
|
||||
<jigsaw-radios [options]="options" [value]="selected"></jigsaw-radios>
|
||||
|
||||
// typescript
|
||||
const options = [
|
||||
{label: '选中', disabled: false},
|
||||
{label: '选中禁用', disabled: true},
|
||||
{label: '未选中', disabled: false},
|
||||
{label: '未选中禁用', disabled: true},
|
||||
];
|
||||
const selected = options[0];
|
||||
|
||||
|
||||
可以看到,这个版本的 HTML 极其简洁,所有配置项均通过数据变量来实现,分别是备选列表 options 和选中条目 value 两个变量。版本 2 的封装方式是一种比较好的方式,主要体现在组件的配置项都是通过一系列变量来实现,低代码编辑只要正确生成变量的代码就可以了,无需关注 HTML/CSS 代码的生成。
|
||||
|
||||
下面我们再看一个示例:
|
||||
|
||||
// html
|
||||
<jigsaw-table [data]="tableData"></jigsaw-table>
|
||||
|
||||
// typescript
|
||||
const tableData = {
|
||||
header: ['列1', '列2', '列3', '列4'],
|
||||
field: ['field1', 'field12', 'field13', 'field14'],
|
||||
data: [
|
||||
['cell11', 'cell12', 'cell13', 'cell14'], // 第1行
|
||||
['cell21', 'cell22', 'cell23', 'cell24'], // 第2行
|
||||
['cell31', 'cell32', 'cell33', 'cell34'], // 第3行
|
||||
['cell41', 'cell42', 'cell43', 'cell44'], // 第4行
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
这里,我们采用一个数据 tableData 数据结构描述了一个表格,将表头、列名、数据都通过数据的方式来配置。
|
||||
|
||||
我将这种封装方式称为数据驱动模式,这种模式的关键特征是组件将 HTML/CSS 彻底封装到其内部,只暴露出一些属性对外提供配置入口。这样的封装方式对低代码平台的代码生成器是非常友好的。我们应该优先挑选具有这种 API 特征的组件集,自行实现时也需要采用这样的封装方法。
|
||||
|
||||
为了加深你的理解,我列出了现在市面上常见的组件的封装方式,作为补充和对比:
|
||||
|
||||
第一种我们叫它数据驱动封装方式。它的典型特征是所有 API 都以数据的方式来驱动,彻底将 HTML/CSS 封装在其内部。前文给的第二和第三个例子就是用这样一种方式封装出来的组件,这是一种适合给低代码编辑器使用的封装方式。
|
||||
|
||||
第二种我称之为模板驱动封装方式,典型特征是 HTML 部分非常复杂,数据、样式、状态都几乎在 HTML 模板里实现,前文给的第一个 radio 例子就是用这样的方式封装出来的组件。这种组件更加适合 Pro Code 模式来使用,由于它直接将 HTML 模板当做一种 API 暴露给应用,因此应用可以按需改造 HTML 模板,灵活实现特定功能。也就是说,它具有更强的定制性,这方面是数据驱动类型组件所不具备的能力。
|
||||
|
||||
最后一种我们暂且称它为 CSS 样式模板,典型特征是只给出了 CSS 样式,没有带动作。它的封装度极低,需要由于自己写动作来完成组件的功能,比如 boostrap 组件。这样类组件集是非常不适合用于低代码平台的开发的,不仅如此,由于封装度太低,实际上也不适合用于 Pro Code 模式的大规模开发,只能作为一种轻量的规范化的模板在特定场合使用。
|
||||
|
||||
功能强大
|
||||
|
||||
虽然我把组件集的功能排到第三位考虑因素,但不意味着组件集的功能不重要。
|
||||
|
||||
实际上,低代码平台对组件集的功能需求是非常大的。低代码编辑器自身必然不是一个简单的 Web 应用,状态多、形式多样、功能丰富、交互密集、性能要求极高等标签是可以毫不犹豫地往上贴的。要能承载这样一个复杂 Web 应用,对 Web 组件集的要求显然不会低。能同时满足前面提到的几个标签的组件集,就已经超过了市面上 90% 以上的组件集了。
|
||||
|
||||
那么,什么样的组件集可以称之为功能强大呢?我们可以参考下面这些指标:
|
||||
|
||||
|
||||
组件集里至少包含 50 个以上的原子组件和容器类组件,才能基本覆盖完整日常所需;
|
||||
具有良好的视图悬浮(气泡化)功能封装和多层视图叠加管理能力。低代码编辑器往往有密集的配置入口,许多配置项需要就地弹出气泡甚至多级气泡来承载,避免打断当前的开发工作;
|
||||
数据采集类的组件(文本框、数字框、下拉选择等)必须对表单友好,这样才能更容易实现出表单类页面;
|
||||
对常用功能要有统一封装,在 Angular 里称为指令 /Directive,特点是这些功能可以“外挂”到任何普通 dom 节点、组件节点上,实现功能扩展。比如,像任意视图下拉、上传功能、多功能徽标、下拉多级菜单、拖拽功能等功能都值得封装;
|
||||
一个符号图标库,这个不一定非要自己做,但必须要有。编辑器密集的配置界面上“寸土寸金”,多用图标可以节约许多空间。当然,我们也可以直接使用开源的,font-awesome、material design都提供了不错的基于 svg 的图标库,基本满足日常使用所需。
|
||||
|
||||
|
||||
这里,我要再强调一下对组件集的性能要求,这个要求主要来自于低代码编辑器本身,以及所见即所得效果的实现方式。
|
||||
|
||||
低代码编辑器的复杂度非常高,特别是在画布界面,有各种各样的编辑器、配置界面、悬浮气泡、对话框等。活动视口(ViewPort)上同时有一两百活动组件都是家常便饭,这对组件的渲染性能和脏检查性能提出了很高的要求。
|
||||
|
||||
其次,如果画布上的所见即所得效果不是采用 iframe 实现的,而是在画布上采用一个动态模块直接渲染出来的,那对编辑器的性能要求就直接翻倍了。此时,不仅需要高效渲染编辑器本身,还要在画布上把 App 的运行效果也实时动态渲染出来。另外,有的 App 本身也具有很高的复杂度。这样一来,在画布界面上同时存在两三百个活动组件实例也是可能的。如果原子组件的性能不佳的话,整个画布操作起来就会非常卡,影响开发效率,也影响开发者的心情。
|
||||
|
||||
皮肤深度定制能力(可选)
|
||||
|
||||
虽然,组件集支持多种颜色的皮肤并不是一个必备的能力,但对低代码编辑器来说,这确实是一个非常实用的功能。这个能力赋予了应用在整体外观上的配置能力,而且低代码编辑器可以很容易地做到一键换肤。在同个色系下实现不同颜色的皮肤难度并不大,而且多数还可以做到热切换皮肤颜色。
|
||||
|
||||
换肤能力更进一步的需求是,需要支持跨色系的皮肤。一般来说,至少需要支持有两种基础色系:明亮色系和黑暗色系。
|
||||
|
||||
跨黑白色系的皮肤实现的难度不小,而且只有组件自身支持跨色系还不够,还需要有一套机制可以帮助应用定制的视图也实现跨色系换肤,这方面难度就更大了。因为组件集内部的实现是可控的,但是应用定制的视图结构是不可控的。一个比较好的实现方式是利用 CSS3 的变量特性,但这个功能在 IE11 上支持不好,需要有一个取舍。下面这段 CSS 简单演示了这个特性:
|
||||
|
||||
:root {
|
||||
--blue: #1e90ff;
|
||||
--white: #ffffff;
|
||||
}
|
||||
body { background-color: var(--blue); }
|
||||
h2 { border-bottom: 2px solid var(--blue); }
|
||||
|
||||
|
||||
换肤能力再深一步,就需要支持黑白同屏功能了。前面我介绍了组件集可以同时支持黑白不同色系的皮肤以帮助应用构建出明亮风格和黑暗风格 App。但这两种色系只能二选一,那有没有需要在同一个页面上支持两种相反色系共存呢?
|
||||
|
||||
这个特性乍听起来很扯淡,但细一想,这样的场景是很多的。比如一个页面的整体是明亮色系的,但带了一个深色的 header 或侧边导航。组件支持黑白同屏的皮肤可能是换肤能力里的最顶级场景了。黑白同屏的换肤能力能让低代码编辑器自身和应用页面具有非常自由的皮肤切换能力。
|
||||
|
||||
有机会我会说说如何实现支持黑白同屏的换肤能力,以及如何做到热切换。
|
||||
|
||||
总结
|
||||
|
||||
今天我详细介绍了在启动低代码平台研发之前需要具备的基础设施:一套合适的组件集。组件集之所以如此重要,是因为贯穿了基于 B/S 架构的低代码编辑器三大基本功能:构筑低代码编辑器自身、构筑编辑器的开发能力、构筑业务应用。这些功能覆盖了低代码平台 60%~80% 的功能。可见,组件集对低代码平台的研发有着决定性的影响。
|
||||
|
||||
在挑选组件集的时候,我给出了 3 个重要的考量维度,按重要程度分别是自主可控、封装程度和功能是否强大。由于所选组件集是给低代码编辑器使用而非人类使用,因此会有一些看似奇怪的定制化需求。封装程度高使得代码生成器实现起来更加容易,组件集需要有强大的功能才能满足低代码编辑器自身需求,才能满足编辑器的开发能力,才能满足业务团队的各种需求。
|
||||
|
||||
组件集的研发务一定要比低代码平台的研发先启动。视投入的人力数量和质量,我建议你至少提前 6~12 个月。两者研发的启动时间不宜靠得太近,在组件集无法支撑低代码的研发需要的时候,往往只能暂停低代码平台的研发,先补充组件集的能力。但启动时间也无需间隔过长,在组件集有一定功能积累之后,两者可以并行演进。
|
||||
|
||||
在组件集启动研发之前,最好已经有一定基础的 UX 规范了,UX 规范对组件集和业务应用的“面子”有这巨大的影响。但 UX 规范不一定需要完全自己开发,如果资源不允许,完全可以考虑抄抄大厂的作业,阿里的Ant Design和 Google 的Material Design都是不错的选择。腾讯的ISUX和字节的ArcoDesign也是很优秀的,可以通盘接受,也可以基于某个规范做一些定制化。UX 规范会对组件集、低代码编辑器、业务应用等产生全系列的影响,不可忽视。
|
||||
|
||||
最后我还简要介绍了一个可选能力:皮肤定制能力。这是一个锦上添花功能,没有它确实不会怎么样,但是有这个能力可以让低代码平台到业务应用具有快速调整外观的能力。特别是黑白色系皮肤的支持,可以大大提升业务应用的展示张力。我常说的一句话是:深色皮肤自带高大上光环,相同的一套 UI 设计,深色系的比浅色系的要显得更加高端,也更加节能和环保。如果能做到深浅色系自由切换,那就更赞了。
|
||||
|
||||
技术积累是这一切一切的基础:一两位有深厚技术积累的领军人物是更重要的家底。
|
||||
|
||||
最后安利一下我司开源的组件集Jigsaw,由我主导研发。它对中兴低代码平台提供了非常良好的支持,没有Jigsaw就没有中兴低代码平台,Jigsaw性能和功能都非常优秀。
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
为了能满足构筑低代码编辑器的开发能力,组件集需要具备哪些功能特征和非功能特征?
|
||||
为了更好满足你的业务开发需要,你认为组件集的哪方面能力更加重要?为什么?
|
||||
|
||||
|
||||
欢迎在留言区写下你的想法。下节课见。
|
||||
|
||||
|
||||
|
||||
|
159
专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md
Normal file
159
专栏/说透低代码/06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?.md
Normal file
@ -0,0 +1,159 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06|踏出新手村便遭遇大Boss:如何架构低代码的引擎?
|
||||
可视化开发是所有低代码工具 / 平台(下文简称低代码或 Low Code)的标配,是成为低代码工具 / 平台的一个必要条件。而承载可视化开发的核心基础设施,就是所见即所得的编辑器。
|
||||
|
||||
这个编辑器非常重要,它的使用体验、能力和易用性在极大程度上决定了低代码整体的成败。由于编辑器的实现是一个非常大的话题,我们需要分成 8 讲才能说清楚。所以,今天这节课我们先从编辑器的引擎切入,从架构的角度聊一聊应用代码生成器与编辑器之间的关系。
|
||||
|
||||
在讨论低代码到底是银弹还是行业毒瘤那一讲中,我们了解到低代码的开发过程就是不停地在描述和细化一个业务最终的样子。这就要求低代码编辑器能实时反馈出开发人员的操作结果,这也就是所见即所得的含义。
|
||||
|
||||
而通过模拟,是难以保持“所见”与“所得”的一致性的。为了达到“所见”与“所得”的高度一致,我们就需要实时地把应用创建出来,创建应用的过程就是生成并运行应用代码的过程。显然,生成应用代码是这一切的基础。编辑器在收集到开发人员的操作后,应该立即生成应用的代码。
|
||||
|
||||
那么,生成代码的功能在架构上和编辑器可以有什么样的关系呢?不同的关系对低代码长期演进会有什么样的影响呢?不同的组织应该如何选择合适的架构呢?这正是你我今天要探讨的主要内容。
|
||||
|
||||
代码生成器与编辑器的关系
|
||||
|
||||
我了解过许多失败的案例,它们多数有一个共同特点:开始于一个玩具。故事基本上可以归纳为某个人或者团队因为偶然的心血来潮,写了个小玩意,有个界面,支持拖来拖去,可以生成特定功能。老板了解后都觉得有用(老板觉得没用的那些自然就不会被了解到了),于是加大投入,想做得更大、更好。结果,初创团队“鸡血”了几个月后,基本就玩不转了,要么销声匿迹,要么推倒重来。
|
||||
|
||||
其实界面拖拖拽拽生成特定功能的门槛并不高,但是要承载厚重的低代码战车,则需要有很深远的设计和思考,其中最重要的一环是如何生成应用的代码。
|
||||
|
||||
代码生成器与编辑器之间的关系,可以大致分为这几个层次:
|
||||
|
||||
|
||||
Level 1:没有代码生成器的概念,或者极其粗糙;
|
||||
Level 2:有相对独立的模块用于生成代码,但该模块与编辑器耦合严重;
|
||||
Level 3:代码生成器与编辑器基本相互独立,具有同等地位;
|
||||
Level 4:插件系统与生态,编译器必须再次抽象才能实现插件系统。
|
||||
|
||||
|
||||
Level 1
|
||||
|
||||
如果连代码生成器的概念都没有,或者由散落在代码仓库各个角落里的三两个函数构成,那么这样的编辑器显然是无法区分编辑态和运行态的。即使有代码生成器,也是在编辑器的功能基础上通过 if else 来实现的,比如引入一个只读状态,在这个状态下无法编辑,并将它作为运行态来用。很显然这样的实现方式,是极不妥的。由于没有足够的抽象,功能点加多了后,编辑器的代码就会变得极其难以维护。
|
||||
|
||||
关于代码的可维护性,我常常说的一句话是:一个 if 一个坑。使用 if 就等于在尝试对事物状态进行枚举。简单事物可以枚举所有状态,但多数事物的状态是不可枚举的。每少考虑一个状态就是一个 bug 或需求,同时也是对已有逻辑的一次冲击。而为了能够区分出相似的情况,往往需要增加新的 flag,在 flag 数量达到某个数之后,这份代码就再也改不动了。
|
||||
|
||||
Level 2
|
||||
|
||||
当你意识到 if 解决不了问题时,往往就会开始考虑对编辑器做抽象了。而编辑态和运行态是编辑器两种最基本的状态,这两种状态有很多共同的部分,也有一小部分差异。所以抽象的第一步就是把公共部分抽取出来,并在编辑态和运行态下改写公共层里的某些行为。这是 OOP 思想的最基础的应用。
|
||||
|
||||
我们这一讲先不展开讲解如何去抽象,但是有一点是非常明确的:生成代码的能力是两个状态都需要的,应该归入公共层中。所以,很自然的做法是把散落在代码仓库各角落里的那三两个函数挪到一起,最起码是放到同一个文件里去,然后适当调整入参和依赖,让它们能恢复正常功能。更进一步,需要将原有的 if else 改用 OOP 的多态特性来实现。
|
||||
|
||||
到了这个时候,你就需要好好考虑一下 TypeScript 了。在这节课中,我不想展开讨论是 TypeScript 好,还是 JavaScript 好这样的细节,但是我的建议是此时应该坚决引入 TypeScript。因为 TypeScript 提供了一套非常完整的 OOP 实现,而 OOP 是用于对复杂事物做抽象的必备武器,基于 JavaScript 原型链自行实现的 OOP 没有那么完善(我敢保证!),弃之勿留恋!
|
||||
|
||||
另一个关键原因是,TypeScript 提供了极完善的静态类型支持。记住,你正在开发一个编辑器,除非你打算一直把它当作一个玩具,否则你要先做好至少 5~10 万行代码的觉悟。这样量级下的代码,如果没有类型的辅助,我们的开发效率将是有静态类型支持下的 1⁄2~1/3,甚至更低。
|
||||
|
||||
当你的编辑器的公共层里有了一个相对独立的代码生成器模块之后,编辑器的长期演进就有了一个架构基础,但也仅此而已。
|
||||
|
||||
此时你的 APP 依然只能在编辑器上运行,无法独自运行,这会导致将来你的业务应用和你的低代码平台之间产生耦合。要么你把应用包住对外提供业务价值,要么应用把你包住,这取决于谁更强势。但无论如何,耦合是不可避免的。
|
||||
|
||||
到了这里,你就需要好好思考一下将来你和你的应用团队之间的关系了。如果你的老板本就打算或允许你们耦合在一起,那么到这里你就可以继续开发编辑器的其他功能了。反之,这事还没完。
|
||||
|
||||
提醒一点:请务必确认你的老板知晓平台和应用是可以实现解耦的,否则请适当给予他一些提示(写一份 PPT 吧,他会因此更喜欢你的),并且确认他确实经过一番思虑后才做出保持耦合的决策。
|
||||
|
||||
因为,与应用之间保持耦合这个事情,开弓就没有回头箭了。如果你的老板依然保持模棱两可的状态,那么我建议你将生成代码这事继续完善下去,避免日后他后悔了并给你下一个你压根就无法实现的任务。与应用解耦状态下是可以融合部署对外提供业务的,反之是走不通的。
|
||||
|
||||
Level 3
|
||||
|
||||
为了解除平台与应用之间的耦合关系,我们需要将代码生成器进一步独立出来,提到与编辑器同等地位上。在 Level 2 中,我们是把它当作一个公共模块(一个库),而到 Level 3,我们是要把它作为与编辑器对等的一个独立功能来实现。
|
||||
|
||||
一个比较好的实现方式是将它做成一个命令行,可以在 shell 终端里跑。命令行的好处很多,主要很容易与 DevOps 流水线结合使用,或者实现各种自动化。应用跑一下命令行就可以生成一份可以独立运行的代码出来,这样你爱放哪运行都与编辑器无关了。
|
||||
|
||||
将代码生成器与编辑器彻底分离,让它们可以脱离对方独立运行,需要对代码做更深一层次的抽象。在 Level2 中,两者的关系是下面这样子的:
|
||||
|
||||
|
||||
|
||||
两者之间是紧耦合的关系,这意味着代码生成器很难被扩展和定制。不可否认的是,业务的发展速度是远大于代码生成器的,因此业务的需求永不终结。在代码生成器难以扩展和定制的前提下,低代码平台非常容易为了满足某个紧急业务需求,不得不将该业务耦合到代码生成器的实现中。
|
||||
|
||||
一旦开了这样的口子,代码生成器架构的腐化也就开始了,逐渐就会跟越来越多的业务耦合进来,不需要太多时间,这样的代码生成器就无法继续演进下去了。
|
||||
|
||||
而在 Level 3 架构下,编辑器和代码生成器之间的关系变成了下面这样:
|
||||
|
||||
|
||||
|
||||
代码生成器和编辑器处于同一层级,并且引入了协议层。协议层的作用之一,就是对生成代码各种功能的 API 做出抽象和约束。代码生成器主要职责是实现这些协议,而编辑器的主要职责则是调用协议层提供的 API 来完成代码生成等一系列上层活动。
|
||||
|
||||
然而,引入协议层的更深层目的,是为了方便应用扩展和定制。从实现角度看,协议层实际上就是一堆接口(interface),这些接口的默认实现是由代码生成器按照其通用的、与应用无耦合的方式来实现的。当通用的实现不满足应用的需要时,应用要有渠道来编写适合其自身的协议层的实现,并覆盖掉默认通用的实现,从而实现编译器的定制化和按需扩展 *。
|
||||
|
||||
当然,让应用实现一套完整的编译协议是不合理的。我们可以在编译协议层里加入部分实现,这样应用在定制时就可以只覆盖少数不适合的实现,复用大部分的已有实现,这样可以大大减轻应用定制的工作量和难度。因此,这个架构的实际关系是下面这样的:
|
||||
|
||||
|
||||
|
||||
虚线框里的部分,实际上就是应用定制代码生成过程所需的 SDK。这就是 Level 3 的架构,它不仅具有更好的长期演进基础,还具有非常好的、非常优雅的扩展性和定制性。至此,我们可以给代码生成器换一个高大上的名字了,将其称为编译器。
|
||||
|
||||
Level 3 只提供了扩展和定制的可能。如果我们对这些扩展和定制点再做一番系统性的设计,在此基础上抽象出编译流程的各个时机(可称为编译生命周期),以及对各个时机具备的可扩展点加以规范,从而形成一套完备的插件开发能力,那我们可以让这个架构演进到 Level 4 了。Level 4 的最主要目的是要形成完善的插件系统,进而具备打造生态圈的基础。Level 4 已经超出这节课的范畴太多了,在这个专栏的最后,我会留一讲专门来说这个话题。
|
||||
|
||||
最后,我再分享一点我负责的低代码平台目前的架构演进的状况,可以作为案例给你一点参考。
|
||||
|
||||
其实,我也不是一开始就有如此清晰的分层演进的考虑的。不过庆幸的是,我在编码开始前多花了点时间做了类似前文的思考,所以我直接跳过了 Level 1 和 Level 2,直接按照 Level3 的架构目标开始编码。但为了更快做出 MVP(最小可用版本),我并没有严格按照 Level 3 的架构实现,而是偷了点巧,先把编译器当作一个库实现出来,在 MVP 完成之后老板认可了,才专门花了点时间把编译器独立出来,完成 Level 3 架构的转型。
|
||||
|
||||
我想说的是,这里所说的架构分层是一个目标,你在实现时,可以根据实际情况(工期、人力等)灵活调整。但即使我未严格遵循 Level3 的架构要求实现,我还是让编译器尽可能地与编辑器保持独立,同时在过程中逐渐抽取 API 作为协议层。
|
||||
|
||||
目前,我负责的低代码平台正处于 Level 3.5,它已经拥有了相对完善的插件系统,具有发展生态的基础了。演进到了这个阶段,它的编译器已经很稳定了,目前它的主要任务是推广和布道,枯燥且乏味。
|
||||
|
||||
低代码编辑器要能实现最基础的“所见即所得”的闭环(就是达到给老板演示的最低要求),就不得不实现生成代码的功能。这个看似简单的小功能,背后却是牵动着日后长期演进的许多考虑。
|
||||
|
||||
至此,你是否觉得我们这节课的标题起得非常好?低代码开发者刚走出新手村碰到的第一个怪物就是一个实力 Boss,关键是这个 Boss 看起来和一个普通小怪差不多,我们冲上去居然不会被它秒杀,嗑点药居然还能站得住。
|
||||
|
||||
当你磨了半天都没能杀掉它的时候,不妨停下来仔细观察它,你会发现原本你以为的青铜怪居然是一位王者。此时我们应该立刻回城并想办法升级(氪金 ^_^)装备和技能,决不可恋战!
|
||||
|
||||
生成代码总体流程
|
||||
|
||||
接下来,我再说说实现编译器的两种可行方法及其优劣和适用场景。注意,下面讨论的默认应用是以 UI 复杂且交互密集为主要特征的 PC 端网页,具有类似性质的移动端页面也适用。C/S 架构下的 UI 不在讨论范围内。
|
||||
|
||||
编译器的输入是一组结构化的数据。即使编辑器与编译器之间采用某种 DSL(领域编程语言)作为协议,但是输入给编译器之前,我们肯定要将输入数据解析为结构化的数据。为了描述方便,我使用 SVD 这个内部术语来指代这组结构化数据。编译器的唯一任务就是将 SVD 转成代码。这里有两种编译选择:
|
||||
|
||||
|
||||
直接法:直接将 SVD 生成出浏览器能识别的代码;
|
||||
间接法:先将 SVD 生成出某种 MVVM 框架的代码,再利用其编译器进一步编译成浏览器能识别的代码。
|
||||
|
||||
|
||||
两种方式各有优劣,直接法的最大好处是架构简单,特别是不需要再引入和协调另一个编译器,能少写不少代码;其次是效率高,JiT 编译器只需间接法的约 20%~30%,几乎可以做到全程百毫秒以内的 JiT 编译消耗。
|
||||
|
||||
当然,直接法的代价也是非常明显的。它最大的问题在于,你必须考虑你所在场景里的前端技术栈选型。因为到了 Level 4 的时候,你就需要考虑生态的问题了,而现在前端生态最大的割裂莫过于 AVR(Angular、VUE、React)等框架的技术栈选型了,极少有裸奔(含 jQuery)的。如果你期望到时能和它们玩到一起去,那么现在就应该选择相同的技术栈。
|
||||
|
||||
无论你选择了 AVR 中的哪个,都意味着你只能采用间接法。如果你处于一个强势的机构并且你老板坚定为你站台,所有不用你平台的应用团队一律给考核 C,那么可以无视这条(你好幸福)。或者你的平台不打算给内部开发人员使用,也可以不考虑这条。
|
||||
|
||||
直接法的另一个约束在于页面组件集的实现。等等!编译器和组件集居然也能扯上关系?实际上,我认为低代码实现之初主要的基础设施之一就是要有一套合适的、可控的组件集。你一定要记住,细节管控的程度会对低代码的易用性和适用性产生决定性的影响,而权衡之下最合适的细节管控粒度就是 Web 组件集。
|
||||
|
||||
Web 组件集将大量的 HTML/CSS 细节封装掉,只通过 API 的方式暴露出来,而低代码通过引入一套组件集可以屏蔽掉 HTML/CSS 的所有细节,大大提升易用性。如果你的组件集是用 jQuery 做的,那么可采用直接法生成代码。但是,我猜大概率你不会这么做,即使曾经有 jQuery 实现的组件集,也早就被你嫌弃并使用 AVR 之一重写了(这是一个重复再造轮子并获得晋升的绝佳机会,如果你没抓住,那就太可惜了)。
|
||||
|
||||
如果你选用的组件集是 AVR 之一实现的,那很可惜,你只能采用间接法。不过,随着 Web Component 的发展,目前 AVR 都已推出 Web Component 的转译器。利用 Web Component 技术,你就可以直接使用浏览器原生技术来动态渲染 APP。如果你有条件使用这个技术,那也可以无视这条约束了。比如,华为云的低代码平台就是采用这个技术来屏蔽 AVR 差异的,这是一个成功的案例。莫春辉老师在GMTC2021 深圳站上详细介绍了这个案例,感兴趣可以去看看。
|
||||
|
||||
只要符合上述两个条件之一,就只能采用间接法。现在我再说说间接法。
|
||||
|
||||
估计你已经看出来了,间接法就是一个备胎。只有在你没有条件使用直接法的时候,才用它。它的好处是能兼容 AVR 等各种框架,对 Web 组件集的要求也会低很多,也不需要考虑组件集提供 Web Component 版的工作放在哪。
|
||||
|
||||
间接法的代价是架构复杂,需要额外协调 AVR 提供的编译器,这事需要额外消耗许多脑细胞,而且社区里提供的资料都是如何“正常”地去用它。而协调这些编译器这事却是不走寻常路,这样碰到坑大概率要去看编译器的源码,难度很大。当然,如果你是一名技术极客,把这事当作一个优势看待,我也不拦你。
|
||||
|
||||
其实,间接法有一个绝对优势,那就是可以支持 Low Code 和 Pro Code 混合开发一个 APP。这是直接法所做不到的。支持混合开发的重要意义在于,你可以相对妥善地处理好存量代码(没错,就是你的低代码做出来之前的那些页面代码)。这点有机会我们再聊。
|
||||
|
||||
最后,如果你和我们一样选了 Angular,那恭喜,你还需要额外多协调 TypeScript 编译器。我在 QCon+ 有一次在线分享,讲的就是如何在浏览器构建 TypeScript 的 JiT 编译器以打通整个编译流水线,那个分享具有非常好的实操价值,你可以去看看PPT。
|
||||
|
||||
总结
|
||||
|
||||
今天,我们从架构的角度,对低代码编辑器与应用代码生成器(编译器)之间的关系做了详细的讨论。根据抽象程度的不同,我把代码生成器划分为 4 个层级,并详细说明了前三个层级的特点和适用的场合,以及各个层级对低代码平台的长期演进会产生啥样的影响:
|
||||
|
||||
|
||||
Level 1:基本上就是一个玩具,适合用作试验品,用于快速试错、收集你的低代码潜在用户的反馈,如果确定要开发一个可以称为工具的低代码,那么强烈建议推倒重来;
|
||||
Level 2:基本上可以称为是一个低代码工具了,应该可以一定程度上发挥低代码的优势来,也具备了长期演进的架构基础,但是扩展性和定制性也很弱,容易与应用产生耦合,因此只能在小范围内使用,无法规模推广;
|
||||
Level 3:基本上可以称为是一个低代码平台了,它的架构清晰、层次分明,具有非常好的长期演进能力,且具备良好的扩展性和定制能力,能处理好业务团队提的各种需求,对于其能力之外的需求,也可以通过扩展和定制的方式优雅地处理,适合大规模推广。
|
||||
|
||||
|
||||
同时,这一讲我们也讨论了实现代码生成器的不同方法,主要侧重讨论了如何根据现有技术选型、长期演进方面挑选合适的实现线路。
|
||||
|
||||
直接法架构简单、性能好、实现工作量低,但是选用的先决条件苛刻,主要有两点:一是你所在机构的前端技术选型;二是所用组件集的前端技术选型。间接法作为直接法的备胎,当没有条件使用直接法时,只能选择间接法。间接法架构复杂、JiT 及实时渲染性能差、需要额外协调所用框架的编译器等。但间接法额外带给我们一个直接法所没有的能力:允许 Low Code 与 Pro Code 混合使用,进而可以相对妥善地解决低代码模式与传统模式存量代码之间的关系。
|
||||
|
||||
在下一讲中,我将会从实操角度,介绍如何做出一个和人工一样实现几乎任何业务功能的代码生成器,以及一个 Level3 层级的编译器如何实现。
|
||||
|
||||
思考题
|
||||
|
||||
根据抽象程度的不同,应用代码生成器与编辑器之间可以分为几个层级?各个层级的关键特征是什么?不同层级对低代码平台长期演进具有什么样的意义?欢迎在留言区写下你的看法。
|
||||
|
||||
我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
348
专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md
Normal file
348
专栏/说透低代码/07|结构化代码生成法:代码如何生成代码?.md
Normal file
@ -0,0 +1,348 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07|结构化代码生成法:代码如何生成代码?
|
||||
编辑器是低代码平台一个非常重要的基础设施,而代码生成器是编辑器的引擎,是编辑器做到所见即所得效果的基础。
|
||||
|
||||
在上一讲中,我从架构的角度详细理清了代码生成器和编辑器之间的关系,以及代码生成器与低代码长期演进之间的关系。
|
||||
|
||||
那么今天,我们就从实现的角度说清楚代码生成器是如何实现的。
|
||||
|
||||
人类是如何写代码的?
|
||||
|
||||
虽然写代码是我们日常工作内容,没有啥特别的。但这一讲的目的,是让代码代替人工来生成代码,所以我们需要快速回顾一下我们日常敲代码的过程,以及敲出的代码都有哪些部分。为了帮你快速回顾这个过程,我把手工正常开发一个组件的部分过程录屏下来了。
|
||||
|
||||
需要特别说明的是,首先我是用 Angular 写的这段代码,即使你没学过 Angular,但你光看代码也是可以轻松理解的;其次我今天介绍的这个方法是通用的,不限于生成 Angular 的代码,你可以用这个方法生成任意框架代码,甚至用来生成 Java/C/C++ 等后端代码。
|
||||
|
||||
下面我们看第一段视频,演示的是创建组件骨架代码:
|
||||
|
||||
|
||||
|
||||
把这十来秒的视频多播放几次后,你会有一个感触:正常敲代码的整个过程是按需的。显然你不会一上来就去敲第一行的 import,而是当前需要用到 Component 这个渲染器时,你才会想起来:哦,我应该去 import 一下。
|
||||
|
||||
接下来是第二段视频,给组件编写样式:
|
||||
|
||||
|
||||
|
||||
如果给一个不会写代码的人看这段视频,他可能会觉得,你们敲代码怎么是东一榔头西一棒的,一会在这里插入几个字符,一会在那插入几个字符,而不是像写文章一样,基本保持自上而下、从头到尾的节奏。
|
||||
|
||||
然后是第三段视频,给组件创建输入条件(用 Angular 术语是:输入属性 /Input):
|
||||
|
||||
|
||||
|
||||
组件的外部可以通过视频里的 title/content 两个参数给这个组件喂数据。显然,输入属性的种类、数量、类型都是按需定义的,我们无法事先约定一个组件需要啥输入属性。下面这行代码就很好地演示了如何使用这两个属性:
|
||||
|
||||
<my-comp title="the title" content="the content"></my-comp>
|
||||
|
||||
|
||||
除此之外,一个普通组件的开发过程还至少包括如下的内容:
|
||||
|
||||
|
||||
类成员方法的定义:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
组件对外事件的定义(用 angular 术语是:输出属性 /Output):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
某些比较复杂的组件,可能还会涉及类的继承和接口实现定义,关注下图中的 extends 和 implements 关键字:
|
||||
|
||||
|
||||
|
||||
|
||||
我们再把前面几段视频展示的内容列出来,会发现还有:
|
||||
|
||||
|
||||
import 其他类(含第三方的或者其他类);
|
||||
组件 HTML 模板的定义;
|
||||
组件样式的定义;
|
||||
类成员变量的定义和初始化;
|
||||
类构造函数需要注入的功能,这里,注入 /Inject 是一个 Angular 术语,比如下图中的 _http 变量就是通过注入而来的:
|
||||
|
||||
|
||||
|
||||
|
||||
如果你不是用 Angular 开发前端,那可能不会涉及所有条目,但根据你所用技术栈,可能也会多出一些新的条目来。不过,没有关系,我给出的方法依然适用,你只需要将多出的内容像我一样将其列出即可。
|
||||
|
||||
结构化代码生成法
|
||||
|
||||
有了前面的铺垫,我们现在就可以来介绍一下如何让代码生成代码了。
|
||||
|
||||
前面我们提到,正常写代码的过程是按需、跳跃式地在不同部位插入一个个片段,没人能像写文章一样自上而下、从头到尾、一气呵成地写出一个视图组件的代码,利用代码来生成代码的过程也不可能是这样的方式。
|
||||
|
||||
虽然正常写代码过程是东一榔头西一棒,一会在这里插入一个片段,一会在那插入一个片段,甚至还要修改别的文件,但是插入点的类型总是有限的!如果你能意识到这一点,那你就朝着正确的方向迈出第一步了。
|
||||
|
||||
接下来需要解决的问题就是,都有哪些类型的插入点呢?
|
||||
|
||||
如果此时你还无法马上回答这个问题,那么再翻回去,重温一下前面的内容。我在第一个小标题末尾处详详细细列出了开发一个普通视图组件要做的事情,每一条就是一类插入点。起码在生成一个普通视图组件的代码的时候,插入点种类就这么多了。
|
||||
|
||||
那么如何生成一段有特定功能的代码呢?我们先看一段简单但有代表性的伪代码:
|
||||
|
||||
import {EventEmitter} from "@angular/core";
|
||||
|
||||
export class MyComp {
|
||||
public select: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
this.select = new EventEmitter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这段代码功能极其简单,在一个类中定义了一个名为 select 成员变量,然后在构造函数里初始化了 select 变量的值,就这么多。但是开发这段代码至少涉及了 3 类插入点,分别是:
|
||||
|
||||
|
||||
import 区;
|
||||
构造函数区;
|
||||
成员变量区。
|
||||
|
||||
|
||||
假设现在有 3 个值分别代表这 3 个区:importSection,constructorSection,memberSection,那我们实际上只是向这 3 个值里分别放入对应的代码片段,完成之后,这 3 个值分别大概如下。
|
||||
|
||||
|
||||
importSection:
|
||||
|
||||
|
||||
import {EventEmitter} from "@angular/core";
|
||||
|
||||
|
||||
|
||||
constructorSection:
|
||||
|
||||
|
||||
this.select = new EventEmitter();
|
||||
|
||||
|
||||
|
||||
memberSection:
|
||||
|
||||
|
||||
public select: EventEmitter;
|
||||
|
||||
|
||||
仔细观察一下,你就会发现各个插入点的代码片段都是普通的文本了,此时的它们没有任何语义,也没有任何功能。
|
||||
|
||||
再仔细观察一下,你会发现代码片段里包含了许多关键字,这些关键字实际上是冗余的,可以省略。
|
||||
|
||||
最后,不难发现,每一个插入点应该是一个数组,因为任何功能的实现都可能会往一个或多个插入点插入点添加片段。所以经过一番思索后,各个插入点的内容可以先优化成这样:
|
||||
|
||||
|
||||
importSection:
|
||||
|
||||
|
||||
[
|
||||
{identifier: 'EventEmitter', from: '@angular/core'}
|
||||
]
|
||||
|
||||
|
||||
|
||||
constructorSection:
|
||||
|
||||
|
||||
[
|
||||
{statement: 'this.select = new EventEmitter()'}
|
||||
]
|
||||
|
||||
|
||||
|
||||
memberSection:
|
||||
|
||||
|
||||
[
|
||||
{modifier: 'public', identifier: 'select', type: 'EventEmitter'}
|
||||
]
|
||||
|
||||
|
||||
这里,我不仅删去了冗余的关键字,还把代码片段解析成了一个个结构化的数据,可以方便我们后面的处理。
|
||||
|
||||
下面我们继续给这个类添加新的修改。假如我想让 MyComp 类继承一个名为 Base 的类,并实现两个接口:OnInit/OnDestroy,也就是我们预期将这行代码改为:
|
||||
|
||||
export class MyComp extends Base implements OnInit, OnDestroy
|
||||
|
||||
|
||||
其中,Base 类来自工程里的另一个文件 base.ts,OnInit/OnDestroy 类来自 Angular 的 npm 包。
|
||||
|
||||
采用同样的方法,你需要往多个插入点添加新的数据,先在 importSection 这个插入点里,添加如下 3 个数据:
|
||||
|
||||
[
|
||||
{identifier: 'EventEmitter', from: '@angular/core'}, // 原来已有的
|
||||
{identifier: 'Base', from: './base'}, // 下面3个是新增的
|
||||
{identifier: 'OnInit', from: '@angular/core'},
|
||||
{identifier: 'OnDestroy', from: '@angular/core'}
|
||||
]
|
||||
|
||||
|
||||
再往 extendSection 里添加一个数据:
|
||||
|
||||
[
|
||||
{identifier: 'Base'}
|
||||
]
|
||||
|
||||
|
||||
最后往 implementSection 里添加如下数据:
|
||||
|
||||
[
|
||||
{identifier: 'OnInit'},
|
||||
{identifier: 'OnDestroy'}
|
||||
]
|
||||
|
||||
|
||||
其他的任何修改只是重复上述过程,所做的事情无非就是找到对应的插入点,然后把代码片段拆解为适当的结构化数据,再追加到该插入点列表中去。
|
||||
|
||||
你可能会有疑问:插入点里会不会有重复项,出现了重复项该怎么办?
|
||||
|
||||
答案是插入点出现重复项是正常的。比如多个功能点需要 import 相同的类,那么 importSection 里就会出现多个重复的条目,在转为代码时,应该先将 importSection 里的重复条目过滤掉。
|
||||
|
||||
那么是不是所有插入点都要去重呢?
|
||||
|
||||
不一定,不同的插入点处理方式不一样。比如成员变量插入点里,如果有两个条目的 modifier、identifier、type 三个属性都一样,就可以认为是重复条目,过滤掉就好了;如果这三者之一有不一样,那此时就应该报错,否则代码生成器可能会生出类似下面的代码来:
|
||||
|
||||
public select: EventEmitter;
|
||||
private select: EventEmitter;
|
||||
|
||||
|
||||
显然,这样的代码是有错误的。再比如,构造函数插入点里的重复条目就不应该过滤掉,应该按照顺序依次生成代码块。
|
||||
|
||||
你可能还会有疑问:插入点里的顺序敏感吗?
|
||||
|
||||
答案依然是视不同插入点而定。比如 import 插入点的顺序一般是不敏感的,生成代码时可以按照特定顺序排列,还可以将 from 值相同的合并到一起去。而一些输出是代码块的插入点(如构造函数插入点)则对顺序是敏感的,此时不应该随意调整顺序。
|
||||
|
||||
另一个可能的疑问是:在插入点里添加结构化的代码片段非常繁琐,手工不可能完成啊。
|
||||
|
||||
请注意,往插入点里插入代码片段的,是另外一段代码,而不是人工!而这里的“另一段代码”就是我们的代码生成器了。
|
||||
|
||||
我前面花了这么大篇幅详细说明了如何把一段我们习以为常的代码拆分,并散落不同的插入点里去的过程,实际上就是对代码生成器的逆向工程,这样说是为了让你更好地理解代码生成器是如何生成代码的。只有理解了代码生成器的工作原理之后,我们才能更容易地实现它。
|
||||
|
||||
计算机非常擅长读写结构化数据,因为结构化数据没有二义性。DSL 也好、自然语言也罢、甚至包括编程语言,都充斥着各种二义性,需要上下文才能准确解释,计算机很难轻易理解这种形式的数据。
|
||||
|
||||
因此,低代码平台基本都会采用结构化数据作为持久化的数据格式,而我在前文里则给出了一个如何将一组结构化数据转为代码的方法。虽然编辑器持久化采用的结构化数据,与代码生成器所需的插入点结构化数据不是严格对应的。但是两者之间的数据结构已经非常接近了,只需要做少量简单的转换,我们就可以将编辑器持久化采用的结构化数据转为插入点数据,然后再传给代码生成器。
|
||||
|
||||
代码生成器会先将各个插入点的数据做校验,一旦发现有冲突,就会报错。校验通过之后,代码生成器需要将各个插入点的结构化代码片段,按照该类插入点的语法拼装成一个代码块。之后,再按照语法要求的顺序将代码块拼装在一起,最终就得到了一大块代码,此时的代码才具有语义和功能,也可以被其他编译器编译了。
|
||||
|
||||
这个时候,你再看看最终生成出来的代码,会发现它与手写的代码非常接近,这样的代码是可以被人类理解和二次编辑的。但我们应该极力避免这样做,保持对这些代码的只读,因为任何对这些代码的编辑都难以反向同步到输入的结构化数据上。
|
||||
|
||||
HTML 模板的生成
|
||||
|
||||
插入点生成代码的过程基本都很简单,唯独组件的 HTML 模板的生成比较复杂,需要专门拎出来说明。
|
||||
|
||||
Web 组件集提供的组件可以分为两大类,一类是普通组件,另一类是容器。容器具有普通组件的所有特性,但与普通组件不同的是,容器可以将任何普通组件、容器装到它内部去,这样逻辑上就形成了一棵树。
|
||||
|
||||
我们不难理解,从树叶到树根的各个节点都是相互独立的。那么同样,编辑器持久化时也必须保持各个节点相互独立,这样的一个特性就给生成 HTML 模板代码造成了一些小麻烦。这是因为,每个节点只能知道自己能产生啥样的 HTML片段,而不知道其父级和子级的 HTML 片段,但是现在的目标是要生成视图的完整HTML。
|
||||
|
||||
我们可以通过这个例子来理解一下:
|
||||
|
||||
|
||||
|
||||
单从结构来看,上面这段 HTML 代码是这样的结构:
|
||||
|
||||
jigsaw-box
|
||||
├─ div.header
|
||||
│ ├─ img.logo
|
||||
│ ├─ icon.xxxx
|
||||
│ └─ icon.yyyy
|
||||
└─ jigsaw-tab
|
||||
├─ Tab1
|
||||
│ ├─ ...
|
||||
│ └─ ...
|
||||
└─ Tab2
|
||||
├─ ...
|
||||
└─ ...
|
||||
|
||||
|
||||
这个例子里,有两个问题需要解决。一个是各个节点生成的 HTML 片段都不一样:
|
||||
|
||||
|
||||
jigsaw-box 节点需要生成这样的片段;
|
||||
div 节点需要生成这样的片段;
|
||||
jigsaw-tab 需要生成下面这样更复杂的片段:
|
||||
|
||||
|
||||
<jigsaw-tab-pane>
|
||||
<ng-template>
|
||||
</ng-template>
|
||||
</jigsaw-tab-pane>
|
||||
|
||||
|
||||
另一个问题是:需要把这些独立节点的 HTML 片段融合成一个整体。
|
||||
|
||||
如果你观察得足够仔细,就会发现 jigsaw-tab(页签组件)节点实际上是一个容器。每个 tab 页签内部完全有可能再放一个独立的页签组件,也就是它的内部完全可能包含另一棵类似这个例子的节点树。这样一来,这棵节点树就形成了一棵具有递归结构的树了。
|
||||
|
||||
最后还有一个重要的要求:为了保持代码有良好的封装内聚性,不允许采用 if else 的方式来解决节点之间的差异。
|
||||
|
||||
那怎么同时满足这三个要求呢?我采用的解决方法是,给每层数节点上都定义一个相同签名的函数(我们起名为 htmlCoder),每层节点的 htmlCoder 只干两件事情:
|
||||
|
||||
第一,如果该节点有子级,则正确地组织好参数并调用其子级节点的 htmlCoder,驱动其子级生成 HTML 片段;
|
||||
|
||||
第二,正确地生成好自己的 HTML 片段,该生成几层包裹层就就生成几层,并将子级返回的 HTML 片段与自身生成的 HTML 包裹片段,正确组装成一个合法的 HTML 片段,并返回给父级节点。
|
||||
|
||||
这段话虽然简洁但是比较绕。这里实际上是一个深度优先的递归调用过程,HTML 片段总是从最深处(树叶)开始真正组装完成,然后一级一级向树根递归出去。直到递归到树根时,一个完整的 HTML 代码也就生成好了。
|
||||
|
||||
用这个方法可以完美达成我预设的 3 个要求:节点与节点之间松耦合,没有 if else,且无论节点自身需要多复杂的 HTML 片段,都能满足。
|
||||
|
||||
其实如果你能熟练使用 OOP,大概现在就能猜到我在实现这部分代码的时候,会让描述节点的类都去实现一个包含 htmlCoder 这个函数的接口:
|
||||
|
||||
export interface IHtmlCoder {
|
||||
htmlCoder(sCode: StructuredCode, env: ProjectEnv): string;
|
||||
}
|
||||
|
||||
|
||||
这样做的好处,是在编译时就约束各个节点的代码必须要实现这个 htmlCoder 函数,并且函数的签名和返回值必须全部保持一致。
|
||||
|
||||
这样我们在运行时就不需要去判断某个节点实例是否有 htmlCoder 函数了。如果在代码长期维护过程发现 htmlCoder 的签名发生了变化,也不用担心哪些节点代码忘了对齐,因为编译器会不厌其烦地、一遍又一遍地检查所有节点的代码。
|
||||
|
||||
作为这个知识点的补充,你可以去找找我在 2021 年 12 月 GMTC 深圳站上关于低代码平台实现方法的演讲,在演讲的 17 分 36 秒到 20 分 43 秒,我从代码级详细介绍了这个知识点。这里我们就不再展开更多的细节了。
|
||||
|
||||
实际上采用相同的方法,可以把各个节点隔离得非常彻底,可以实现节点之间的极致松耦合,而 TypeScript 为我提供一个非常完善好用的 OOP 实现。在此我要顺道再安利一波 TypeScript!
|
||||
|
||||
如何应用这个方法?
|
||||
|
||||
那么低代码编辑器是如何使用这个方法来生成代码的呢?
|
||||
|
||||
编辑器侧重于交互的易用性和人性化设计,显然不可能按照生成代码所需的顺序设计交互过程,这就导致编辑器和人工编码类似,会在不确定的时间和位置插入某些功能逻辑。结构化代码生产法可以很好地满足编辑器的这个需求。
|
||||
|
||||
简单一点说,在新建一个 App 的时候,编辑器会创建一个类似下面结构的空白数据:
|
||||
|
||||
{
|
||||
importSection: [], constructorSection: [], memberSection: [],
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
然后将这个对象传递给各个编辑流程(可能是一个表单,或是一个文本框),所有编辑流程在采集到开发人员的编辑数据后,按照该功能所需的逻辑往这个对象不同片区插入数据。最后,编辑器通过我们前面介绍的方法,就可以将这个结构化的数据转为代码了。
|
||||
|
||||
虽然我用了 TypeScript+Angular 为例,介绍了结构化代码生成法的原理和简要实现方式,但并没有将它和这两者的任何特性绑定,所以你使用相似的原理和思路,完全可以做出一个可以生成 JavaScript+Vue 的代码生成器,抑或是用于生成 JSX+React 的代码生成器。
|
||||
|
||||
我已经将这个方法推广到了生成 TypeScript+Nodejs 的后端 Rest 服务的代码生成中了,整个推广过程异常顺利,几乎没有做任何修改就直接切换过去了,丝滑得有点不真实。实际上,任何一门指令式的计算机语言,都是可以采用相似的方法来生成代码的,也就是说,只要你愿意,你可以使用这个方法来生成 Java 代码,甚至是 C/C++/Python 等后台语言的代码。
|
||||
|
||||
不过,由于我本人比较痛恨函数式编程,对这种编码方式理解得并不深刻,所以此时此刻,我无法下结论是否这个方法也适用于函数式编程语言,但以我对 Scala 语言肤浅的了解,我觉得问题也不大。如果你对这方面比较了解,也欢迎你在留言区说说你的看法,我们一起讨论。
|
||||
|
||||
总结
|
||||
|
||||
这一讲中,我采用逆向工程法从实际代码反推出了代码生成器的工作原理,并将复杂的人工编码过程抽象成了向有限种类插入点添加代码片段的过程,再在编辑器各处按需往插入点中添加所需的代码片段。这样,我们对收集到的代码片段做过滤、合并和优化之后,再按照一定的顺序重新组装,就可以获得一份完整的代码了,这份代码与手写的代码相似,对人类友好,具有可读性。
|
||||
|
||||
为了帮助你更好地理解结构化代码生成法,在最后我再拿它生成代码的过程与手写代码做一次比较,其实结构化代码生成法创造代码的过程和你我日常写代码的过程,是一样的。你可能会反驳:我平时编码是在 IDE 上直接敲入一行行代码,从没用一个数据结构替代代码,更没将它存到哪个数组里。
|
||||
|
||||
假设有一个偏执狂码农,他写代码的时候总是极其严格地、甚至比编译器还严格地按照特定的顺序写代码。比如严格遵守这样的顺序:所有 import 代码→所有 HTML 模板→类声明→类构造函数→类成员变量→类成员函数→…,即使编译器允许某些代码交织在一起,他也会偏执地保持同一类代码聚集在一起。
|
||||
|
||||
此外,他还有令人发指地要求同一个片区内,后插入的代码一定是追加在最后面。在这个情况下,每类代码所在片区实际上就是一个数组,而他在编写特定功能代码时,就是按需在各个片区里插入代码片段。现在,你是否会觉得这个偏执狂码农的编码过程和我给出的结构化代码生成法生成代码的过程极其相似呢?
|
||||
|
||||
普通码农不会按照偏执狂码农的方式来编码,你我敲的代码不存在永不交织的代码片区,但对应的永不交织的逻辑片区是存在的,它们存在于你的逻辑思维中。所以我说,结构化代码生成法创造代码的过程和你我日常写代码的过程其实是一样的。至于结构化代码生成法是往代码片区里插入一个结构化数据,这只是为了在最后拼装成代码时可以更容易做优化而已。
|
||||
|
||||
也正是因为结构化代码生成法与人工写代码的过程实际上是一致的,最后我才能下这样的结论:结构化代码生成法几乎有无限的推广空间。
|
||||
|
||||
思考题
|
||||
|
||||
如果采用结构化代码生成法来生成 Java 或 C++ 代码,你会设计出哪些插入点?
|
||||
|
||||
欢迎在评论区写下你的想法。我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
197
专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md
Normal file
197
专栏/说透低代码/08|布局编辑器:如何做到鱼和熊掌兼得?.md
Normal file
@ -0,0 +1,197 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08|布局编辑器:如何做到鱼和熊掌兼得?
|
||||
从这一节课开始,我们正式学习 App 开发三部曲相关的内容,这三部曲分别是布局、交互和数据。这是 App 开发过程的三个主要步骤,也是业务团队开发 App 的三个主要工作内容。在时序上,这三个步骤并非顺序执行,而是交织进行的。但布局多数出现在 App 生命周期的早中期,交互和数据则集中在中晚期。
|
||||
|
||||
所以今天我就先来说说三部曲中的布局篇。顾名思义,布局就是按照 UX 设计稿或需求说明书里的草图,把需要的组件逐个放到界面上,并按照要求排列整齐,形成 App 雏形的过程。
|
||||
|
||||
Pro Code 开发模式下的布局过程是极抽象的过程,开发人员需要把形象化的 UX 设计稿转换为一行行抽象的指令,同时在脑海里想象这些指令的渲染效果。而在低代码模式下,布局过程是非常形象的过程。我们可以利用低代码编辑器的布局器,通过画布上的拖拉拽,可视化地完成这一过程。而且,由于新手初次尝试低代码开发所做的事儿就是布局,所以拖拉拽往往成了大家对低代码模式的第一印象。
|
||||
|
||||
显然,布局过程非常机械,低代码平台应该有能力自动化这个过程。所以,在专栏的最后一讲里,我会给你简单介绍实现一个 D2C(Design to Code)的思路,实现低代码平台的自动化布局。
|
||||
|
||||
先别急着去翻最后一讲!D2C 再牛再酷,也只是辅助手段,从设计稿里自动识别出来的 App 布局,也需要微调,如果后面业务需求更新了,还需要手工维护布局。因此 D2C 并不能替代今天这讲的内容,请耐心学习这一讲。
|
||||
|
||||
鱼和熊掌
|
||||
|
||||
首先,我们要明确,不同类型的业务场景下的布局器会有很大差别。
|
||||
|
||||
比如,表单场景总体上是以行为单位,自上而下布局。而且一行之中同时包含多个元素:标签、编辑器、附加说明、出错提示,等等。基于这样的特点,我们可以设计出类似下面这样的布局器:
|
||||
|
||||
|
||||
|
||||
这样的布局器针对表单场景来说,无疑是高效、易用的。但如果我们要用这个方式来布局一个 Dashboard 类 App,不仅毫无效率和易用可言,甚至连能否做到,都要打一个疑问号。因为,一个比较好的 Dashboard 场景布局器,应该是基于卡片的,要方便对大块的区域进行切分,从而快速获得尺寸合适的卡片,而且还要能方便地将小片区域融合成更大的卡片。
|
||||
|
||||
从前面举的两个例子可以看出,不同类型的业务场景的布局器确实会有很大差别。那么,有没有一种布局器能同时用于布局表单和 Dashboard 场景呢?
|
||||
|
||||
有!基于绝对坐标的布局器就能胜任。这种布局器很容易就能实现通过拖动来改变物体位置和尺寸的功能,比如下面这样的效果:
|
||||
|
||||
|
||||
|
||||
不过,这里要注意的是,布局器是基于绝对坐标实现的,但这并不意味着 App 在运行时无法获得具有弹性尺寸的页面。你可以先思考一下,我们后面就会给出解决思路。
|
||||
|
||||
好,现在问题来了。既然网格布局器(布局器背景有网格线辅助对齐,由此得名)可以同时实现表单和 Dashboard,那我们是否只实现网格布局器就好了呢?
|
||||
|
||||
当然不是。我们在关注功能的同时,还需要同时关注另一个维度:效率。我们可以很明显地看出来,基于行的布局方式开发表单的效率要远高于网格布局器,同理,基于卡片的布局方式来开发 Dashboard 的效率也远高于网格布局器。从这个角度看过去,网格布局器毫无优势。
|
||||
|
||||
好了,我们现在可以总结一下这部分了。效率和通用是两个相互制约的维度,无法同时获得,两者的关系可以定性地用下图来体现:
|
||||
|
||||
|
||||
|
||||
鱼和熊掌不可兼得
|
||||
|
||||
而且,效率和通用性不仅会在不同场景下相互制约,即使在同一个场景下,它们也会相互制约。我们拿 Dashboard 来举例,布局之初,页面有大片的空白区域,基于卡片的布局器可以快速地将空白区域切分为多个小卡片。
|
||||
|
||||
你可以看看下图,红线示意了卡片的切分过程,我们可以非常迅速地实现下面布局:
|
||||
|
||||
|
||||
|
||||
但如何在一个卡片内进行精细布局呢?如果继续采用切分的方式是否依然高效呢?
|
||||
|
||||
显然不是的。你看下上面这张图,随着布局越来越精细,一次操作可以影响到的界面面积越来越小。比如对一个卡片内部进行精细布局所需的操作次数,与对页面整体进行布局所需的次数可能还要更多。也就是说,对卡片内部的布局操作而言,相同布局操作的性价比就非常低了。
|
||||
|
||||
现在请你把目光聚焦在左下角那 3 个环形图上。对于这个区域,采用相同布局方式继续切分当然是可以做到的,但是效率并不高。下面这张图上,我把这个区域所需的布局操作用黄色线画出来了。你可以数一数,一共需要 9 次操作才能完成对这个小区域的布局,而在前面的图中,我们布局整个页面也才花了 8 次操作。
|
||||
|
||||
|
||||
|
||||
此时,如果我们换成网格布局或类似自由式布局方式,是否效率会更高一些呢?效率会更高,借助网格布局的快速对齐和空间自动分布工具,我们可以更快地完成这个布局。
|
||||
|
||||
因此,效率和通用性不仅在不同场景下有相互约束,在同一个场景的不同阶段,也有相互约束。那么面对二者选其一,我们是要效率,还是要通用性呢?
|
||||
|
||||
我都要
|
||||
|
||||
有道是:只有小孩子才做选择,成年人当然是全都要。如何做到效率和通用全都要呢?这就是我们这部分要解决的主要问题。
|
||||
|
||||
根据前面的分析,我们得到了这样的结论:靠单一的布局器是无法同时获得效率和通用能力的。所以,我们很容易想到可以采用组合的方式,兼顾高效率和通用能力。
|
||||
|
||||
现在,我们继续以前面那个垃圾分类页面为例讨论一下。我们已经知道,卡片布局器和网格布局器都具有很好的通用性。但在布局初始阶段,显然采用卡片的方式效率高,而在布局的后期,使用网格布局器进行精细化布局的效率更高。那么,我们将这两种布局方式组合使用,就可以得到一个既高效又通用的布局器了。下面这张图就描述了这样的过程:
|
||||
|
||||
|
||||
|
||||
现在,问题就简化成了如何实现多种布局器自由组合使用了。
|
||||
|
||||
为了更好地介绍具体的实现方法,我需要带你简要回顾一下【第 6 讲《踏出新手村便遭遇大 Boss:如何架构低代码的引擎?》】中的相关内容。在第 6 讲中,我将代码生成器与编辑器之间的关系分成了 4 个 Level,其中 Level 3 架构下,编辑器和代码生成器之间的关系变成了下面这样:
|
||||
|
||||
|
||||
|
||||
这个阶段,代码生成器与编辑器之间没有直接耦合。它俩各自与编译器协议产生耦合,布局器是编辑器的一部分。在这个架构下,只要各种布局器能严格遵守编译器协议来实现,那代码生成器就可以识别各个布局器的布局输出。
|
||||
|
||||
简单地说,无论布局器之间有多大差异,只要遵守同一套编译器协议,它们就可以玩到一块去,从而实现自由组合。此时的架构图可以细化成下面这个样子:
|
||||
|
||||
|
||||
|
||||
得益于编译器协议这层抽象,编辑器可以根据需要实现多种截然不同的布局器。并且,由于各个布局器都可以直接与代码生成器对接,所以从代码生成的角度来看,各个布局器之间已经是可以自由组合的了。这就好比是一片竹林,从地面上看有许许多多独立竹子,但他们的根是相同的。布局器就是地表一棵棵竹子,编译器协议就是它们的根。
|
||||
|
||||
相反地,如果你并未事先理清楚代码生成器与编辑器之间的关系,没有保持两者之间足够充分的松耦合关系,那此时此刻,你要做到任意布局器输出的数据都能和代码生成器无缝对接,就没那么容易了。
|
||||
|
||||
那这要怎么解决呢?一种原始做法是在代码生成器中采用 if-else 形式来处理不同布局器的差异。这当然是能走得通的,但问题是每多出一种布局器,判断的情况就会翻倍(指数增长),用不了三五种布局器,判断条件数量就将大到无法继续。相信我,这个过程将会相当痛苦。
|
||||
|
||||
现在,问题就进一步简化成了如何在编辑器的 UI 上实现各种布局器的相互嵌套使用的问题了。这样的问题,基本已经可以直接交给 UX 交互设计师和普通开发去处理了。实现方式也不止一种,这里我简要介绍一下我使用的方法,你可以参考一下。
|
||||
|
||||
我将布局器包装成了一种容器。容器也是一种组件,它具有组件的任何特性,但具备一个普通组件没有的能力:它能装得下其他组件或容器。容器支持相互嵌套的方式简直与布局器是天生一对。
|
||||
|
||||
最后,我们再以前面那个垃圾分类页面的布局过程为例,讨论一下多种布局器的配合使用过程。
|
||||
|
||||
首先上场的一般是卡片布局器,它可以快速地将页面横竖切分为多个卡片。在布局某个卡片内容时,开发人员可以根据卡片内容特征选择一种他认为更高效的方式来完成。他可以选择继续用卡片切分,也可以选择拖入一个网格布局器。
|
||||
|
||||
如果拖入了网格布局器,这个卡片的内容就变成了网格布局方式,可以发挥网格布局的优势实现高效精细化布局。同样地,如果该卡片的内容与表单更接近,他可以拖入一个表单容器,采用表单的方式进行布局。
|
||||
|
||||
可以看到,按页面特征挑选合适布局方式进行布局,可以发挥各种布局器的优势,又有效避免了它们的缺点,实现全程高效布局。鱼和熊掌你可以都要。
|
||||
|
||||
各种布局器
|
||||
|
||||
那现在,我们再来看看这几种常用的布局器特点、适用场景,以及关键实现思路。不过这里,我们先不深入介绍他们的实现细节,有机会我会再补充一下这方面的内容。
|
||||
|
||||
首先是网格布局器。
|
||||
|
||||
将网格布局器放第一个介绍,是因为它是我第一个做出来的布局器。第一个实现它是有原因的,因为它最主要特征就是通用。在不考虑效率的前提下,它可以在页面布局的各个阶段中用到,所以把它做出来之后,我们就可以一把搞定低代码编辑器的布局能力,非常划算。我推荐你也优先实现它。
|
||||
|
||||
在编辑态下,网格布局器采用绝对坐标来定位各个组件,所以非常容易实现鼠标点选、框选、拖动位置、拖动尺寸等基本编辑过程。当然,绝对坐标过于自由和精细,会导致很难实现画布上多组件的对齐,这点我们可以通过类似 PPT 那样的磁力线自动吸附来辅助解决。
|
||||
|
||||
虽然磁力线自动吸附很酷,但这不是最优解!主要有两方面原因:一是工作量,实际上这个功能还挺难实现的;但这不是关键,第二点才是关键否决票,UX 规范对组件间留白有严格规定,UX 小姑娘让你留 8px,你就不能留 7px 或者 9px。你想靠鼠标拖动来做到刚刚好 8px?别开玩笑了。为了解决这个问题,我们就需要开发出更加复杂的磁力线。
|
||||
|
||||
那最优解是啥呢?很简单,固定拖动步长。那么固定多少合适呢?我的选择是 8px。注意:一定要和 UX 团队取得一致意见,要求设计稿中,任何两个组件间的距离和尺寸必须是 8px 的整数倍。
|
||||
|
||||
其实,8px 不是协商来的,而是对已有设计稿分析后得出的,UX 团队也认为以 8px 作为基本单位约束所有 UI 是非常合适的。所以我们在画布上拖动组件位置和尺寸时,并不是丝滑的,而是以 8px 为单位一跳一跳的。这样做就省去了开发磁力线的麻烦,又可以很容易达成 UX 规范所需的留白要求,一举两得。
|
||||
|
||||
比如下图中的网格,就是 8×8px 的,网格布局器也由此得名:
|
||||
|
||||
|
||||
|
||||
那是不是网格布局器只能生成绝对坐标的页面呢?
|
||||
|
||||
显然不是的。这里就涉及一个复杂的分组算法。我们简要介绍一下,这个算法是根据平铺在画布上的各个组件的位置,对它们进行适当分组,同时对分出的各个组再进行相似的操作,直到无法再继续分组为止。这个时候我们就可以得到一颗有层级关系的树,之后我们再采用 Flex 布局就可以实现弹性了。如果要说清楚这个算法的实现,一讲内容可能都不够,所以我们只能留到以后再找机会补上了。
|
||||
|
||||
接下来是卡片布局器。
|
||||
|
||||
CSS 的盒子模型把二维 UI 做了极致的抽象。在 CSS 中,整个二维世界都是盒子组成的。无疑,这个抽象是成功的,但代价却是难用。
|
||||
|
||||
我在盒子模型的基础上做了一点点扩充,把盒子分成两种,一种是水平盒子,一种是垂直盒子:
|
||||
|
||||
|
||||
|
||||
左边是水平盒子,右边是垂直盒子
|
||||
|
||||
布局规则极简单:水平盒子会将它的直接子级按照水平排列,垂直盒子则将其直接子级垂直排列。关键点是,水平和垂直盒子支持任意层级的自由组合。这样做后,不可思议的事情就这样发生了,两个极简单的盒子通过自由组合,居然可以产生无限种可能,从而可以用它们来描述整个二维 UI。
|
||||
|
||||
这里我们还是以前面垃圾分类页面来举例。我们先看下面的动画:
|
||||
|
||||
|
||||
|
||||
我们来数一数这个界面包含了哪些盒子:
|
||||
|
||||
|
||||
土色部分是一个垂直盒子,只有 1 个;
|
||||
中间蓝色部分是水平盒子,有两个;
|
||||
最里头红色部分是垂直盒子,有好多。
|
||||
|
||||
|
||||
是不是很简单?
|
||||
|
||||
这个盒子布局的全部代码,都随着我们的低代码可视化组件集 Jigsaw一起开源在 GitHub 上了,如果你有兴趣的话,可以打开这里看看它的其他 demo。这里有两个 box 实现,一个是用于运行时的轻量化 box,一个是用于编辑时的 box,二者的实现差异非常大。
|
||||
|
||||
|
||||
帮忙点个星星哟!
|
||||
|
||||
|
||||
然后是表单布局器。
|
||||
|
||||
我在【第 5 讲《基础设施 :启动低代码平台研发之前,你需要有什么家底?》】中将市面上的组件集的封装方式归为 3 类,分别是数据驱动型、模板驱动型、以及 CSS 样式模板。
|
||||
|
||||
如果要将表单封装成一个组件,那显然采用模板驱动方式封装会更合适。因为表单对 UI 定制的需求巨大,非常难以按照数据驱动的方式来封装。但在那一讲中我也说了,数据驱动型的组件更适合用在低代码平台上。
|
||||
|
||||
这个矛盾如何解决呢?难道我们硬着头皮按照数据驱动方式来封装表单吗?
|
||||
|
||||
不需要,开源社区里已经有优秀的解决方案了。我这里给你两个推荐方案。首先,我在用的是ngx-formly,这是一个非常强大的数据驱动型实现,能满足我 99% 的需要(扣 1 分是因为它在复杂异步校验方面并不完美)。如果你恰好采用的是 Angular 技术栈,那么可以选它。Jigsaw 对应 ngx-formly 表单控件封装,我们也开源了,你可以看看。
|
||||
|
||||
另一个推荐的数据驱动表单的实现是阿里的Formily,支持 React 和 Vue,也是非常优秀的一个作品。
|
||||
|
||||
有了数据驱动型的表单的帮助,低代码编辑器只要采用可视化的方式生成一个复杂的数据对象,然后将这个数据对象喂给表单组件,就可以渲染出表单视图了。甚至,Formily还提供了可视化生成数据对象的设计器,你也可以考虑直接将它集成到你的低代码平台上,节约更多工作。
|
||||
|
||||
最后是流程编排器
|
||||
|
||||
这种布局器是特定场景下专用的,通用性不高,但和表单相似,在适用的场合下,会大幅提升布局效率。我主要将它用于流程审批类 App 的布局,简单地说就是开发一些流程审批类的 App 时,直接用它当做顶层布局器,然后各个流程环节配合使用表单等其他布局器,可以实现非常高效率的 App 开发效果。
|
||||
|
||||
同时,我们还可以将它用于数据编排场景(这不是 App)。服务端给的数据不见得都是合适前端使用的,多数情况下都需要做一定的编排组合、结构转换后才能给前端组件使用,这个过程就需要用到数据编排了。数据编排过程和流程编排非常相似。我在第 11 讲《业务数据:巧妇难为无米之炊,再好的 App 没有数据也是白搭》中会对这个过程有专门介绍。
|
||||
|
||||
具体实现方面,同样地,我并未自行实现编排功能,而是集成了社区里的开源软件X6,你可以参考一下。
|
||||
|
||||
小结
|
||||
|
||||
今天我详细介绍了 App 开发三部曲中的布局篇。我们知道,不同类型的 App,布局方式迥异,即使相同的 App 在不同开发阶段也有不同的布局需求。我认为布局器最主要需要满足两方面的诉求,一是通用性,二是效率。通用性是我们在低代码编辑器研发早期主要关注的维度,随着低代码编辑器越发成熟,对效率的追求就逐渐超越了对通用性的追求。
|
||||
|
||||
得益于低代码编辑器早期充分发展了通用能力,因此在它成熟之后,我们就可以把效率作为唯一的追求,将效率发挥到极致。为了做到这一点,我们甚至需要关注不同布局方式在同一个 App 的不同开发阶段下的效率表现,通过组合的方式来发挥不同编辑器的最大效率,并规避各自的短板。从而得到鱼和熊掌兼得的效果。
|
||||
|
||||
这一讲中,我还给你介绍了网格、卡片、表单、编排等 4 种布局器的特征和实现要点,同时也介绍了相关的开源资源。多种布局方式需要相互组合才能发挥各自的最大效用,我建议你将各个布局器当作容器来实现。容器天然具有多层级相互嵌套的特性,这使得各个布局器可以自由组合使用,应用团队可以按需、按 App 特征挑选合适的布局器来实现快速布局。
|
||||
|
||||
思考题
|
||||
|
||||
在你的场景中,网格、卡片、表单、编排哪种布局器的需求是最大的?除了这 4 种布局器之外,你还需要哪些布局器?
|
||||
|
||||
欢迎在评论区留言。我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
403
专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md
Normal file
403
专栏/说透低代码/09|属性编辑器:如何解除Web组件属性与编辑器的耦合?.md
Normal file
@ -0,0 +1,403 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09|属性编辑器:如何解除Web组件属性与编辑器的耦合?
|
||||
今天这一讲,我会带你推开编译器协议层的大门,并在协议层中实现一个功能,这个功能将会解除低代码编辑器和组件之间的耦合。我这里放了一张架构图,当然今天我们会对这个图进行详细讲解,现在你只需要有个大概印象就可以了:
|
||||
|
||||
|
||||
|
||||
在开始之前,我想请你思考一下这个问题:低代码编译器(指代码生成器)是怎么知道自己应该如何使用一个组件的呢?
|
||||
|
||||
这个问题乍一想挺简单的,但是思考越深,你会发现它越难。因为我们人类是通过学习组件 API 的方式来使用组件的,但编译器没有智能,它能像人一样去学习组件的 API 吗?不仅如此,我们还希望编译器除了“学会”内置组件集的用法,还能“学会”外来的其他组件,这可能吗?
|
||||
|
||||
教会编译器使用组件
|
||||
|
||||
图文是人与人之间传递知识最好的方式,就像这个专栏一样,我把我的知识以图文形式记录下来,你通过图文来学习。但图文对代码(编译器实质就是一串代码)是极不友好的,对代码友好的“教材”至少需要包含这些特征:
|
||||
|
||||
是指令式的:即这个“教材”必须是指出“怎么做”,而不是“做成啥”这种描述性的。
|
||||
|
||||
那如何给编译器提供一份符合这些特征的“教材”呢?
|
||||
|
||||
我们通过几个例子来逐步归纳。我在【第05 讲】讲低代码基础设施的时候,用到了一个表格的例子,我们以它为例。下面这些内容就是典型给人类阅读的 API 内容:
|
||||
|
||||
// html
|
||||
<jigsaw-table [data]="tableData"></jigsaw-table>
|
||||
|
||||
// script
|
||||
const tableData = new TableData();
|
||||
tableData.header = ['列1', '列2', '列3', '列4'];
|
||||
tableData.field = ['field1', 'field2', 'field3', 'field4'];
|
||||
tableData.data = [
|
||||
['cell11', 'cell12', 'cell13', 'cell14'], // 第1行
|
||||
['cell21', 'cell22', 'cell23', 'cell24'], // 第2行
|
||||
['cell31', 'cell32', 'cell33', 'cell34'], // 第3行
|
||||
['cell41', 'cell42', 'cell43', 'cell44'], // 第4行
|
||||
];
|
||||
|
||||
|
||||
这份手册包含了这几个主要信息:
|
||||
|
||||
|
||||
HTML 模板的写法;
|
||||
表格通过 data 属性来接收输入数据;
|
||||
如何在脚本中创建一个 TableData 对象。
|
||||
|
||||
|
||||
而且你要注意到,这份教材完全是描述性的,也就是这份教程是在告诉读者你要做成啥样。
|
||||
|
||||
如果把这份文档翻译成计算机教材,同时满足结构化和指令式,我们大概可以这样做:
|
||||
|
||||
{
|
||||
html: function() {
|
||||
return `<jigsaw-table [data]="tableData"></jigsaw-table>`;
|
||||
},
|
||||
script: function() {
|
||||
return `
|
||||
const tableData = new TableData();
|
||||
tableData.header = ['列1', '列2', '列3', '列4'];
|
||||
tableData.field = ['field1', 'field2', 'field3', 'field4'];
|
||||
tableData.data = [
|
||||
['cell11', 'cell12', 'cell13', 'cell14'], // 第1行
|
||||
['cell21', 'cell22', 'cell23', 'cell24'], // 第2行
|
||||
['cell31', 'cell32', 'cell33', 'cell34'], // 第3行
|
||||
['cell41', 'cell42', 'cell43', 'cell44'], // 第4行
|
||||
];
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
看起来开始有点样子了。但是这个“教材”所体现出的“表格通过 data 属性来接收输入数据”这个重要信息,好像有点问题,因为它在 HTML 函数里被写死了。写死 data 属性至少带来了两个问题:
|
||||
|
||||
|
||||
表格之外的其他组件,不一定有 data 属性,如果没有 data 属性,则生成的代码就错啦;
|
||||
表格的其他属性怎么办?
|
||||
|
||||
|
||||
看来,“教材”里不仅要包含 HTML 如何生成的信息,还需要包含属性如何生成的信息。所以,我们要把属性从 HTML 里单独拎出来,让它成为一个独立的函数。于是,这份教材就被改成了这样(没有变化的部分我省略了,后面的代码块也是这样):
|
||||
|
||||
{
|
||||
html: function() {
|
||||
const properties = this.properties();
|
||||
return `<jigsaw-table ${properties}></jigsaw-table>`;
|
||||
},
|
||||
properties: function() {
|
||||
return `[data]="tableData"`;
|
||||
},
|
||||
script: function() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这个版本最大的改进,就在于引进了 properties 函数,用来实现组件属性的动态创建。
|
||||
|
||||
接下来,我们再把目光挪到 script 函数,是不是感觉越看越不对劲儿?script 函数里的内容,全都是在为 data 属性服务的啊。从内聚的角度来说,这段代码应该放 properties 函数更合适,但是这里直接挪过去了。也不对,properties 函数处理的是 HTML 的一部分,和脚本无关。
|
||||
|
||||
这里,script 函数的位置之所以有争议,是因为 properties 函数身兼数职,它既要告诉编译器如何生成组件的一部分 HTML,又要告诉编译器如何生成对应的脚本。所以,问题就出在了 properties 的形式上了,它不应该是一个函数,而应该将它升级为一个类。
|
||||
|
||||
要先说明一下,从这里开始,这一讲中剩下的示例脚本都是用 TypeScript 来编写的,因为 JavaScript 已经不够我用了。不过,如果你对 TypeScript 不熟悉没关系,只要有 ES6 基础,从字面上理解并加一点猜测,也是可以理解的。
|
||||
|
||||
class Property {
|
||||
property() {
|
||||
return `[data]="tableData"`;
|
||||
}
|
||||
script() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
注意,这里我命名 Property 类的时候,用了单数,而非复数。因为从语义上说,我只指望 Property 类处理好一个属性就可以了,多个属性的管理不是 Property 类的职责。
|
||||
|
||||
相应地,那我们把前面那个包含 html 函数的对象,也改造为 TypeScript 类吧。改造后的关键代码如下:
|
||||
|
||||
class SVD {
|
||||
properties: Property[];
|
||||
html() {
|
||||
const prop = this.properties.map(p => p.property()).join(' ');
|
||||
return `<jigsaw-table ${prop}></jigsaw-table>`;
|
||||
}
|
||||
script() {
|
||||
return this.properties.map(p => p.script()).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
我这里增加了一个叫 SVD 的类,它用作描述这份给编译器的“教材”的入口。SVD 有一个名为 properties 的属性,它是前面创建的 Property 类的数组。这样看起来就顺溜多了。
|
||||
|
||||
此时,你可能会问一个问题:这份“教材”中有些内容是写死的,比如 jigsaw-table 这样的 selector,data 这样的属性等。
|
||||
|
||||
是的,所以它还需要进一步改造,我们要让这些写死的内容彻底动态化。这个改进实际上非常简单,我们直接看改好后的“教材”就行了,它变成了下面这样。
|
||||
|
||||
|
||||
首先是 Property 类:
|
||||
|
||||
|
||||
class Property {
|
||||
name = '';
|
||||
value = '';
|
||||
member = 'tableData';
|
||||
property() {
|
||||
return `[${this.name}]="${member}"`;
|
||||
}
|
||||
script() {
|
||||
return `
|
||||
const ${member} = new TableData();
|
||||
${member}.header = ${this.value.header};
|
||||
${member}.field = ${this.value.field};
|
||||
${member}.data = ${this.value.data};
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
然后是 SVD 类:
|
||||
|
||||
|
||||
class SVD {
|
||||
properties: Property[];
|
||||
selector = '';
|
||||
html() {
|
||||
const prop = this.properties.map(p => p.property()).join(' ');
|
||||
return `<${this.selector} ${prop}></${this.selector}>`;
|
||||
}
|
||||
script() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
提示:为了便于你理解关键信息,代码省略了所有的错误处理和其他细节。
|
||||
|
||||
|
||||
现在“教材”有了,接下来我们就要说说编译器如何“学习”了。
|
||||
|
||||
估计你现在也看出来了,这里的“教材”就是 SVD 和 Property 等类的代码,那么相应地,“学习”就是学如何执行这些代码了:
|
||||
|
||||
// 初始化2个类
|
||||
const dataProp = new Property();
|
||||
dataProp.name = 'data';
|
||||
dataProp.value = {
|
||||
header: ['列1', '列2', '列3', '列4'],
|
||||
field = ['field1', 'field2', 'field3', 'field4'],
|
||||
data = [
|
||||
['cell11', 'cell12', 'cell13', 'cell14'], // 第1行
|
||||
['cell21', 'cell22', 'cell23', 'cell24'], // 第2行
|
||||
// ...
|
||||
]
|
||||
}
|
||||
const tableSvd = new SVD();
|
||||
tableSvd.properties = [dataProp];
|
||||
tableSvd.selector = 'jigsaw-table';
|
||||
|
||||
// 执行并得到代码
|
||||
const html = tableSvd.html(); // <jigsaw-table ...></jigsaw-table>
|
||||
const script = tableSvd.script(); // const tableData = new TableData...
|
||||
|
||||
|
||||
你应该能看出来,这份“教材”在生成代码方面,实际上还比较粗糙,还有很多细节没解决,但作为示例来用是足够了。我们暂且不去纠结这些细节问题,而是继续从架构的角度讲演进和抽象。
|
||||
|
||||
不过,我接下来要讲的内容会用到 OOP(面向对象编程)思想,所以在开始之前,请确保你能比较准确地理解类的继承、覆盖、多态、接口和实现等概念。
|
||||
|
||||
到现在,这份“教材”中,表格只有一个 data 属性。显然表格不应该只有一个属性,现在我们假设表格有另一个名为 columns 的属性。这样一来,Property 类就会有问题了,因为很明显只用一个 Property 并不能很好地描述两个属性。一个非常合适的解决方案就是派生出 Property 的子类来处理多个属性,因此,Property 类可以改造成这样:
|
||||
|
||||
abstract class Property {
|
||||
// 这是一个抽象函数,只有声明,无法实现
|
||||
abstract script();
|
||||
|
||||
name = '';
|
||||
value = '';
|
||||
member = '';
|
||||
property() {
|
||||
return `[${this.name}]="${member}"`;
|
||||
}
|
||||
}
|
||||
|
||||
class DataProperty extends Property {
|
||||
name = 'data';
|
||||
member = 'tableData';
|
||||
// 在子类中,script函数就知道如何实现了
|
||||
script() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
class ColumnsProperty extends Property {
|
||||
name = 'columns';
|
||||
member = 'tableColumns';
|
||||
// 各个子类需要生成不同的脚本
|
||||
script() {
|
||||
return `
|
||||
const ${this.member} = [];
|
||||
...
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
你可以看到,我把 Property 类改成了 abstract(抽象)的了。因为从基类的角度看,它完全不知道如何去实现 script 这个函数,只有在具体的子类中才知道如何实现这个函数。所以我们派生了一个 DataProperty 子类用来描述表格的 data 属性,同时又派生了一个 ColumnsProperty 子类用来描述表格的 columns 属性。同理,表格其他的使用属性,也可以采用相似的方式来解决。
|
||||
|
||||
那么相应地,我们调用的代码也需要做些调整了:
|
||||
|
||||
const tableSvd = new SVD();
|
||||
tableSvd.selector = 'jigsaw-table';
|
||||
tableSvd.properties = [
|
||||
new DataProperty(), new ColumnsProperty(), new XxxProperty()
|
||||
];
|
||||
|
||||
|
||||
经过多个版本的演进,目前来看,这份“教材”终于可以很好地教会编译器如何使用 jigsaw-table 组件了。而且,我们还顺便对 Property 类做了一个重要的改进,通过派生子类的方式解决了冗余代码问题。后面,我们也可以把类似方法用到对所有组件的“教学”上。
|
||||
|
||||
学会使用任何组件
|
||||
|
||||
不过,到现在,代码编译器也才学会了如何使用表格这一个组件,接下来我们要做的当然是教会它处理多个组件如何描述的问题了。
|
||||
|
||||
我们采用和 Property 相似的方法,把 SVD 作为基类,用来描述组件中相同部分的逻辑,而各个组件特性部分的逻辑则放到子类中去,改造后的代码为:
|
||||
|
||||
abstract class SVD {
|
||||
properties: Property[];
|
||||
selector = '';
|
||||
// 能在基类里实现的方法都尽量在基类提供实现,这样子类直接复用就好了
|
||||
html() {
|
||||
// ...
|
||||
}
|
||||
// 和Property类似,基类里不知道如何实现的方法,都只声明,不实现
|
||||
abstract script();
|
||||
}
|
||||
|
||||
|
||||
那么,表格的教材也应该相应地调整为:
|
||||
|
||||
class JigsawTable extends SVD {
|
||||
properties: Property[] = [
|
||||
new DataProperty(), new ColumnsProperty(), new XxxProperty()
|
||||
];
|
||||
selector = 'jigsaw-table';
|
||||
script() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
此时如果再来一个其他组件,比如 select 组件,我们的代码编译器也可以轻松支持了。
|
||||
|
||||
class JigsawSelect extends SVD {
|
||||
properties: Property[] = [
|
||||
new DataProperty(), new ValueProperty()
|
||||
];
|
||||
selector = 'jigsaw-select';
|
||||
script() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
我们可以看到,教会编译器使用组件,只是我们对这份教材多次演进的一个次要目标,我们更主要的目的是从架构上调整教材,让它有一层抽象层和一层实现层。
|
||||
|
||||
基于这样的层次架构,新来的任何组件,只要实现了抽象层中的方法,以及按需覆盖抽象层里的已有方法之后,编译就可以学会如何使用它了。
|
||||
|
||||
不知道你有没有注意到,我所说的“任何组件”不仅包括了低代码内置组件集里的任何组件,更包括了其他未知来源的组件集里的组件。也就是说,以后业务团队要是问你,你的低代码平台能否不用内置组件集,而使用一套他们指定的组件集,你就可以很爽快地答复“没问题”了。这就是低代码平台通用能力的一种体现。
|
||||
|
||||
当然,完全拒绝使用内置组件集的情形并不多见,但业务团队要求补充少量业务组件到低代码平台中来是一种非常常见的情形。
|
||||
|
||||
业务团队往往会封装出若干非常适合他们内部使用的业务组件,虽然这些业务组件没啥通用性可言,但如果这些业务组件无法在低代码平台使用,就会大大降低他们的开发效率。再加上人人都有的敝帚自珍的情感,在这些团队中推广低代码平台的阻力将会巨大。反之,如果低代码编译器能通过某种方式学会使用这些业务组件,并将他们纳入到平台中使用,那推广起来就相对容易了。这样的情形在我推广 Awade 过程中,非常常见。
|
||||
|
||||
编译器 SDK
|
||||
|
||||
好了,前面做了这么多铺垫,这里终于可以来说说一个重要的知识点,如何提取编译器 SDK 了。我们前面花了九牛二虎之力,从架构上把编译器的教材分成了两层,其中有一个是抽象层,我把代码拎到一起:
|
||||
|
||||
abstract class Property {
|
||||
// 这是一个抽象函数,只有声明,无法实现
|
||||
abstract script(): string;
|
||||
|
||||
name: string = '';
|
||||
value: string = '';
|
||||
member: string = '';
|
||||
property(): string {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
abstract class SVD {
|
||||
// 和Property类似,基类里不知道如何实现的方法,都只声明,不实现
|
||||
abstract script(): string;
|
||||
properties: Property[];
|
||||
selector: string = '';
|
||||
// 能在基类里实现的方法都尽量在基类提供实现,这样子类直接复用就好了
|
||||
html(): string {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
这部分代码对任何已知和未知的组件来说,都是通用的。并且,任何要纳入到低代码平台的组件都需要继承这些基类,派生出描述具体某个组件如何使用的子类。
|
||||
|
||||
那么这些子类应该由谁来实现呢?全部由平台团队来实现吗?业务团队能否参与进来?为了能让业务团队也参与进来实现这些子类,我们应该将这些基类代码从平台的代码仓库里独立出来,以 npm 包的形式发布出去。这样一来,平台团队也好,业务团队也罢,都可以通过 npm 拿到这个包,从而继承包里的基类来实现子类的开发了。
|
||||
|
||||
没错,这样的一个 npm 包,就是低代码平台插件系统的 SDK 的一部分。这个包处于下图中蓝框中的“协议部分实现”这层中:
|
||||
|
||||
|
||||
|
||||
现在,再来看这张图,你会有更深的理解。编译器协议是抽象的,因为它只有一部分实现(参考前文的 Property 和 SVD 这两个抽象类),但抽象的基类完整地描述了生成代码会用到的所有 API 的定义。基于这份完整的 API 描述,我们就可以写出代码生成器的所有代码了,这份 API 描述就是图中所说的“编译器协议”。
|
||||
|
||||
当然,此时的代码生成器是无法正确运行的,因为协议层中只实现了一部分函数,另一部分函数只有定义,没有实现。那么缺失的这部分实现由谁来补呢?这项工作可以由平台或业务团队任何一方来完成,平台团队负责实现内置组件,业务团队负责实现自定义组件。
|
||||
|
||||
此外,SDK 还需要提供一个组件“教材”工厂,用来登记和生产各个组件的教材。教材工厂的实现比较简单,是一个典型的工厂模式,你可以参考这段代码:
|
||||
|
||||
// 实际开发时就不会用“教材”这样的称谓了,awade将这部分命名为metadata
|
||||
class MetadataFactory {
|
||||
// 用于登记组件教材,不熟悉TS的小伙伴,对Type<SVD>这个类型可能理解略费劲
|
||||
// Type<SVD>这个类型代表着一个组件类(而非该类的实例)
|
||||
register(selector: string, component: Type<SVD>): void;
|
||||
// 用于获取组件教材类(而非组件实例),拿到组件类后,再new一下就可以得到实例
|
||||
getMetadata(selector: string, rawSvd: object): Type<SVD>;
|
||||
}
|
||||
|
||||
|
||||
这里你应该注意到,这个工厂将组件的 selector 作为组件的身份证了。把“教材”,即所有子类(JigsawTable / JigsawSelect / DataProperty / ColumnsProperty)都聚到另一个 npm 包,并调用工厂的 register 函数挨个在工厂里登记。
|
||||
|
||||
不过,这里别忘了,我们还有一个组件集的 npm 包。把这些包都画到一个图上是这样的:
|
||||
|
||||
|
||||
|
||||
我们可以看到,“教材”、编辑器、代码生成器等模块都是低代码平台正常运行不可获取的重要部分,但图中它们三者之间却没有直接的耦合关系,相互隔离。这样的代码层次分明,职责明确,是一种非常好的关系结构:
|
||||
|
||||
|
||||
编辑器负责收集开发者的编排结果,并驱动代码生成器来生成代码;
|
||||
代码生成器则根据编辑器收集到的原始数据,从工厂中获取“教材”的实例,然后执行 script 函数获得代码;
|
||||
“教材”负责描述各个具体组件的代码应该如何生成。
|
||||
|
||||
|
||||
我们再看编辑器和组件(集)之间,你会发现它们之间没有任何关系,甚至连间接依赖都不存在。这非常违反直觉,是不是觉得很不可思议?编辑器的所见即所得功能,就是时时刻刻在动态渲染组件,但在代码的结构中,这两者之间却没有任何关系。
|
||||
|
||||
我们千方百计地把组件和低代码核心模块编辑器分开的,根本目的就是为了达到下面这个图的效果:
|
||||
|
||||
|
||||
|
||||
这张图中,右边红框里的是业务团队自行定制的组件。你看,这不就是一个插件吗?左侧绿色框是内置组件集,它和右侧插件的结构是一致的,所以内置组件集的和插件提供的组件是平起平坐的,架构上,插件并不低人一等。
|
||||
|
||||
总结
|
||||
|
||||
今天这讲,我们通过代码实例的演进,非常详细地说明了如何一步步解除低代码编辑器、代码生成器和组件三者之间的耦合关系。
|
||||
|
||||
我们从一份给人类阅读的常见 API 手册出发,将它改造为具有结构化和指令式的特征的一份教材,这样做的目的是让教材更加适合计算机阅读。然后,针对组件的多个属性,我们抽取出了一个属性的基类 Property,把通用的代码提取到基类 Property 中。
|
||||
|
||||
虽然组件属性基类并不知道如何生成代码,但我们做了一个非常重要的动作:在基类中定义了一个抽象的 script 函数,这个抽象函数的最大作用在于规定了组件属性的子类应该如何完成代码的生成。由此,虽然我们并没有完整实现组件属性基类,但是它却完整地定义了属性代码生成的流程。
|
||||
|
||||
接下来,采用相同的方法,我们抽取出了 SVD 类作为组件的基类,并也定义了一个抽象函数,用于规定组件的子类该如何生成代码。
|
||||
|
||||
到此,这一讲设定的目标就已经完成了,编辑器可以在基本无耦合的前提下调用组件的“教材”来生成正确的代码。但是我们并没有停下脚步,而是将这个方法进一步演进,做出了这个专栏迄今的第一个插件:业务组件。低代码编辑器在和业务组件完全没有耦合的情况下,通过插件,就可以知道如何正确地生成和渲染业务组件。而且,在这讲中,我们也首次实现了一条允许业务团队对通用型低代码平台做定制的通道。
|
||||
|
||||
思考题
|
||||
|
||||
你所在的团队有哪些功能适合内置到低代码平台作为通用组件来实现,哪些功能适合以成插件的形式集成到低代码平台中去?为什么?
|
||||
|
||||
欢迎在留言区写下你的想法。我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
213
专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md
Normal file
213
专栏/说透低代码/10可视化编程:如何有效降低App前后端逻辑开发的技能门槛?.md
Normal file
@ -0,0 +1,213 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 可视化编程:如何有效降低App前后端逻辑开发的技能门槛?
|
||||
今天我们来聊聊低代码平台实现可视化开发过程中一个难点功能:可视化编程。可视化编程解决的是应用开发三部曲(布局、交互、数据)中的交互环节。
|
||||
|
||||
但这样说有点狭隘,如果低代码平台同时支持开发后端 Rest 服务,那可视化编程的方法可以完全复用到后端的 Rest 服务开发中,而不仅限于前端交互逻辑的开发。因此,这一讲的内容实际上同时覆盖了前后端的低代码实现,如果没有特别的说明,这讲的所有内容都适用于前后端低代码场合下使用。
|
||||
|
||||
在开始之前,我想请你想一想这个问题:编码难在哪?
|
||||
|
||||
作为一个写了近 20 年代码的职业码农,面对这个问题,我的第一感觉是难点很多,数都数不清,但要列个一二三来,又觉得不好下手。仔细一想,编码就像艺术创作,比如绘画,虽然绘画有一定的套路,但从开始到最终完成,有着巨大可自由发挥的空间,而填满这些自由发挥空间的,只能是作者的经验。并且,决定一幅画是否有灵魂的,也只能是作者的经验。
|
||||
|
||||
编码何尝不是这样呢?大概套路是有的,但细到每一个函数、每一个类如何编写,则完全由开发人员的经验决定。专家写的代码不仅性能好,bug 少,而且可读性非常高,反之,缺乏经验的开发人员能按预期把功能跑通就不错了,哪还顾得上可读性或性能。那有没有一种方法,可以让新手也可以写出专家级的代码呢?这正是我们今天要解决的问题。
|
||||
|
||||
可视化逻辑编排
|
||||
|
||||
代码具有非常强的流程逻辑,所以可视化编程要解决的第一个问题就是如何进行逻辑流程的编排。
|
||||
|
||||
编码时的流程逻辑是通过一行行代码自上而下来体现,可视化逻辑编排需要对逻辑有不同的组织方式。一种比较常见的逻辑可视化组织方式是流程图,通过流程图的形式来表达一个逻辑过程是非常自然的想法。
|
||||
|
||||
比如下面这个流程图,描述了一个订单审批的过程,看上去逻辑是比较清晰的:
|
||||
|
||||
|
||||
|
||||
你可以注意到,这个简单的流程图里包含了代码逻辑的三种基本控制结构:循环结构、选择结构、顺序结构,并且这三种结构在图中的呈现和融合都非常自然。关键是,BOHM & Jacopini 早在 1966 年就从理论上证明了,只要能同时支持这 3 种结构的流程,就可以表达任何复杂的程序逻辑。因此,至少在理论上我们不需要担心这个方式的可行性。
|
||||
|
||||
而且,你肯定听过,或者用过“一图胜千言”这句话,它指出了我们人类大脑对所处理的信息的“偏好”。人类的大脑和计算机不一样,人类对可视化的信息(比如一张图、一幅画)的处理效率要远高于其他信息(如文字)。实际上,人脑的 80% 功能都是用于处理视觉信息的,人们对接收视觉信息具有天生的敏感度。
|
||||
|
||||
所以,无论从计算机理论,还是从人脑认知的角度看,流程图式的逻辑编排是一种对大多数人都非常友好的方式,我们在实现可视化逻辑编排时,流程图式的逻辑编排是一种不错的备选。
|
||||
|
||||
接下来,再继续深入。我们日常写代码,有些逻辑是要复用的,我们常常使用函数和类来解决代码复用的问题。那么,流程图能搞得定吗?
|
||||
|
||||
在展开这个知识点之前,我要先提醒一下,可视化逻辑编排,无需要复刻任何一种编码技巧。不仅如此,实际上,可视化编程只需要实现极少量的编码技巧就可以了。
|
||||
|
||||
为啥?因为低代码平台的可视化开发手段多种多样,可视化编程只是其中的一种方法。实际上,低代码编辑器的主要职能就是实现各式各样的可视化开发方式,而这门课的主要内容就是在介绍低代码编辑器。比如前一讲我详细介绍了 App 的布局,这也是一种可视化开发手段。这样应该好理解吧?
|
||||
|
||||
那么,可视化编程必备的编码技巧是啥呢?我认为只需有函数定义和调用就足够了,连类都用不上。在低代码意义下的可视化编程,实际上只需要完成交互逻辑即可。
|
||||
|
||||
而一个 App 代码的架构、框架部分的代码是不需要低代码平台的使用人员来实现的,更无须通过可视化方式生成的这些代码,这部分的逻辑,低代码编译器会 100% 生成。通过可视化方式编排出来的逻辑,会被低代码编译器添加到它自动生成的 App 的框架代码中去。因此,需要低代码的使用者通过可视化编程方式开发的逻辑,是简单而少量的。
|
||||
|
||||
那么,我们收回来,采用流程图式的逻辑编排如何来定义和使用函数呢?我认为有两种方法:第一种是显式定义,这是比较比较通用的方法,直接使用一个独立的流程来定义函数的逻辑,并允许在另一个流程里调用;第二种是融合式,也就是将函数“藏”到流程图里。
|
||||
|
||||
方法一很好理解,实现起来也简单,不需要过多讨论,我们主要说说方法二。
|
||||
|
||||
你先想想,一个逻辑流程图能有几个入口和几个出口?一进一出?一进多出?这两种就是最普通的流程图了,对应类似下面这样的逻辑结构:
|
||||
|
||||
function f() {
|
||||
// ...
|
||||
if (xxx) {
|
||||
return 1;
|
||||
}
|
||||
// ...
|
||||
if (yyy) {
|
||||
return 2;
|
||||
}
|
||||
// ...
|
||||
if (zzz) {
|
||||
return 3;
|
||||
}
|
||||
// ...
|
||||
return 4;
|
||||
}
|
||||
|
||||
|
||||
那如果是多进多出呢?又是啥情况?
|
||||
|
||||
仔细一想,好像无法与任何代码逻辑结构对应上啊。其实,多进多出是有的,而且多进多出的流程同时把函数的定义和调用都描述出来了。似乎有点难理解?那我们先看看下面这个图:
|
||||
|
||||
|
||||
|
||||
上面这个动画演示了一个多入口流程图。从上图可以看到,它包含了两个独立的工作流。你可以认为一个工作流就对应着一个事件回调函数,或者一个工作流对应着一个 rest 服务。在这个情况,两个工作流重叠部分实际上就是一个函数了。
|
||||
|
||||
是不是很哇塞?竟然还能有如此优雅自然的函数定义和复用方式!
|
||||
|
||||
如果是一个完全没有编程经验的人来用方法一,你是不是还要费一番口舌才能让他理解啥是函数,并且还要求他必须养成先定义后使用的习惯?而采用方法二呢,函数在哪?在哪调用?既在图上,又不在图上,这种方式对人脑非常友好。这就是可视化编程的魔力。
|
||||
|
||||
但是!没有一种方法是完美无瑕的。流程图方式编排逻辑方法的最大问题在于,当逻辑复杂之后,它的可读性和可维护性会极大下降。
|
||||
|
||||
比如下面这个申请商标的流程,你可以先看看:
|
||||
|
||||
|
||||
|
||||
你看懂了吗?反正我没看懂。
|
||||
|
||||
那么这个问题应该怎么解决呢?有两个方法。首先我们想到的显然是折叠和展开了。之所以图会复杂,就是因为展示了过多细节,只要我们把不必要的细节隐藏起来,问题就可缓解了。目标确实非常明确,但是如何折叠?如果框选一部分节点后,不一定能叠起来呢?
|
||||
|
||||
|
||||
|
||||
你看,现在上图选中的 3 个节点,就无法折叠,但如果我们把下面那个 Resubmisstion 一起框起来,貌似这块就可以折叠了。那能折叠和不能折叠的特征是啥,它们怎么区分呢?
|
||||
|
||||
很简单,就是数一下框住的所有节点有几进几出就好了。一进一出时就可以折叠,否则就不能折叠。你可以再看看下面框住的 5 个节点,一进一出,所以它们可以折叠。反之这 5 个框少了哪个,都不能折叠。
|
||||
|
||||
|
||||
|
||||
这里我留个小问题给你,如果不选择图中的 Upload 节点,那么此时被框住的 4 个节点一共是几进几出?欢迎在评论区留言。
|
||||
|
||||
把这 5 块折叠以后,它们就变成了一个子流程了,然后我们再把前文的 4 个节点也叠起来,此时的逻辑图是这样的,看起来就简洁多了:
|
||||
|
||||
|
||||
|
||||
要提升复杂逻辑的可读性,其实还有另一种方法,就是采用树状形式来组织逻辑。比如,把例子中的流程图改写为树状形式,是这样的:
|
||||
|
||||
|
||||
|
||||
显然,采用树状形式来组织逻辑没有流程图那么“酷”,但好处是不挑逻辑复杂度:几乎任何复杂度的逻辑采用树来呈现后,都差不多,而且折叠展开也非常方便。而且我们可以通过拖放树节点的方式,做到对已有逻辑的大调整,这是流程图无法比拟的。
|
||||
|
||||
|
||||
|
||||
那么这两种逻辑组织方式,是否只能非此即彼呢?
|
||||
|
||||
不是的,满足一定条件时,两种方式是可以相互转换。比如,在选中的节点只有一个入口(出口数量不限)时,这两种逻辑组织方式是等价的,可以相互转换。
|
||||
|
||||
不过,到这里,我们都在讨论逻辑流程如何编排。接下来我们更进一步,请你把眼光聚焦到流程(或逻辑树)中的单个节点,我们来看看如何实现可视化开发单个节点的逻辑。
|
||||
|
||||
填空即开发
|
||||
|
||||
如果说,可视化逻辑编排是编码时的编辑器输入框,那么编排界面上的各个节点就是代码输入框里的一行行代码。当然,这句话说得不全对,一个节点不一定对应着一行代码,有可能对应着几行甚至更多的代码,这是节点与代码行之间表面的差异,它们的本质差异是:节点是功能层面的概念,而代码则是实现层面的概念。
|
||||
|
||||
为了更好理解,我们做一个类比:如果说代码是一个个原子,节点则是一个个的分子。高中化学告诉我们,分子都是由原子通过不同的排列顺序组合而成的,原子不具有化学性质,而分子是有化学性质的。一堆氧原子不能用于你呼吸,而两个氧原子结合在一起之后成了氧气,氧气是可以拿来呼吸的。
|
||||
|
||||
代码和节点的关系,与原子和分子的关系非常相似。单独一行代码可能没有任何业务意义,但是多行代码按一定顺序组合在一起,就可以形成特定功能。因此,一个节点是由一行或多行代码组成的,具有特定功能。所以我说代码是实现层面的概念,而基于代码组合而成的节点,则是功能层面上的概念。再延伸一点,如果非要在代码层面上为功能节点找一个位置,你可以把它当作一个函数或者一个类来看待。
|
||||
|
||||
理解了代码和节点的关系之后,设计单个节点的方法就明确了:找功能(而不是找代码)。比如创建订单功能,或是订单审批功能,或是发送电邮功能,等等,还有很多很多。这样看节点的数量就没完没了了。
|
||||
|
||||
节点的功能有大有小。功能越大,意味着使用的条件就越苛刻,所需的功能数量也就越多;反之,功能越小,单个功能的可用场景就越多,通过他们的组合就能形成新的功能,因此,功能节点的数量需求就越少。
|
||||
|
||||
那么,你一定会追问,功能多小算小呢?这个问题需要分通用和业务两个层面来回答。先说业务层面,这方面没有标准,只能根据实际业务情况而定。与业务无关的通用功能,则相对明确,我们来详细说说。
|
||||
|
||||
所谓的通用功能节点,指的是与编程语言、组件操作等紧密相关的那些功能。比如下面这个图基本就把大多数通用功能节点分类都列举出来了:
|
||||
|
||||
|
||||
|
||||
你可以根据上面这个图为线索来设计你的通用功能节点。
|
||||
|
||||
接下来,我就以条件判断这个功能节点为例,说明功能节点如何实现。因为我们这一讲专注的是可视化编程,那么功能节点的使用当然也是可视化的了。我们可以把一个功能节点粗糙地理解为一个函数,类似的,功能也是有输入参数和返回值的。
|
||||
|
||||
我们先来看看输入条件可视化配置。
|
||||
|
||||
即使是一个条件判断功能,也是要有输入参数的,比如下面这个图,展示了条件判断功能的配置界面:
|
||||
|
||||
|
||||
|
||||
是不是没想到一个看似简单的条件判断功能节点的输入条件,也能玩出这么多花样来?这样的设计主要有两方面考虑:
|
||||
|
||||
|
||||
判断方法为“表达式”是为方便有少量编程技能的用户使用,直接写一个表达式可以避免啰嗦界面配置,注意这里我把“表达式”设置成了默认值;
|
||||
其他判断方法,基本都是给无编程技能者使用的,它覆盖了 JavaScript 常见的条件判断方法。
|
||||
|
||||
|
||||
基本上,其他功能节点都可以采用类似方式来处理输入参数的选择。
|
||||
|
||||
接下来我们说说功能节点的结果可视化配置。
|
||||
|
||||
无论功能节点生成的代码有多少,总要通过它的结果对外产生影响,而且这个结果必须交到开发者手里,进而开发者可以将上一个功能节点的结果作为下一个功能节点的输入,从而最终形成一串逻辑。想象一下,逻辑编排出来的逻辑就像一根管子一样,数据就是管子里的水,水从管子的一头流向另一头,中间穿过一个个节点,每一个节点都对数据产生一点影响,最终所有影响叠加到一起,就是开发者所需的最终结果。
|
||||
|
||||
那么,如何可视化配置呢?其实超级简单,比如下图的最后一行,有两个输入框,它们都接受一个字符串作为变量名来接收功能节点的结果:
|
||||
|
||||
|
||||
|
||||
与函数的返回值不同,我要求开发者直接提供变量名给我,动作节点对应的代码计算好了后,把结果直接赋值给变量,这样一个配置就可以解决许多问题。
|
||||
|
||||
好了,我们现在可以来解释一下这部分的小标题为啥是填空即开发了。每个功能节点都有输入条件和结果需要配置,因此我们为每个功能节点都定制了一个表单,接收开发者对当前功能节点的配置,这就是“填空”的过程。
|
||||
|
||||
那填空后呢?填好后,功能节点就根据开发者填写的内容,把代码正确生成出来,这便完成了 Pro Code 模式下的“开发”过程。我把这个过程简洁地概括为:填空即开发。
|
||||
|
||||
兜底策略
|
||||
|
||||
接下来,我们需要讨论一下兜底策略在可视化编程中的应用。
|
||||
|
||||
无论可视化编排多么强大,无论功能节点多么丰富,无论这两者多么易用、讨喜,但是你必须承认,它们有可能无法解决所有问题,或者在特定复杂场景下,它们的效率比不上 Pro Code 方式。这个时候,我们就需要启用兜底功能了。
|
||||
|
||||
先别慌,其实兜底功能实现起来非常简单。先回顾一下咱这个专栏所说的低代码的一个重要特点:它和 Pro Code 一样,也是基于代码来构建的应用的。这个特点在今天这讲的内容中有多处体现,比如功能节点在填空背后,实际是把代码生成出来了。
|
||||
|
||||
既然填空 = 代码,那是不是可以给开发者一个代码编辑器,让开发者自己把代码填上去,就能解决所有可能出现的问题呢?
|
||||
|
||||
答案是肯定的!所以说兜底策略在这里的实现其实非常简单。我这里放了两张图,是我们低代码平台 Awade 现在用的方法,你可以参考一下:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
简单粗暴,但是好用!
|
||||
|
||||
虽然功能实现确实比较简单,但写过代码的人都知道,一个 IDE 绝对不仅仅是能敲代码,能跑就可以的。这些功能可能还占不到 IDE 功能的 10%,其他的如智能提示、出错提示、编译错误等功能是非常影响编码体验和开发效率的。虽然我们这里只是一个代码输入框,但往大了说,你应该把它当作一个 WebIDE 来理解。
|
||||
|
||||
但是这个功能点的实现弹性是非常大的,你可以根据你手里的资源,决定要把这个代码编辑器做到哪个程度。
|
||||
|
||||
总结
|
||||
|
||||
可视化编程是可视化开发中的一个难点,总体可以分成两个主要功能点:可视化逻辑编排和功能节点。限于篇幅,这一讲我们没有从代码实现层面讨论具体如何实现,但是我已经从方案和特性层面将这两个功能点基本吃透了。后续在动态更新部分,我会从代码实现层面来说说这两个功能落地过程中的难点。
|
||||
|
||||
我介绍了两种可视化逻辑编排的方法,分别是流程图式和逻辑树式,这两种方式可以相互结合,而不是非此即彼。可视化逻辑编排的方式,不仅可以很好地解决程序逻辑的编排场景,实际上,这个方式还可以独立出来作为一个独立的 App 场景:流程审批场景。我们只要再加上泳道,事件等功能的话,再采用这讲给出的方法,几乎可以直接实现一个符合 BPMN 规范的场景。以后有机会我们展开讨论这个话题。
|
||||
|
||||
在功能节点配置界面上,开发者填的是数据,而代码是自动生成的。不仅如此,功能节点的实现靠的不是前后端的语言专家,就是业务专家,功能节点将这些专家的经验凝聚在了它生出的代码中。所以,即使是一个无技能者,只要他正确填写了表单,就可以得到专家级的代码。这就是低代码平台的能力!
|
||||
|
||||
这一讲中我只详细讨论了通用型节点的实现,对业务功能型节点则是一带而过。其实,对于一个通用型低代码平台来说,其职责也主要在通用型节点的实现,至于业务功能型节点,我建议你统统移到插件中来定制。
|
||||
|
||||
这么做有两个原因,一是业务功能节点数量非常多,胡子眉毛一把抓地全放到平台上,会非常乱;二是恐怕平台团队不知道如何去实现业务功能节点,甚至连要哪些功能节点都不知道,这本就是业务团队才知晓的知识。所以平台团队应该与应用团队合作来开发插件,这样才能利用好业务专家的经验。
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
你认为哪些功能节点是比较常用,需要第一优先级实现的?条件判断、数组操作、字符串操作,还有其他哪些呢?
|
||||
有的功能节点是异步的,可视化逻辑编排如何自动处理异步功能节点?这是一个有点难度的问题,欢迎挑战一下,把你的方案留在评论区。
|
||||
|
||||
|
||||
下一节课我们将要讨论可视化编程中的高低代码混合开发模式,你可以做些准备。我们下节课见。
|
||||
|
||||
|
||||
|
||||
|
210
专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md
Normal file
210
专栏/说透低代码/11|亦敌亦友:LowCode与ProCode混合使用怎样实现?.md
Normal file
@ -0,0 +1,210 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11|亦敌亦友:Low Code与Pro Code混合使用怎样实现?
|
||||
今天我们来聊聊低代码平台中的纯代码,理一理这对欢喜冤家的恩怨情仇。
|
||||
|
||||
一般的低代码平台,总爱宣传自己开发过程需要编码的地方是多么多么少,甚至已经消灭了所有代码。久而久之,不免给人一种感觉,如果低代码平台上还有代码的存在,就会显得很失败。低代码是新欢,纯代码是旧爱,有了新欢,抛弃旧爱,这样可不好。
|
||||
|
||||
其实,把低代码平台上的纯编码开发模式视为洪水猛兽,大可不必。编码有编码的优势,毕竟可视化不是低代码的目的,高效率才是。如果高低代码的结合使用可以提升开发效率,哪怕是只能在特定条件下提升效率,那都可以纳入低代码平台的能力范围。
|
||||
|
||||
为啥还要编码?
|
||||
|
||||
最开始,我们思考一个问题,既然可视化开发是低代码平台的特色和卖点,那为啥低代码平台还要提供纯编码入口呢?
|
||||
|
||||
你还记得【第 3 讲】中我描绘的天花板级别的低代码平台吗?其中一条标准和低代码平台的用户有关。天花板级别的低代码平台需要能支持各种技能水平的人同时使用。
|
||||
|
||||
也就是说,高技能水平的开发者也是低代码平台的目标用户。在这部分用户专业领域范围内的内容,他们更想要采用直接编码的开发模式。除开对这个人群来说,直接编码与可视化开发模式相比的效率因素,一个可能的原因是他们需要有职业获得感,以及出自人性深处的那种“不羁放纵爱自由”的叛逆心理。
|
||||
|
||||
我们前面说过,可视化模式凝聚了各路专家的经验,这个经验约束了你必须先这样做,然后再那样做,才能获得更好的效率和更优质的 App。从另一个角度看,就是强制规定了我们开发的套路,你只能这么做。这对于领域小白来说,当然是很舒服的,只要照做就能得到最优解。但高技能水平者往往有自己的见解,期望按照自己的思路来实现,给他们一个代码编辑功能,让他们自由发挥是更好的解决方法。
|
||||
|
||||
当然,不见得所有的低代码平台都要做到天花板上去,那发展中,甚至是刚起步的低代码平台,是否就没有纯编码开发的要求了呢?
|
||||
|
||||
当然不是。如果说天花板低代码平台的编码模式是锦上添花,为满足一部分人的需要而开放的能力,但对发展中的低代码平台来说,则是刚需,某些时候甚至是雪中送炭。发展中的低代码平台由于可视化开发能力不完善,难免出现搞不定的情况,这种情况往往就需要纯编码模式来兜底。
|
||||
|
||||
即使在低代码平台成熟之后,使用纯代码作为一种兜底策略,依然是一种非常好的选择。因为任何事情都逃脱不了二八原则,低代码的可视化模式再好,也只能适用于 80% 的场景。剩余的那 20% 边边角角的场景,如果硬上可视化模式,反而可能吃力不讨好,所以我们把那剩下的 20% 的场景留给纯代码来兜底,是一种很明智的选择。
|
||||
|
||||
因此,无论是在发展中的低代码平台还是已经比较成熟的低代码平台,可视化模式和纯代码模式的混合开发,都是有很明确的需要的。
|
||||
|
||||
说到采用纯代码方式来实现兜底策略,我在这里自爆一点我们低代码平台 Awade 的黑料。我在【第 7 讲】中介绍了 Awade 的结构化代码生成法,它有一个很重要特点,就是可以生成和人类手写的代码相似的、人能读得懂的代码。多数低代码平台并不会考虑生成对人类友好的代码,为啥 Awade 要这么做呢?
|
||||
|
||||
这是因为在 MVP 的初期,我们内部对 Awade 提供的可视化模式的端到端交付能力还心存疑虑,我们也不确定它能不能搞得定。于是心想如果人能读得懂、改得了 Awade 生成的代码,那万一开发哪个 App 时 Awade 搞不定,起码我们还能把代码导出来,手工继续完成剩余部分,不至于烂尾。
|
||||
|
||||
后来的事实证明我们的担心完全是多余的,Awade 提供的可视化模式可以很好地应对各种 App 的开发需要。即使如此,它能生成对人友好的代码的特性,并没有浪费,这个特性让 Awade 可以实现高低代码模式的无缝切换,这一点我们等会儿还会再说。
|
||||
|
||||
高低代码如何混合开发?
|
||||
|
||||
要注意,在我自爆的黑料中,我们原本打算使用的可视化模式搞不定时就人工接着搞的方式,实际上也是一种高低代码结合的方式(高代码指的也就是纯代码)。只不过,这是一种很无奈的、很被动的混合方式,甚至可以说是一块遮羞布。
|
||||
|
||||
那么,低代码平台如何优雅地实现高低代码混合开发呢?
|
||||
|
||||
在前面几讲中,我已经多次提到,低代码平台的开发过程是有代码参与的,代码是低代码平台的副产品,所以在低代码平台上能否实现高低代码混合开发,从来就不是一个问题。真正的问题在于要不要开放纯代码接口,这一点我们前面已经给出结论了。另一个问题是如何实现混合开发,这正是我们现在要讨论的问题。
|
||||
|
||||
形形色色的表达式是高低代码混合最常见的一种方式了,因为这是可视化模式的主要盲区。表达式的作用,往往就是作为一个小功能点,而如果我们用可视化模式实现一个真假表达式这样的小功能点,就像是拿大炮打蚊子,性价比极低。
|
||||
|
||||
可视化模式更适用于实现连续性的、复杂的逻辑。一个实际的 App 会包含许多这样的大块逻辑,并且这些大块逻辑之间充满了缝隙,需要使用表达式来“粘连”在一起。比如下面这个场景就是一个很好的例证:
|
||||
|
||||
|
||||
|
||||
这个图展示的是用可视化方式编排一个逻辑判断的功能。我们都知道,逻辑判断功能最关键的一个点是要判断真假,但是用可视化方式来编排出真假的结果,却并不容易。因此,这个例子中,我们直接就让应用开发人员填写一个表达式,这个表达式将用于计算出这个条件的真或假。这个例子给出的混合方式非常自然,即使是没有多少开发经验的应用,也能很快掌握。
|
||||
|
||||
还是同一个例子,我们来看看采用纯可视化的方式是如何来完成真假判断的:
|
||||
|
||||
|
||||
|
||||
这张图是在判断一个数组是否为空(即长度是否为 0),可以看到,纯可视化方式在这里就显得啰嗦一些了,这样的一个表单最终只生成了 array.length == 0 这样一个表达式。
|
||||
|
||||
那么常见的表达式还有哪些呢?除了真假表达式外,还有三目运算、正则表达式、求值表达式、逻辑运算、数值计算等,很多,我就不一一列举了。
|
||||
|
||||
这些表达式都很适合与可视化模式混合使用,不仅可以作为可视化模式的有用补充,在生成代码时,往往还起到直接表达业务逻辑的关键作用。表达式与可视化混用是如此常见,以至于如果我不指出来,你也许都不会注意到这也是一种混用场景。
|
||||
|
||||
也许你会反驳:表达式不算是高代码。那接下来,我们就来说说有点“代码含量”的情形:代码块。
|
||||
|
||||
可视化编程是可视化模式中最难做好的一个功能点。在上一讲中,我给出了一种通过形象化的方式来表达和编排程序逻辑的方法,但是那种方式主要是给程序编程能力很弱的人使用的,一般能写点代码的人会更乐意直接编码。
|
||||
|
||||
我认为,代码块就是可视化编程功能中实现高低代码混合的最好方式。我这里说的代码块指的是若干行完整代码组成的、有特定功能的程序块,一个函数可以包含一个或者多个代码块。比如下面这个示例图中就包含了 3 个代码块:
|
||||
|
||||
|
||||
|
||||
在一个完整逻辑流程中,可视化逻辑块和代码块可以按需地交织混合在一起,如下图:
|
||||
|
||||
|
||||
|
||||
图中橙色块就是纯代码块,其他块则是可视化逻辑块。这种混合方式完全是由应用开发人员按需编排出来的:有可能是开发人员觉得某个功能使用可视化块效率更高;也有可能是他不知道如何写出符合预期的代码块,而不得不用可视化块;还有可能是没有合适的可视化块,而不得不使用代码块顶替;甚至有可能随编排时的心情状况随机挑选了一种方式。总之,可视化编排器需要能支持这样一种高低代码混合的方式。
|
||||
|
||||
也许你还要反驳:代码块的“代码含量”依然不足,不能算作高低代码混合。
|
||||
|
||||
那接下来,我们就说说纯度达到 24K 的高低代码混合方式。表达式也好,代码块也罢,都是在可视化模式为主导的开发流程中,使用少量代码作为辅助的方式,也难怪会被质疑这样的方式不是纯编码。
|
||||
|
||||
那“代码含量”纯度为 24K 的开发方式是啥呢?总不能和我前面自爆的 Awad 黑料一样,把 App 整个工程的代码都生出来,让应用开发导入到 VSCode 这样的 IDE 继续完成剩余的开发吧?
|
||||
|
||||
揭开谜底之前,我先介绍一下我为切分 App 工程代码设定的粒度:模块。一个完整的 App 工程由多个模块组成:
|
||||
|
||||
|
||||
|
||||
你可以从这张示意图中看到:
|
||||
|
||||
|
||||
一个 App 工程至少包含一个模块,也就是图中最中间的启动模块,启动模块有且只有一个;
|
||||
模块有多种不同的类型,而且各类型模块的数量按照 App 所需创建;
|
||||
模块是 App 工程的唯一一种粒度。
|
||||
|
||||
|
||||
其实还有几点要注意,不过这几点与这讲内容关系不大,所以我没有画到这张示意图上,但作为模块的重要知识,我们略微扩展一下:
|
||||
|
||||
|
||||
模块之间可以相互引用,一个模块可以引用多个其他模块,也可以被多个模块引用;
|
||||
启动模块由框架直接引用;
|
||||
模块是实现 SPA(单页面应用)、应用自定义组件等的重要入口。
|
||||
|
||||
|
||||
为了减少 App 开发的学习成本,我吝啬地只用了模块这一种粒度,它承载了切分 App 工程、实现 App 代码复用、做各种关键功能入口的职能。关于模块如何做到如此复杂的职能、但又以极轻量的面貌展示在 App 开发人员面前,如果你觉得有需要详细说明,可以在留言区留言,有需要的话我会用专门一讲来介绍。
|
||||
|
||||
好了,收回来。介绍了模块后,不需要我多说,你应该已经猜到了,我所说的“代码含量”纯度为 24K 的开发方式,就是按需将一个模块的开发方式从可视化方式,切换为纯代码模式:
|
||||
|
||||
|
||||
|
||||
上面动画演示了将当前模块的开发模式,从可视化模式切换为纯编码模式的过程。通过这样的操作方式将一个模块彻底转为纯编码模式之后,App 开发人员就可以按照纯代码的方式,继续完成这个模块剩余的开发工作了。
|
||||
|
||||
由于这里我生成的代码是基于 Angular 的,因此 App 开发人员必须严格按照 Angular 的方式来编码。他可以按需创建新的 Angular 组件,或者引用已有的组件来完成他剩余的开发工作。这是一种非常纯粹的编码开发模式了。
|
||||
|
||||
|
||||
提示:如果你熟悉 Angular,你会发现我这里提及的“模块”这个概念和 Angular 里的“组件”的概念是对等的。
|
||||
|
||||
|
||||
特别需要注意的是,切换成纯编码模式这个动作只影响当前模块,其他模块依然保持原有开发模式。并且,通过纯编码开发的模块可以继续被其他模块引用,它也可以继续引用已有的模块。也就是说,从 App 整体来看,它的开发模式就出现了高低代码混合的局面。
|
||||
|
||||
高低代码如何混合编译?
|
||||
|
||||
老生常谈,我再强调一遍,低代码平台的开发过程是有代码参与的。因此,如果低代码编译器能够生成对人类友好的代码的话(方法见【第 7 讲】),把一个模块切换为纯编码方式的难度并不大。难的是如何实现高低代码混合编译,进而实现高低代码所见即所得的效果。
|
||||
|
||||
接下来我们就来简要说说如何实现,限于篇幅,只能把思路说清楚,而无法给出代码级的指导,如果你实现过程碰到了什么问题,欢迎在留言区提问。
|
||||
|
||||
我在【第 6 讲】的“生成代码总体流程”的部分有提到,如果低代码平台实时渲染视图采用的是直接法,也就是直接生成浏览器能识别的代码并渲染出视图,实现高低代码混合下的所见即所得效果就会非常麻烦。反之,如果采用的是间接法,即先生成某种 MVVM 框架下的代码,再实时编译成浏览器能识别的代码并渲染视图的方式,实现所见即所得则会简单许多。
|
||||
|
||||
|
||||
|
||||
间接法下从代码到视图的流程
|
||||
|
||||
从实现的流程图上看,采用间接法时,高低代码混合模式的编译流程几乎是一致的,因此实现起来非常容易。这个流程图是以代码为视角来画,如果改从所见即所得效果的实现过程来看的话,这个图应该进一步细化为这样:
|
||||
|
||||
|
||||
|
||||
这张图中指出了各个关键环节之间采用了哪个编译器来处理:
|
||||
|
||||
|
||||
编译开始于用户配置数据 SVD,使用低代码平台的编译器 awadec 将 SVD 编译成 TypeSript 代码;
|
||||
如果是纯编码模式,则不需要 awadec 的编译,手工敲出来的就是 TypeSript 代码;
|
||||
接下来使用 tsc 编译器将 TypeSript 代码编译成 JavaScript 代码;
|
||||
再调用 Angular 或者其他 MVVM 框架的 JiT 编译器将 JavaScript 代码编译成组件描述符;
|
||||
然后交给 MVVM 框架的渲染器,它会生成 DOM,并驱动浏览器渲染成视图。
|
||||
|
||||
|
||||
其中第 3 步将 TypeSript 代码编译成 JavaScript 代码的这个过程,目前看只有你使用 Angular 才会有,虽然 Angular 也支持 JavaScript 风格的 API,但官方不建议使用。因此我建议你采纳这个建议,先生成 TypeSript 代码。
|
||||
|
||||
如果你选用的是 React,那虽然没有 TypeSript 转 JavaScript 这步,但要多出将 JSX 转成 JavaScript 的步骤。如果你选用了 Vue2 就不用将 TypeSript/JSX 转 JavaScript 了,但现在 Vue3 也拥抱了 JSX,又需要有这一步了。
|
||||
|
||||
之所以把这个过程写得这么详细,主要是因为,为达到所见即所得的效果,代码从编译到视图实时渲染的全过程都有非常高的性能要求。因此建议你和我一样,将整个编译过程全部前置到浏览器中完成。但这样做就需要摸索出如何在浏览器中完成这所有步骤的方法了。其中 TypeSript 转 JavaScript 的详细方法,我整理成了一个 PPT,放在这里。你需要的话,可以参考。
|
||||
|
||||
而从 JavaScript 代码一路到浏览器视图渲染之间的步骤,我这里就不详细展开了。这部分和 MVVM 框架选型有直接关系,我用的 Angular,考虑到在国内 Angular 是小众,因此我的经验不一定是你需要的。当然,你也可以在留言区留言,如果很多人都需要的话,我可以专门找一讲来说清楚,这部分的难度其实还挺高的。
|
||||
|
||||
这部分的最后,我直接回复一下一个我被问了无数次的问题:一个模块被转为纯编码模式后,还能再回退到可视化模式吗?
|
||||
|
||||
答:不可能!可视化编程总是要有一些条条框框来约束的,而一旦转为纯编码模式后,等于放飞了思维,人的思维有多复杂,就有可能写出多复杂的代码来。因此一旦冲出了可视化编程设定的条条框框,就再也收不回去了。
|
||||
|
||||
追问:那有没有办法给出一些约束条件,在满足约束条件前提下可以再回退到可视化模式?
|
||||
|
||||
答:目前我还没仔细去思考这个问题。如果给出很强的约束那当然是可以的,但人的行为是不可控的,因此不可能给出太强的约束条件,我们需要设定出一些对人类非常友好的、简洁的规则来。关键原因是,目前我们没有这样的需求,使用可视化模式 + 代码块已经可以完成绝大多数的开发需要。
|
||||
|
||||
如何提升编码体验?
|
||||
|
||||
编码体验是纯代码模式下一个绕不过去的话题,这个话题往往是你好不容易走通了高低代码混合模式后,欣喜若狂之时的一盆冷水。
|
||||
|
||||
从高低代码混合的功能来说,编码体验不能算是一个问题,但在实际编码过程中,如果没有代码智能提醒、补齐、出错提醒、全局搜索、重构等功能,想象一下,这还算是在写代码吗?现代的编码 IDE(IntelliJ/WebStorm/VSCode)已经把纯代码开发开发人员的胃口给伺候得极其刁钻了。
|
||||
|
||||
在低代码平台上的高低代码混合开发,即使在编辑一个表达式,至少也就需要有智能提醒、补齐、出错提示了。如果使用了代码块的混合方式,那么这方面的需求比较强烈了。到了模块级的纯编码,那此时的编码体验的要求,就基本和 Web IDE 甚至和 Native IDE 相似了。难道要求我们在低代码平台上做一个 VSCode 出来吗?
|
||||
|
||||
怎么办?我们分成两种情况来讨论。表达式和代码块属于比较轻量混合,我们放一起讨论,而模块级的纯编码混合方式作为另一个情况单独讨论。
|
||||
|
||||
表达式和代码块对编码体验的要求其实已经不低了,但还没到非常极致的情况,因此只要有一个足够强大,扩展性足够强的编辑器,应该就可以应付。
|
||||
|
||||
这方面我走过一些弯路,过程直接跳过,我直接告诉你结果。我最终选择了Monaco这个开源编辑器,即使你没听说过这个名字,你也有可能已经是它的深度用户了,它就是 VSCode 的底层编辑器。功能自不必说,主要是扩展性极强,可以玩出非常多的花样,所以我们引入 Monaco 编辑器用于应对表达式和代码块场景的编码体验就完全足够了。
|
||||
|
||||
不过模块级的纯编码混合方式对编码体验的要求就非常高了,基本和 IDE 在同一层级了。此时,有两种可能的选择,一是继续基于 Monaco 编辑器,为它加上各种功能和扩展,深入将它融入低代码平台。这样做的好处是集成度非常好,低代码平台提供了一站式的开发体验。代价也是显而易见,你需要投入大量的资源来扩展和定制 Monaco 编辑器,把资源投入在这个方面,是低代码平台所需的吗?
|
||||
|
||||
另一个可能的选择是,把球踢给某个 IDE,比如 VSCode,我们需要的功能,这些 IDE 已经都有了,而且做得已经足够好了,我们只需要想办法利用这些 IDE 即可。
|
||||
|
||||
那如何做到平滑地复用呢?插件,任何现代 IDE 都支持插件。所以,我们可以基于某个 IDE 做个插件,用来从低代码平台过渡到 Native IDE 上。这个方式的成本比较低,具有很高的可行性,但缺点就是集成度不是很好。特别是在各个 App 开发人员的电脑上安装 Native IDE 和插件,有可能会有一些不必要的困难。
|
||||
|
||||
总结
|
||||
|
||||
今天这讲,我们详细讨论了低代码平台式的高低代码混合开发模式,“代码含量”从低到高分别是表达式、代码块、模块级。总体来说,高低代码混合的开发方式是低代码平台上非常有必要的能力,特别是需要对表达式、代码块这两种混合方式提供良好的支持。无论低代码平台处于哪个发展阶段,刚起步也好,已经成熟也罢,我认为低代码平台对表达式和代码块的混合的支持都是必须的。
|
||||
|
||||
表达式和代码块这两种高代码开发方式,在低代码平台上的实现方式是相似的,都是以可视化的低代码开发流程为主。在可视化方式的不可达或者友好的位置,使用表达式或者代码块来填补,两种方式可以非常好、非常自然地融合在一起。而模块级的编码模式,则是彻底丢弃可视化模式,转为纯编码开发方式,尽管如此,在纯编码开发的模块也可以和可视化开发的模块混合使用。
|
||||
|
||||
在完成了高低代码融合之后,编码体验是我们不得不面对的一个问题。好的编码体验可以提供更高的开发效率,减少试错的次数和避免部分低级问题。但要获得好的编码体验并不容易,虽然我们引入 Monaco 这样的编辑器,可以利用其强大的基础功能和扩展能力来有效提升编码体验,但是需要有一定的扩展的工作量。
|
||||
|
||||
我建议你使用 Monaco 编辑器来解决表达式和代码块的混合。而模块级的纯编码模式,虽然 Monaco 编辑器也能搞得定,但是扩展工作量会很大,还不如牺牲一些集成度,给 VSCode 等 Native IDE 做插件,从而最大化利用这些 Native IDE 的已有能力。
|
||||
|
||||
虽然这讲到这里的篇幅已经非常大了,但是这个话题依然还有许多必要的内容没能讲到。包括:
|
||||
|
||||
|
||||
高代码模式下如何处理存量代码?
|
||||
如何扩展 Monaco 编辑?
|
||||
如何开发 VSCode 插件?
|
||||
如何处理好第三方依赖?
|
||||
|
||||
|
||||
后续等有机会我们再翻出来说清楚这些内容。
|
||||
|
||||
思考题
|
||||
|
||||
你认为提供模块级的纯代码开发模式有多大的必要性?请结合你的场景聊聊你是怎么看的。
|
||||
|
||||
欢迎你在留言区写下你的想法,下一讲我们将会来讨论 App 开发过程中数据配置的问题。我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
239
专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md
Normal file
239
专栏/说透低代码/12业务数据:再好的App,没有数据也是白搭.md
Normal file
@ -0,0 +1,239 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 业务数据:再好的App,没有数据也是白搭
|
||||
今天我们来说说 App 开发过程中获取数据的配置。
|
||||
|
||||
数据配置是应用开发三部曲(布局、交互、数据)中的第三个环节,根据 App 的不同,它与数据之间的关系也不同:有的 App 可以产生数据(信息采集类);有的 App 则是数据消费者,或者兼而有之。数据采集 + 推送,包括文件上传的方式总体来说都比较简单,不在今天的讨论范围内,这一讲我们主要讨论组件如何获取和渲染数据。
|
||||
|
||||
而且,由于我们这个专栏所说的低代码平台生成的 App 都是 B/S 架构的,App 首选的获取数据方式当然是 HTTP 通道,实际上,即使是 C/S 架构的 App,HTTP 通道也依然是一个非常好的选项。所以,这一讲我们就只讨论通过 HTTP 通道来获取数据的情况。
|
||||
|
||||
请求参数、数据结构修正、数据模型
|
||||
|
||||
我们先来讨论数据获取的最基本动作,从请求发出去到数据展示到 UI 上,全程会涉及参数设置、返回的数据结构修正、数据模型映射等几个主要环节。
|
||||
|
||||
你要注意,这几个环节不包含获取数据的异常处理流程。异常处理是相对简单的一部分,只要别忘了在配置界面上增加对应的出错处理配置,生成的代码注意捕获 HTTP 异常即可。
|
||||
|
||||
第一个基本动作是 HTTP 请求的参数配置。HTTP 协议允许我们在多个不同的位置设定参数,可能传参的位置至少有三处:通过 url 传参、通过请求头传参,通过请求 body 传参。你在设计参数配置界面的时候,别忘了要给这 3 个可能传参的位置留出配置界面。
|
||||
|
||||
其中 url 传参这块是很容易被忽略的。比如下图的配置界面中,很容易把 url 输入框作为静态文本输入:
|
||||
|
||||
|
||||
|
||||
这样的话,应用开发要通过类似下面这样的 url 传参,就不行了:
|
||||
|
||||
# $v1 和 $v2 都是变量
|
||||
/some/data/url?p1=$v1&p2=$v2
|
||||
/some/data/p1/$v1/p2/$v2
|
||||
|
||||
|
||||
我的解决方法是,支持类似模板字符串的语法,即在 url 输入框中填写这样的 url,表示包含变量:
|
||||
|
||||
/some/data/url?p1=${v1}&p2=${v2}
|
||||
/some/data/p1/${v1}/p2/${v2}
|
||||
|
||||
|
||||
其次,url 传参还有一个容易被忽略的问题:url 编码。
|
||||
|
||||
比如前面例子中的 v1 或者 v2 变量,如果运行时,变量值包含敏感字符如“&”,或者包含汉字,此时拼出的 url 会出错,导致请求失败。解决方法也很简单,我们只需要解析应用开发给的 url,自动添加编码函数,即在实际生成的代码中,自动把应用开发填写的 url 自动处理为:
|
||||
|
||||
/data/url?p1=${encodeURIComponent(v1)}&p2=${encodeURIComponent(v2)}
|
||||
/data/p1/${encodeURIComponent(v1)}/p2/${encodeURIComponent(v2)}
|
||||
|
||||
|
||||
不过,在处理时,我们也不能无脑处理,因为有的应用开发有这方面的编码经验,有可能他填进来的 url 就已经有包含了 encodeURIComponent 的调用了,此时如果我们再编码就错了。真是操碎了心,有没有!
|
||||
|
||||
而且,我们通过请求头传参时也要注意参数值的编码,但通过 body 传参就不需要了,HTTP 传输层会自动编码。
|
||||
|
||||
配置了正确的参数之后,数据应该就可以拿到手了。如果服务端给的数据结构不符合预期,我们还需要对数据做加工,又或者,如果你打算把拿到的数据做可视化渲染,比如渲染成各种图形,则还需要做数据模型映射。接下来我们将这两个动作放一起考虑。
|
||||
|
||||
前端组件普遍对输入的数据的结构有预设。有的要求输入的数据必须是一个一维数组,有的则要求具有特定结构,比如Jigsaw的表格要求输入这样结构的数据:
|
||||
|
||||
{
|
||||
header: [ "Column1", "Column2", "Column3" ],
|
||||
field: [ "field1", "field2", "field3" ],
|
||||
data: [
|
||||
[ "cell11", "cell12", "cell13" ], //row1
|
||||
[ "cell21", "cell22", "cell23" ], //row2
|
||||
[ "cell31", "cell32", "cell33" ] //row3
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
通用性较高的低代码平台对接的服务端往往是无法事先预知的,所以对方返回的数据结构也是无法预知的。这要求在前端收到数据之后,要有数据结构如何做转换的配置。数据结构修正尽量自动完成,这样可以减少应用开发的配置工作量,以及降低应用开发的难度。一个比较好的方法是,尽可能收集各种可能的输入数据结构,然后根据特定输入结构,设定参数,从而可以达到自动生成转换代码的目的。
|
||||
|
||||
我这里给出两种比较常见的输入数据结构,分别是二维表结构以及准二维表结构。虽然现在服务端持久化数据的方式多种多样,但大多数还是采用 RMDB 来存储,所以服务端从数据库中读取到的原始数据,多数就是二维表结构的:
|
||||
|
||||
[
|
||||
['v11', 'v12', 'v13', 'v14', 'v15'],
|
||||
['v21', 'v22', 'v23', 'v24', 'v25'],
|
||||
['v31', 'v32', 'v33', 'v34', 'v35'],
|
||||
['v41', 'v42', 'v43', 'v44', 'v45'],
|
||||
['v51', 'v52', 'v53', 'v54', 'v55'],
|
||||
]
|
||||
|
||||
|
||||
另一种是二维表的变体,如果服务端用的是 node.js 实现的,很可能会返回这样的数据结构:
|
||||
|
||||
[
|
||||
{f1: 'v11', f2: 'v12', f3: 'v13', f4: 'v14', f5: 'v15'},
|
||||
{f1: 'v21', f2: 'v22', f3: 'v23', f4: 'v24', f5: 'v25'},
|
||||
{f1: 'v31', f2: 'v32', f3: 'v33', f4: 'v34', f5: 'v35'},
|
||||
{f1: 'v41', f2: 'v42', f3: 'v43', f4: 'v44', f5: 'v45'},
|
||||
]
|
||||
|
||||
|
||||
当然,这里还需要有兜底方法,用于处理预设类型之外的其他情况。但在这个情况下,只能编写数据转换逻辑了。我们可以引入第 10 讲的方法,通过可视化编程方式来编排转换逻辑,也可以直接给一个编辑器,让应用开发填写转换逻辑。两种方式的流程都是一样的,都是给出一个原始数据,要求应用返回一个处理后的数据:
|
||||
|
||||
origin => {
|
||||
// 把你的处理逻辑放在这里
|
||||
const result = ...;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
如果说数据结构是对数据的一种逻辑表达,那么数据模型则就是对数据的一种抽象化描述。
|
||||
|
||||
数据建模往往与业务强相关,根据特定的业务模型来对数据做抽象和归类,这也就导致了不同业务可能会使用不同的模型来描述数据。我这里给出的是我们用于描述电信领域相关业务,特别是运营商大数据相关业务的数据模型,多年的应用表明在这个业务下有非常好的表现。实际上,这个模型是可以推广到所有采用 RMDB 来持久化的关系数据的。
|
||||
|
||||
|
||||
|
||||
这个模型把数据库表的所有字段,分为维度和指标两类。维度是描述一个事物的各个实体,比如省市区这 3 个维度可以用于行政区,再比如手机厂商、手机型号也是维度,以此类推。
|
||||
|
||||
指标就更好理解了,绝大多数的指标字段都是数值型的,比如今天的温度、这篇文章的字数,以及新冠确诊人数等。有的指标是可枚举的值,虽然不是数值型的,但也是指标,比如考核等级 S/A/C。时间是一个比较容易混淆的字段,它看起来是数值型的,但我们将时间作为维度来对待。
|
||||
|
||||
在对数据可视化渲染时,数据模型可以帮助低代码平台大幅降低数据可视化配置界面的复杂度,也可以让数据可视化配置过程更加具有业务含义,提高配置效率,减少试错次数。所以,在配置应答数据的结构时,我们还要把这笔数据用作可视化渲染,还必须要求应用开发填写必要的数据模型信息,越详细越好。
|
||||
|
||||
比如下面这个表格,就是描述一份天气数据的模型:
|
||||
|
||||
|
||||
|
||||
数据打桩
|
||||
|
||||
接下来,我们再谈谈另一种获取数据的方式,也就是在开发态下获取数据的各种骚操作,我们可以偷,可以抢,甚至可以造假。
|
||||
|
||||
App 在开发时,一个非常普遍的情况是,它的数据还没准备好,或者不在当前开发环境下。总之,就是没有数据可用,那低代码平台应该如何帮助 App 开发者解决这个问题呢?
|
||||
|
||||
数据打桩就是解决这个问题的功能。
|
||||
|
||||
数据打桩的基本实现是给一笔假数据作为模拟。实现的方法非常简单,就是给 XHR 请求加一个拦截器,通过 url 等筛选出需要模拟的 rest 服务,然后直接造假。这样甚至可以做到在不实际发出 XHR 请求的前提下,实现数据打桩。
|
||||
|
||||
更进一步,你可以让应用开发配置一定的规则,比如参数 A 的值是 a1 时,返回数据 1,值是 a2 时,返回数据 2,甚至直接让应用填写模拟的代码,这样理论上可以 100% 模拟服务端。采取哪种方式,取决于你需要模拟到啥程度。
|
||||
|
||||
那么,我们如何加 XHR 拦截器呢?其实多数前端框架都有解决方案了:
|
||||
|
||||
如果你使用的是 VUE/React,一般会使用到 axios 来处理 XHR 请求,axios 有 interceptors 属性,可以用于你添加拦截器;
|
||||
|
||||
|
||||
如果你用的是 Angular,那可以直接使用 HttpClient 提供的拦截功能;
|
||||
如果你使用的是 jQuery,可以通过 ajaxSetup 这个函数来设置拦截器。
|
||||
如果你啥框架都没用,那有没有办法拦截呢?
|
||||
|
||||
|
||||
当然也是有的,你可以直接对浏览器原生的 XmlHttpRequest 打补丁,下面这段代码演示了如何换掉 XmlHttpRequest 的 send 函数:
|
||||
|
||||
const originSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
var info="send data\r\n"+body;
|
||||
alert(info);
|
||||
originSend.call(this, body);
|
||||
};
|
||||
|
||||
|
||||
我们的低代码平台 Awade 为了让打桩代码更加真实,就没有采用上述任何一种方法,而是在直接把桩代码生成到服务端,这样前端就不需要做任何处理了,只需要把 url 重定向到 Awade 生成的桩代码即可。
|
||||
|
||||
不过,仅仅有数据模拟是不够的,虽然我们可以把服务端模拟得很像,但是会多出很多无价值的配置。因此,在 App 开发的中后期,直接模拟的方式会被抛弃,改用把请求重定向到真实服务端去。此时所谓“真实”服务器,其实是指某位后端开发人员办公电脑,或者是另一个低代码服务器实例。
|
||||
|
||||
这里有个背景需要注意,我们鼓励应用团队自行部署 Awade 私服,而不是都集中到 Awade 官服上开发,主要是因为官服容量不够,所以在中兴内部有许多的 Awade 私服存在。这就造成了同一个 App 可能分开到两个私服上开发。
|
||||
|
||||
使用 web 服务器(如 Nginx)的反向代理功能来实现服务重定向,是非常容易的,但会有一个棘手的问题,就是往往低代码平台只有一个 web 服务器进程,不同应用需要重定向到不同的服务器,即使无视转发规则冲突的问题,但让转发规则生效时的 reload 操作也是无法容忍的。因为 reload web 服务器的配置会导致进行中的其他请求瞬断,这是无法接受的。
|
||||
|
||||
那可否不经过服务端,绕一圈,直接从低代码平台服务器一个请求到目标服务器上去读取数据呢?
|
||||
|
||||
在解决了跨域问题的前提下,是可以的。本来,跨域问题并不难解决,在服务端配置一下 CORS 策略就可以轻松解决。但这隐含了一个前提,就是你要先知道有哪些服务端,对吧?问题就在于你有可能不知道有哪些服务端!比如我们的低代码平台 Awade 是一个开放性的平台,在它上面开发的 App 的数据来源自然是无法事先预知的。这个情况下,跨域问题似乎是无解的。
|
||||
|
||||
曾经,我也是这么认为的。不过,在走了一段弯路之后,终于找到了一个方法,可以在不配置服务端 CORS 策略的前提下,巧妙地“骗”过浏览器,绕过跨域限制,做到在浏览器中可以跨域请求任何服务器数据的效果。
|
||||
|
||||
这个方法有点绕,我会在这门课的动态更新部分,尝试只用一讲把这个方法说清楚。无视跨域约束地请求任何服务器数据,是通用型低代码平台的一个很有意义的能力。
|
||||
|
||||
个性化数据
|
||||
|
||||
前面我们详细讨论了低代码平台通过可视化方式获取数据的方法、数据模型、模拟数据等,这些都是着眼于通用的获取数据和处理数据的方法。接下来,我们再来说说如何处理个性化数据。个性化数据的通用性低,一般只适用于某个特定的业务场景,这就造成了个性化数据的获取方式形式多,数量大。
|
||||
|
||||
插件机制是低代码平台处理这种情况的最佳选择。
|
||||
|
||||
得益于个性化数据只为特定业务设计的特点,我们可以采用比通用数据更加灵活的方式来获取数据、提取模型和模拟。一个插件只专注处理好一种个性化数据就行,不需要考虑除此之外的其他场景,因而插件的实现难度低、效率高。
|
||||
|
||||
接下来,我们再看看采用啥架构可以让低代码平台支持通过插件的定制,获取个性化数据。思路和【第 9 讲】类似,因此为了更好的学习效果,你可以回顾一下【第 9 讲】看看我们是如何做出第一个插件的。部分相似思路这里我就简略带过,不再和【第 9 讲】那样详细说明了。
|
||||
|
||||
首先,我们把获取数据看作是一个动作。
|
||||
|
||||
说到这里,可能你已经想到了,无论是获取通用化的数据也好,个性化数据也罢,都是获取数据动作的一种具体实现。此时我们可以画出这样的一个图来:
|
||||
|
||||
|
||||
|
||||
基础动作里包含的是两种动作的共同部分。都有哪些呢?至少包含这几部分内容,一是信息采集,一是信息保存,一是代码生成。
|
||||
|
||||
信息采集,就是要定义一个收集开发者配置信息的视图。获取数据的各个动作,需要采集的信息都大不相同,不同的个性化数据需要采集的信息也各不一样。因此,在基础动作中,这部分是抽象的,我们无法知晓具体该绘制啥样的 UI,但可以约束具体动作采用什么方法来绘制 UI,比如你可以要求动作子类采用 jQuery 的方式,或采用 MVVM 框架的动态渲染器的方式。
|
||||
|
||||
我采用的是后者,子类可以将处理 UI 的所有逻辑,都封装到动态渲染器中。并且得益于 TypeScript 特性,我还可以为各种渲染器组件提供一个父类,以减少子类开发时的难度和编码量。此时渲染器与基础动作之间会形成下图这样的关系:
|
||||
|
||||
|
||||
|
||||
信息保存是可以在基础动作中直接实现的,只需要在基类中提供读写数据的 API 给子类使用即可。注意,在基础动作中不能定义一个具体的配置数据结构,这个结构必须由子类自行定义。在动作子类的渲染器中,我们就可以使用基础动作提供的配置数据读写 API,将 UI 上采集到的数据统一存到基础动作中去了。
|
||||
|
||||
你可能会有这样的担忧:既然配置数据存储的结构不能由基础动作决定,那在生成代码的时候,基础动作无法知晓配置数据的结构,自然就无法生成代码了。
|
||||
|
||||
其实,这个担忧是不存在的。和【第 9 讲】使用的方法一样,基础动作只能定义代码生成的过程,但是它无法定义具体如何生成代码,这对基础动作来说是抽象的,只能在动作子类中实现。这个过程我这里就一带而过了,如果你还是无法理解,请回顾一下【第 9 讲】,或者在评论区提问。
|
||||
|
||||
最后,把基础动作的代码合并到下图的编译器协议中,成为 SDK 的一部分。
|
||||
|
||||
|
||||
|
||||
通用获取数据的动作,作为基础动作的一种默认实现,由低代码平台提供官方的实现,个性化获取数据动作则作为一个新的插件,由业务团队实现。此时的架构图如下:
|
||||
|
||||
|
||||
|
||||
当然,并不是说个性化获取数据动作只能由业务团队来实现,实际上低代码平台团队也可以提供一些个性化数据动作的插件实现。
|
||||
|
||||
一般在低代码平台推广初期,平台团队为了减少在业务团队中推广的阻力,会主动将业务团队常用的获取数据方式做成插件给他们使用,从而解决低代码平台与业务团队存量系统的对接问题。可以看到,在这个过程中, 插件起到一种类似胶水的作用,它可以很好地将低代码平台与存量系统粘在一起。
|
||||
|
||||
下面,我们再看一个实例,对比下通用和个性化获取数据的差异。对于同一笔数据,如果它在低代码平台中只能采用通用化方式来获取,我们可以看到参数部分非常复杂,要正确填写这样的参数是不容易的:
|
||||
|
||||
|
||||
|
||||
但在业务团队,却可以在插件中,开发出这样的定制化 UI 来获取同一笔数据:
|
||||
|
||||
|
||||
|
||||
上面这段动画操作完成之后,插件会自动生成第一个图中的参数。采用这样的形式来获取数据谁不爱呢?
|
||||
|
||||
当然,除了采用插件化来定制个性化数据之外,我们还可以用其他形式获取个性化数据,比如可以采用 DAG(Directed Acyclic Graph,有向无环图)的形式来对数据做可视化编排。
|
||||
|
||||
不过,这讲说到现在,我们都是假设数据一次性就可以拿到手,但有时候实际情况并非如此。比如 Awade 曾经处理过这样一个业务需求:一个趋势图展示某个指标,它的数据有一部分来自历史数据,一部分来自实时数据。
|
||||
|
||||
对大数据有了解的小伙伴应该都知道,由于数据量过大,任何大数据系统都会把历史数据和实时数据分开,采用完全不同的方式来处理,而客户要求在一个趋势图上显示这两种数据,这就需要在后台分别读取两种数据之后,将其拼在一起。这个情况下,我们就需要用上可视化数据编排来获取深度定制的个性化数据了。
|
||||
|
||||
这讲我们就不展开可视化数据编排的具体实现了,我找机会再专门聊聊这个内容。
|
||||
|
||||
总结
|
||||
|
||||
这一讲主要专注在低代码平台如何在 App 开发过程中获取数据,从通用和个性化两个角度详细讨论了低代码平台数据的获取。通用方式获取数据的方式,可以适用于大多数 App 的开发过程,但需要对获取到的数据结构进行修正,以及无法预设获取到的数据的模型。因此,在对这个方式获取的数据进行可视化渲染之前,我们还必须配置数据的模型,以降低配置图形可视化渲染的难度,使得图形配置过程更具有业务含义。
|
||||
|
||||
有的企业做了中台化改造,在改造完成的部分,低代码平台通过中台统一获取数据就可以省去非常多的麻烦,包括数据结构和数据模型。数据中台往往会对数据进行治理,在治理完成之后,数据中台就可以给出结构统一的数据,给低代码平台开发者使用了。同时,数据中台在数据治理后,还可以将数据的模型作为资产,提供统一的 API,低代码平台通过数据资产 API 就可以获取到数据模型的信息了。我在【第 1 讲】中提到,中台和低代码的演进线路有相当一部分是重合的,这就是一个例子。
|
||||
|
||||
使用个性化数据的体验往往会比通用化数据要好得多,可以实现更加彻底的可视化方式来使用数据。但是个性化数据的获取需要有大量的定制化配置,这就要求低代码平台必须提供一套插件机制来支持业务团队在获取个性化数据方面的定制需求。这讲中,我结合【第 9 讲】的知识,扩展出了一种新的插件定制架构和方法,基于这些思路,你应该可以做出个性化数据定制机制和二次开发插件的方法了。
|
||||
|
||||
最后,低代码平台还需要提供数据打桩的方法,帮助应用在无法直接获取到数据的情况下,可以基于模拟或者转发的方式来获得数据,使得 App 的开发得以继续。
|
||||
|
||||
思考题
|
||||
|
||||
如要采用这讲给出的方法为你的一个常用的业务场景定制一个获取个性化数据的插件,你会如何设计它的 UI,以及如何生成代码?欢迎在评论区留言。
|
||||
|
||||
我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
204
专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md
Normal file
204
专栏/说透低代码/13|多人协同编辑:野百合的春天为啥来得这么晚?.md
Normal file
@ -0,0 +1,204 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13|多人协同编辑:野百合的春天为啥来得这么晚?
|
||||
这一讲我们来说说低代码平台的一个甜蜜的烦恼:多人协同编辑。
|
||||
|
||||
为什么说这是一个甜蜜的烦恼呢?因为一旦低代码平台有了这样的需求,就意味着它已经可以开发出有相当复杂度的 App 了,也意味着各方对低代码平台已经有了较强的信心,甚至说它在复杂 App 开发方面已经相当深入了。我们可以说这样的低代码平台已经具备了较强的开发能力。
|
||||
|
||||
说它是一个烦恼,是因为往往这个时候的低代码平台已经成型了,底层数据结构必然已经固化。如果平台架构早期未考虑到多人协同的话,此时就很难采用最优解来解决这个需求了,只能退而求次,采用迂回的方法。
|
||||
|
||||
那么今天,我们就从多人协作功能的实现难点入手,聊聊它的实现方案和注意事项。
|
||||
|
||||
多人协作功能的难点是什么?
|
||||
|
||||
面对这个问题,可能你会猜难点是多个编辑器之间的点对点通信和实时数据传输。不可否认,这是一个难点。但现在的 web 技术有太多的解决方案了,WebSocket,WebRTC 等都是极好的解决方案,我推荐优先选择 WebSocket。
|
||||
|
||||
因为 WebSocket 更成熟,服务端实现方案多且完善,它更加适用于一对多广播,相对来说,WebRTC 更适合用于 P2P 传输音像多媒体信息,实现更加复杂。更具体的,你可以自己搜下相关资料。
|
||||
|
||||
那么真正的难点是啥呢?我认为首先是如何解决冲突。你想,多人对同一个工程进行编辑,难免会同时对同一个组件的同一个属性做操作,或者是你在改某个组件,而我要把它删除。这样的操作就会产生冲突。
|
||||
|
||||
那么如何解决冲突呢?有一个办法,我们可以像 git 那样,标记每个冲突点,然后中断 App 开发的工作,强制要求他们做出选择呀。这是一种办法,但是不彻底。这个问题,我们可以用一种釜底抽薪式的解决方案,就是不让冲突出现!
|
||||
|
||||
那么如何避免冲突呢?到这里就需要介绍 CRDT 算法了,CRDT,也就是 Conflict-free Replicated Data Type,无冲突复制数据类型。
|
||||
|
||||
实际上冲突是不可避免的,只是 CRDT 采用了某种策略,就像一个和事佬一样,帮助协同编辑的各方妥善安排了冲突。但这个策略已经超出了这讲的范畴,有兴趣你可以自行了解一下 LWW(即 Last Writer Wins)策略。当冲突发生时,谁对谁错不重要,重要的是,各方能协商一致,且各方都可稳妥地拿到这个协商结果。
|
||||
|
||||
CRDT 是一个算法,而且还挺复杂的,那么有没有实现了这个算法的库呢?
|
||||
|
||||
必须有!适合 JavaScript 生态圈的,有 3 个,分别是 Yjs,automerge 和 ref-crdts。这三者的性能对比,你可以参考下面这个图(引自雪碧的文章):
|
||||
|
||||
|
||||
|
||||
这三个都是当前主流的 CRDT 实现,它们主要的应用场合都是多人协作在线文档,但他们也支持 json 这样的数据结构,所以,也可以用于低代码平台的多人协作功能。
|
||||
|
||||
这里你要注意图中红色的箭头,Yjs 的效率非常高,几乎与横轴贴合在一起。所以应该选择哪个,就不需要我多说了吧?
|
||||
|
||||
前面我说了,多人协作的难点首先是冲突,既然有首先,那当然就有其次了。其次的难点就是历史记录管理。
|
||||
|
||||
请你想想,多人一起编辑一个 App,你总不能把别人的修改给撤销了吧?如果真是这样,那冲突就可能从线上发展到线下去了(要打起来了)。但是大家同时操作产生的历史记录交织在一起,要完全依赖你自己设计解决方案来实现撤销功能,想想都觉得很复杂。
|
||||
|
||||
莫急,Yjs 提供了一个 UndoManager,可以用于记录使用人的历史记录和正确地执行撤销、重做等功能。非常贴心。
|
||||
|
||||
既然有其次的难点,想必还有再次的难点,那就是,很可惜,CRDT 的解决方案,你可能用不了!(手动恐惧)
|
||||
|
||||
这就是所谓“多人协作功能作为一个甜蜜的烦恼”中烦恼的部分。
|
||||
|
||||
为啥这么说呢?如果你采用 CRDT 解决方案,数据保存的格式就必须采用它定义的格式。一句话说明 CRDT 的原理:对所有操作打上时间戳,然后通过网络把各人编辑产生的历史记录分发出去,本地接收后,完成合并。合并历史记录时,所有冲突点,都使用 LWW 策略处理冲突。所以历史记录的格式是所有 CRDT 解决方案的根基。
|
||||
|
||||
但是,历史记录的格式,何尝又不是低代码平台正常运行的基础之一呢?虽然不是决定性的基础,但是在低代码平台成型之后,我们要对这个部分做调整,必然是伤筋动骨啊!
|
||||
|
||||
另外,应用数据的归属也是一个阻碍多人协作的问题。低代码平台基本上都会按照用户来隔离应用数据,即用户只能看到自己的应用数据,无法看到他人的数据,甚至多数平台会采用租户的方式,对应用数据做物理隔离。
|
||||
|
||||
一言蔽之就是,用户数据的存储方式也需要在平台建设之初就有多人协作的考虑,否则后期的调整也是伤筋动骨。
|
||||
|
||||
多人协作这个功能,就是一株野百合,它总能等到属于它的春天的到来,但是春天的脚步是如此缓慢,姗姗来迟,待到低代码平台的成熟度高到足以支持需要多人一起开发复杂 App 的时候,已然太晚,木已成舟,这时再对底层基础功能做调整很不现实。此时,CRDT 解决方案再巧妙,Yjs 性能再好,UndoManager 再贴心,你也只能望洋兴叹。
|
||||
|
||||
这小段文字描述的正是我的心情。但是,当你看到这一讲的时候,希望你还有机会作出选择。
|
||||
|
||||
前面描述了 CRDT 的种种好处,如果你的平台现在还未定型,还有机会选择 CRDT 的话,可能已经摩拳擦掌要试一试 CRDT 了,请先别急,我现在要来泼点冷水。
|
||||
|
||||
CRDT 一定是银弹吗?
|
||||
|
||||
目前业界对 CRDT 最成熟的使用是多人协作在线文档。虽然 CRDT 算法的初衷并非只针对多人协作文档,同时 Yjs、automerge 等实现也确实可以支持 JSON 数据结构,这是低代码平台所必需的。但是,我确实很少听说有低代码平台实际使用这个功能的。
|
||||
|
||||
低代码平台的使用过程(即 App 的开发过程),与在线文档是有很大差别的,我接下来会给出一个情形,说明低代码平台即使使用 CRDT,也会出现冲突。作为一点背景知识,虽然前文已经有提了一点了,这里我们还需要进一步解释为啥 CRDT 可以做到 conflict-free。
|
||||
|
||||
简而言之就是及时,及时意味着任何修改,都要及时记录。注意只需要及时记录就好,不需要及时同步给各个协同者。
|
||||
|
||||
因为及时记录下来的同时,一个时间戳和插入位置就被生成并记录到编辑历史中。此时即使有多人在同时编辑,产生大量编辑记录,但各个历史记录的时间戳却各不一样。即使此时网络都堵塞了,导致这些编辑记录都未能及时同步,也没关系的。在网络恢复之后,他人的编辑记录同步过来后,不同插入位置的编辑记录不会有冲突,它们会按照时序在本地生效。相同插入位置的编辑记录则以 LWW 策略,挑选最后一个改写记录作为最终结果。
|
||||
|
||||
这个过程的示意图如下:
|
||||
|
||||
|
||||
|
||||
那在低代码平台上,啥情况下会出现保存不及时的情形?
|
||||
|
||||
这就和低代码的开发方式的特征有关了。可视化开发是低代码的主要特征,因此在开发过程中,如果涉及复杂的配置,不可避免地需要使用对话框来承载。比如下面这个例子,弹出一个对话框来组织一次复杂配置的过程,是很常见的:
|
||||
|
||||
|
||||
|
||||
注意,为了使用友好,对话框会有确定按钮,等用户点击确定按钮之后,再一次性保存使用的配置,点击取消则会丢弃对话框上所有修改,这是一个很有用的功能。
|
||||
|
||||
问题就出在这个地方了。如果对话框上的修改未能及时保存下来,有人在某个对话框上编辑的同时,恰好另一个人也打开了同一个对话框开始配置,这个情况下,冲突就出现了:
|
||||
|
||||
|
||||
|
||||
图中虚线的编辑记录与他人打开同一个对话框时的记录是相同的
|
||||
|
||||
你可能会有疑问,此时 LWW 策略不再适用了吗?
|
||||
|
||||
其实,LWW 是适用的,但问题在于这样非常不友好。为啥呢?因为对话框承载的是复杂配置,包含大量的配置项,使用 LWW 简单粗暴地进行二选一,这样必然有人的工作会被白白浪费掉。再者,即使在同一个对话框上,也有可能未编辑到同一个属性,此时记录 1 和记录 2 是可以直接合并的,因此我们不能简单粗暴地使用 LWW 策略来处理。
|
||||
|
||||
在这个情形下,CRDT 无法避免冲突。
|
||||
|
||||
CRDT 在低代码平台上“水土不服”的第二个表现是,性能问题。在线文档也有所见即所得效果,任何修改同步过来后,都可以毫秒级生效。但与在线文档不一样的是,低代码的所见即所得效果则“昂贵”许多,它需要时间来编译,需要更多的时间来渲染。在 App 复杂时,需要三五秒才能完成一次反馈。在这样的响应速度下,如果同时编辑 App 人数太多,那基本大家的时间都会消耗在无休止的等待上了。
|
||||
|
||||
第三个“水土不服”的表现是,修改位置。在线文档的插入位置十分简单,只需两个整数记录光标所在的行和列即可。
|
||||
|
||||
但低代码平台的插入位置则复杂得多。因为低代码平台存储的数据是一份结构化的数据,因此插入位置需要使用一个类似 DOM 树的 xpath 的形式来表示。这还不够,一个 xpath 对应的是结构化数据里的一个属性,这个属性的值有可能是简单值,也有可能是多行文本,比如一段代码(关于为啥属性值会是一段代码的原因,你可以回顾一下第 11 讲高低代码混合开发)。
|
||||
|
||||
如果是简单值,那使用 LWW 策略时,二话不说,直接覆盖就完了,但当面对多行文本时,直接覆盖就不妥了。设想一下,前一个人,对某个多行属性做了数十行的改动,而后一个人只对同一个多行属性修改了 1 个字符,无脑应用 LWW 策略的话,前一个人的数十行修改就丢了。这也是一个问题。
|
||||
|
||||
那这些问题有没有解决办法呢?都有。
|
||||
|
||||
|
||||
对于冲突问题,有一个比较优雅的解决方案是,使用一个临时的独立的影子历史记录来及时保存所有修改。在确定要保存这个影子记录时,将所有的修改合入主修改记录序列中,否则将其丢弃即可。影子历史记录里的修改依然保持及时被记录的特征,所以在应用 LWW 策略规避冲突时,被直接覆盖的工作就小到可以忍受的程度;
|
||||
对于性能问题,可以考虑约束同时编辑的人数,但这治标不治本。一个更好的方法是引入懒渲染的方式,即只在需要的时候启动渲染,其他情况下只记录并提示有多少未渲染的修改即可;
|
||||
对于多行属性的问题,可以学习一下 git 的做法,你一定用过 git 合并过代码,对不同行所做的修改,git 合并时是不会冲突的,对吧?所以,除了 xpath 外,还需要增加一个行号。只有对同一个 xpath 属性的同一行做编辑,才需要应用 LWW 策略,其他情况直接合并即可。
|
||||
|
||||
|
||||
这一讲到现在,前半部分是在疯狂安利 CRDT,后半部分却又不停地对它泼冷水,为了避免你不知道我的目的是啥,这里我提前做一点小结。
|
||||
|
||||
总的来说,如果你现在还有得选,那么我建议你引入 CRDT 算法,作为你的低代码平台的底层数据保存和历史记录管理功能的基础,即使你现在看不到有实现多人协作功能的必要,但 CRDT 也可以提供成熟的数据结构、历史记录管理等有价值的功能。还有,万一以后做大了,需要多人协作功能的话,就不需要再去苦思解决方法了。
|
||||
|
||||
现在主流的几个实现 CRDT 的库中,我推荐你重点关注 Yjs,但可以适当了解一下 automerge 和 ref-crdts。Yjs 除了性能优越之外,还提供了 UndoManager 用来处理历史编辑记录,提供了回退和重做的功能,这两点是其他两个库所不具备的。
|
||||
|
||||
同时我们也要注意到,CRDT 算法在低代码平台的应用案例还不多,至少我还没看到有实际应的用案例,所以你需要重点关注和评估我列出的 3 个注意点,也就是由于未及时记录带来的冲突、渲染性能挑战、多行属性值的合并,对应的解决措施你可以翻到前文再看看,我不再重复。
|
||||
|
||||
万一,你和我一样已经没得选了,有没有补救的方案呢?
|
||||
|
||||
有没有补救方案?
|
||||
|
||||
说没有是不可能的,但补救方案要根据已有实现而定,没有标准解。我介绍一下我实际采用的补救方案,希望对你有所启发。
|
||||
|
||||
我先介绍一下我们低代码平台的背景,主要是下面两个困难让我与 CRDT 失之交臂:
|
||||
|
||||
|
||||
应用工程数据与应用开发人员账号是强关联关系(物理关联),并且所有的自动化脚本都建立在这个关联关系之上,如果强制改为逻辑关联,会导致所有自动化脚本都需要改写;
|
||||
前端持久化数据时,一时偷懒,每次都发送全量数据给后台,而后台则是基于全量数据为基础来实现的历史管理功能。之所以保存时采用全量数据,一方面是追求更快实现功能,内网速度快,不需要“省吃俭用”,使用全量数据可以快速实现功能;另一方面是实现非常简单可靠,全量数据拿到后直接持久化即可,如果是增量数据则还需要合并,当时的 Yjs 等完成度远不及当下,没敢入坑。
|
||||
|
||||
|
||||
从这两个困难中可以看出,当初我是一点都没有考虑到多人协作这个功能,不然也不可能实施这样的方案。
|
||||
|
||||
那么在补救方案中,我首先要解决的是应用工程数据与应用开发人员账号强关联的问题。让每个人都用同一个账号是不可能的,因此,我们还是要想办法让多人协作的应用工程数据能做到“人手一份”。
|
||||
|
||||
要实现数据的冗余是很容易的,我设计了一个分享动作,允许开发人员将一个工程分享给其他人,每一次的分享,都是对数据的一次冗余。
|
||||
|
||||
收到分享的开发人员,就可以像对普通工程那样对它做编辑。每次编辑产生的修改记录也和普通工程一样,被发送给后端,持久化到该开发人员名下的那份冗余的数据里,所有持有该冗余数据的开发人员此时是各干各的,互不影响。可以看到,这个过程对已有的流程是完全没有冲击的,除了新增的分享功能之外,完全复用原有流程。
|
||||
|
||||
难点在于冗余的数据如何实现归一。
|
||||
|
||||
如果要做到两两合并,过程会非常复杂,而且极其容易出错。因此我做了一个约束,冗余的数据能且只能合并到最原始的版本去。下面是相应的示意图:
|
||||
|
||||
|
||||
|
||||
无论数据在中途被转发了多少次,它始终记录着原始账户名。只有原始账号才能发起合并操作,并且,每次合并只能指定一个目标冗余数据。这样就可以控制每次合并动作都只发生在两份数据之间,并且合并的方向是确定的,始终都是合入到原始所有者的版本中去。在合并完成之后,原始数据所有者会把合并后的数据发送给对方,对方收到数据后,不需要合并,直接覆盖掉合并前的数据即可完成两者同步。
|
||||
|
||||
在实际操作时,也可以做进一步的约束,不允许二次分享应用数据,这样会让这个拓扑图看上去简单许多,但是这对数据归一的复杂性没有帮助,只是看上去简单一些而已。
|
||||
|
||||
|
||||
|
||||
接下来我们再说说如何合并两份数据。经过前面的一番约束之后,单次合并只剩下两份数据了,看起来可以直接合并,并且不可能出现冲突了。真的是这样吗?你可以先思考思考。
|
||||
|
||||
虽然只有两份数据了,但是这两份数据依然是有 3 个状态,只要有 3 个状态的合并就必然会有潜在的冲突。其实这两份数据完全可以拿 git 的两个分支来类比,如下图。
|
||||
|
||||
|
||||
|
||||
一开始只有一份数据,在分享出去的一刹那,就产生了分叉,并且这两个分叉将在两个不同的开发人员手里独自演进。只要修改没有冲突,那就可以直接无脑合并,所以合并前的关键步骤就是如何检测有哪些冲突点。比如上面这图,是拿节点 2 和节点 3 来比对冲突点吗?
|
||||
|
||||
只有两个节点是不会有冲突的,至少要有 3 个节点才会有冲突,图中的分叉点 1 就是这第三个节点。比对过程是这样的,拿节点 2 和节点 1 相比,得出一组修改集,每个修改点是由属性 xpath 和属性值变更行号组成的二元组。再拿节点 3 和节点 1 相比,得出另一组修改集。然后从这两组修改集中筛选出所有相同修改点。二元组的两个属性都相等,则认为是同一个修改点。每一个相同的修改点,就是一个冲突点。
|
||||
|
||||
如果一个冲突点都没有,那意味着可以直接合并,合并之后的历史记录如下:
|
||||
|
||||
|
||||
|
||||
图中虚线框是原来两个分支各自使用修改点融合在一起后的集合。这个过程就和 git 合并时,把一些 commit squash 在一起然后 rebase,是一样的。
|
||||
|
||||
当冲突出现时,是否有 LWW 这样的策略可以用呢?
|
||||
|
||||
很可惜,没有,只能手工解决冲突。单行属性值的冲突,可以二选一,多行属性值冲突,则需要采用和 git 合并代码时相似的解决方式。Monaco这个编辑器内置提供了 diff 功能,带有差异点着色功能,非常棒,再次推荐你使用。
|
||||
|
||||
解过冲突的人都知道,这不是一个愉快的过程。因此,我建议你采用这样的方法来缓解这个不愉快:任何协同者对应用数据做了修改,都在后台实时计算出是否有冲突,一旦出现第一个冲突的时候,立即给出提醒,甚至强制要求解决冲突。
|
||||
|
||||
这样可以降低解决冲突的难度,避免冲突过多导致无法合并。同时,也可以对未合并的修改计数,当数量超过某个阈值时,通过一些非侵入性的方式提醒协同者要及时合并。常见的非侵入性提示是在侧边弹个气泡,就像这个效果。
|
||||
|
||||
与代码比对不同,低代码平台上应用的“源码”是一份结构化数据。所以直觉上,你可能会首先想到要去找一个 json 比对工具来辅助。但实际上,对应用的结构化源码的比对,不能采用通用的 json 形式。
|
||||
|
||||
举个例子你就清楚为啥了。在已更新的内容中,我不止一次提到容器是一种特殊的组件,它可以把其他的容器或组件装在它内部,所以在应用结构化源码中,容器的子级是一个数组。对通用 json 数据来说,顺序是敏感的,但容器子级数组里的对象的顺序不一定是敏感的(有可能敏感,也有可能不敏感)。在子级顺序不敏感时,如果依然严格按照数组顺序来比对,必然会得到一个很大的错误的修改集。
|
||||
|
||||
所以,比对在应用结构化源码时,我们必须根据组件的 id 找出两份数据中的同一个组件,然后,再根据组件的 schema 依次遍历组件的所有属性,这样获取到的修改集才是准确的。对于同一个组件的配置数据来说,它有哪些属性是事先已知的,你在实现低代码平台的时候,一定会有一份 schema 用于描述组件所具有的配置项。
|
||||
|
||||
总结
|
||||
|
||||
我在前面已经提前对 CRDT 做了总结了,你可以翻回去回顾一下。这里我再补充一点,Yjs 在保存数据时,采用了 quill 的delta数据结构,只发送增量部分的修改,而非发送全量数据。这是一个很好的特性,一方面节约带宽、使得同步更及时,另一方面安全性更好。如果你拿捏不准以后是否需要有多人协作的功能,也可以先引入 Yjs 这样的 CRDT 解决方案,为以后的演进留一条路。
|
||||
|
||||
这讲中我给出了我所使用的一个补救的方案,不一定适合你的实际情况,但我在这部分给出了两个导致我无法使用 CRDT 的原因,你可以着重了解一下,绕开我所犯的错。
|
||||
|
||||
第一点是注意不能让应用数据与开发人员账号形成物理隔离的关系,即不要把应用数据保存到开发人员名下,形成一一对应关系。而是将所有数据都集中存在一个地方,然后通过关联关系挂到开发人员名下,从而形成逻辑隔离,这样日后要实现应用数据与开发人员账号之间的多对多关系,就会简单许多了。
|
||||
|
||||
第二点是数据持久化时偷懒采用发送全量数据的方式,这导致后端保存数据的方式与 CRDT 的各种实现所使用的增量保存的方式有很大差异,从而导致后端改造成本巨大。
|
||||
|
||||
总之,即使你后续不需要实现多人协作功能,也可以现在就引入 Yjs 这样的 CRDT,是一个很好的选择。
|
||||
|
||||
思考题
|
||||
|
||||
假设你也没有条件使用 CRDT 来解决多人协作的问题,你会采用啥样的补救方案?欢迎在评论区简要写下你的方案。
|
||||
|
||||
下一讲我会说说低代码编辑器的编辑历史的实现,你可以做些准备。我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
217
专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md
Normal file
217
专栏/说透低代码/14|编辑历史:是对Git做改造,还是另辟蹊径?.md
Normal file
@ -0,0 +1,217 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
14|编辑历史:是对Git做改造,还是另辟蹊径?
|
||||
今天来聊聊低代码编辑器的编辑历史。人生没有后悔药,但在计算机世界里,编辑历史就是后悔药,不但常见,而且非常廉价。
|
||||
|
||||
多数编辑器,如代码编辑器,或者我正在码字的编辑器,都会配备最普通的编辑历史管理功能。你可以从近到远地撤销(Undo)掉所做的修改。如果撤销了之后,没有做过任何编辑操作,还可以重做(Redo)这些修改,即撤销掉撤销操作。一般在撤销之后,又做了任何操作了,那么所有重做的历史记录都被删除。示意图如下:
|
||||
|
||||
|
||||
|
||||
编辑历史是一个非常重要且友好的功能,它最主要的目的是鼓励应用开发勇于试错。开发的过程,就是一个不停试错的过程,试错成本越低越好。
|
||||
|
||||
作为低代码编辑器,我们同样也要支持上面提到的基本历史管理功能。不仅如此,还要更进一步,支持历史记录多分支功能。比如前面的示意图就是这样,第 3 步重新编辑后,得到了编辑记录 2,此时,编辑器会把记录 1 丢掉,目的是要维持历史记录的单向性。不丢弃记录 1,并且允许该记录继续编辑下去,就会形成历史记录多分支的情形,这和 Git 的分支非常相似。
|
||||
|
||||
做过程序开发的都会用到 Git,程序员们看中 Git 的最主要一点就是它强大便捷的分支管理。低代码编辑器作为一个开发工具,对多历史记录分支的支持也是必要的。所以今天这讲,我们就来说说低代码编辑器的编辑历史功能怎么实现。
|
||||
|
||||
基本编辑历史功能的实现
|
||||
|
||||
上一讲中,我推荐你将 Yjs 这个 CRDT 实现用作多人协作功能的基础算法。即使你不打算提供多人协作功能,我依然推荐你考虑引入 Yjs,它不仅提供了一套完善的增量式编辑历史持久化机制,还提供了一个UndoManager来协助编辑器完成基本历史管理。如果你有机会使用 Yjs,那应该好好考虑一下这个工具。
|
||||
|
||||
今天我们不会展开讨论UndoManager的用法,有必要的话,给我留言,看情况我再决定是否专门聊聊。但不管你是否采用 Yjs 算法,我都建议你继续学习这一部分,我会从别的实现方式讨论编辑历史的实现,他山之石可以攻玉。
|
||||
|
||||
那么,除了 Yjs,还有其他的工具没?
|
||||
|
||||
Git 就是一个很好的备选。Git 轻量小巧,性能优越。而且,它自带了一个文件数据库,用来保存修改历史数据,通过 git commit 和 git log 可以实现对编辑历史的管理。不过,也许你会担心历史记录数量大了之后 Git 的性能表现,我特地去看了Linux 内核代码仓库,现在已经超过 100 万 commit 了。而基于我们的低代码平台 Awade 开发的、那些比较活跃的 App,历史记录数也还未过万。
|
||||
|
||||
Git 有非常强大的历史分支管理能力,用于多人协作时,可以利用 Git 强大的自动合并功能,避免上一讲中自行实现多人协作的许多麻烦。而且,由于所有合并和记录操作都可以在服务端上通过 Git 来完成,前端就变得更轻量了,不需要引入 Yjs 这样的库了。同时,Git 的分支管理能力也可以用于这一讲后面要讨论的、低代码编辑器多分支编辑历史功能的实现。这方面无须对 Git 的能力有任何质疑,它生来就是为解决这个问题的。
|
||||
|
||||
虽然 Git 没有一个官方社区(或者有但我不知道),但现在世界上有数以千万计的开发人员都在用 Git 来管理代码,基本上你碰到的任何问题都可以在网络上找到大量的解决方案。这也是一个无可比拟的优势。
|
||||
|
||||
那我们可以怎么使用 Git 呢?有两种使用 Git 的方式,单仓库方式和多仓库方式。这里我们简单讨论一下。
|
||||
|
||||
单仓库方式是把所有的应用数据都放在一个 Git 仓库中,优势是简单只需初始化一次,代价是仓库可能会非常大,历史记录也会非常多,所以我并不推荐你使用这种方式。
|
||||
|
||||
我推荐你使用多仓库方式,也就是每个 App 的数据都保存在一个独立的 Git 仓库中。这是一个比较好的方式,git init 一键初始化一个仓库,各个应用数据物理隔离,安全性高。
|
||||
|
||||
那么 Git 有没有不适用的一面呢?
|
||||
|
||||
我们都知道,Git 适合用于管理多行文本类的数据,它对长单行文本和二进制数据几乎没有办法。但是,我们的低代码编辑器需要管理少量二进制数据,如图片、字体等,可能还有少量的 Excel 文件。
|
||||
|
||||
对长单行文本的无能为力是 Git 更致命的一个短板。低代码编辑器一般都是基于结构化数据的,为了简单,会将结构化数据序列化(JSON.stringify)之后进行存储。结构化数据序列化后,就是一个长单行文本。
|
||||
|
||||
即使在序列化结构化数据时保持多行的结构,但应用数据里的某些多行属性值,在序列化之后却依然是长单行文本。比如下面这样代码块:
|
||||
|
||||
const a = 123;
|
||||
const b = '123';
|
||||
console.log(a, b);
|
||||
|
||||
|
||||
如果不做任何处理,在序列化后的值是这样的:
|
||||
|
||||
{
|
||||
"value": "const a = 123;\nconst b = '123';\nconsole.log(a, b);"
|
||||
}
|
||||
|
||||
|
||||
那么,面对这样的情况,如果拿 Git 做我们的解决方案,应该怎么办呢?
|
||||
|
||||
我们可以把多行文本按照换行符拆分为字符串数组,然后再存储:
|
||||
|
||||
{
|
||||
"value": [
|
||||
"const a = 123;",
|
||||
"const b = '123';",
|
||||
"console.log(a, b);"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
使用时,再把这个字符串数组读出来,然后用 arr.join(‘\n’) 就可以还原了。
|
||||
|
||||
不过,有的低代码编辑器为了进一步节约空间,不采用 JSON.stringify 来序列化数据,而是采用比如Uint8Array这样的二进制方式来持久化。这个情况下,Git 就几乎帮不上忙了。我们应该避免这样的做法,改用 JSON.stringify。
|
||||
|
||||
此外,还有一个问题不那么“显眼”,但其实非常麻烦,Git 采用 GPLv2 协议对外分发版本。这个协议有“感染性”。简单地说,任何软件如果使用了这个协议的软件,都必须开源,否则人家的基金会就会告你,基本一告一个准,赔了钱后,要继续用还必须开源。所以如果你做的是一个商业性质的低代码平台,那么要谨慎。
|
||||
|
||||
那是不是就不能用了呢?也不是。只要一个商业软件发布的版本里不包含 Git 的任何文件(二进制或者源码)就好了。所以,你可以想办法给你的客户的运行环境上预装好 Git,就可以绕过去了,或者在安装好你的低代码平台之后,再给一个独立的安装 Git 的流程也行。总之就是要解除你的平台和 Git 等 GPL 软件的捆绑。
|
||||
|
||||
也许你会对商业合规嗤之以鼻,那是因为你所在的企业没有像中兴这样,差点因商业合规而面临倒闭。但合规无小事!
|
||||
|
||||
虽然上述两个方案都可行,但它们却都不适用于我们的低代码平台 Awade,不能用 Yjs 的原因我在上一讲已经说得很详细了,不用 Git 的一个很主要因素是它使用了 GPL 的协议。在中兴,除非实在是没有替代品,否则所有使用 GPL 协议的软件都默认不用,即使这个软件有一定优势,但漫长累赘的备案审批流程,就能让人退避三舍,所以我们技术选型时,一看到是 GPL 的,直接 Pass。因此,我们自己搞了一个解决方案,这里简要描述一下,供你参考。
|
||||
|
||||
在上一讲中我说过,Awade 在持久化时,为了简单,是按照全量数据来存储的,在实现编辑历史的时候,也是基于全量数据的,每个历史记录里都是一份完整的数据。因此撤销也好,重做也罢,就是重读一下数据而已,非常粗暴,但实现非常简单。代价是 Awade 的历史记录数据非常大,一个 App 动辄就有数 G 的历史记录。
|
||||
|
||||
每个历史记录都有一个配置文件,用来记录它与其他历史记录的关系。这里的关键部分实际上是一个双向链表。一个历史记录有自己的 id,next 指针指向下一个记录 id,previous 指针指向前一个记录的 id,示意图如下:
|
||||
|
||||
|
||||
|
||||
示意图1
|
||||
|
||||
你可以看到,这是一个典型的双向链表结构,很好理解。同时,App 工程数据里还有一个游标(cursor),用于指向当前正在使用的历史记录。新增修改记录的时候,将游标移向下一个记录,撤销或重做的时候,实际上只要修改 cursor 的值就好了。
|
||||
|
||||
基本编辑历史的实现,大致就是这样,接下来我们更进一步,聊聊前面一直提到的编辑历史多分支的实现。
|
||||
|
||||
编辑历史多分支的实现
|
||||
|
||||
很久之前,我收到一个投诉,应用的一个开发人员气冲冲地抱怨说,他将 App 回退到了某个历史记录,但做了一个意外的编辑操作,Awade 二话不说就把回退的编辑记录全部删除了,这导致他找不到 App 的最新数据了。这个事情让我意识到,是时候要让 Awade 支持编辑历史多分支的能力了。
|
||||
|
||||
作为一个开发平台,代码托管能力是必须要有的,而多分支是代码托管的必备功能。同时,有了编辑历史多分支功能,平台就不需要为了保持单一的编辑历史分支而删除被撤销的历史编辑记录了。
|
||||
|
||||
我们依然使用双向链表来表示这个功能,非常简单。如前文的示意图,当前的游标指向的是历史记录 4,如果应用开发此时做了一个编辑,那么只需要将历史记录 4 作为一个新的链表的开始,并新建一个历史记录 7 就好了:
|
||||
|
||||
|
||||
|
||||
示意图2
|
||||
|
||||
正常的链表是不会出现分叉的,但这里为了示意记录 4 的关联性,我将这两个独立的链表画在了一起。请注意,这里用链表只是为了帮助你理解历史记录的数据结构。实际实现时,我们并不会真的去创建一个独立的链表,而是直接新建一个历史记录,并将其 pre 值指向 4,将记录 4 的 next 指向 7,并将游标从记录 4 移到记录 7,仅此而已。甚至都不需要关心在编辑时的游标是不是最新记录!
|
||||
|
||||
无论应用开发如何撤销、重做、甚至在历史记录中任意跳跃,都只要将游标移动到正确的位置就好了。在编辑时,无论游标在哪,都可以无脑地新建记录节点,配置好相邻 2 个记录的 next 和 previous 值,并移动游标指向新记录就好。
|
||||
|
||||
说到低代码平台需要有代码托管能力,那就不得不再说回 Git,因为 Git 最主要的目的就是解决代码托管问题,而且它是全球数以千万计的开发人员共同的选择。
|
||||
|
||||
而且,要论分支的管理能力,Git 说第二,没人敢说第一,强大的多分支管理是 Git 最有价值的能力之一。因此,如果你选型了 Git 作为历史记录管理器,那么,这里只需要开发适当的可视化界面以充分发挥 Git 的能力即可。
|
||||
|
||||
现在市面上有大量的 Git 命令行可视化工具,也有集成在软件开发 IDE 中的菜单式的 Git 命令行可视化方案。但请注意,这些解决方案没有一个适合用来解决低代码编辑器的编辑历史分支管理。这些解决方案都是为了解决 Git 命令行难用的问题而生的,都是为了 Git 而 Git。但是这里我们仅仅是为了实现编辑历史记录多分支而使用 Git,所以所采用的可视化解决方案都应该是围绕这个目标来设计。
|
||||
|
||||
其实,即使你选型了 Git,你也可以参考 Awade 的可视化方案,把多分支的增删改查都“藏”到历史记录的操作中去:
|
||||
|
||||
|
||||
在回退编辑记录之后再次编辑,则使用 git branch 命令自动新建一个分支;
|
||||
在删除了某个编辑记录(在 Git 中对应一个 commit),则找出所有包含这个 commit 的分支,并将其删除;
|
||||
在需要列出所有的分支的时候,使用 git branch 即可,并且使用 git merge-base branch1 branch2 这个命令来找出两个分支共同的 commit,这样就可以画出类似示意图 2 的修改记录逻辑分支图了。
|
||||
|
||||
|
||||
此时,Awade 和 Git 这两个方案殊途同归,差异只在于持久化数据的方式。
|
||||
|
||||
可视化分支管理的实现
|
||||
|
||||
我们前面的内容都在讨论编辑历史记录和多分支的后台实现,无论是 Awade 的方案,还是基于 Git 的方案,都可以很好地实现编辑历史记录的存储和多分支的实现。接下来我们就要说说前端侧的实现了,也就是如何实现分支管理的可视化。
|
||||
|
||||
我们依然分两个方案进行,我先从 Awade 的方案开始讲。
|
||||
|
||||
前面我说了,Awade 在后台采用了双向链表的方式来存储编辑历史记录和编辑历史分支。我们讨论了半天都在说双向链表,那哪来的多分支呢?
|
||||
|
||||
其实,示意图 2 已经剧透了,示意图 2 就是 Awade 展示在应用开发人员面前的分支视图。当然,实际展示出来的时候,与链表相关的细节是隐藏掉的,他们不关心也不需要知晓背后的机制。
|
||||
|
||||
但你是需要关注这背后的机制的。所以我这里和你简单介绍一下如何找出历史分支的算法。我在前面说过,新增历史记录时,Awade 是不用管游标的位置的,直接无脑新建历史记录即可,物理上看,这些记录实际上只是这样的一串带有编号的平铺的历史记录序列而已:
|
||||
|
||||
|
||||
|
||||
我们需要从编号最小的开始遍历,根据它记录的 next 值,就可以快速找到如示意图 1 所示的一个链表了,我把示意图 1 搬过来了,你不用往回翻了:
|
||||
|
||||
|
||||
|
||||
示意图1
|
||||
|
||||
这就是第一个分支,标记这个链表里的所有节点。然后我们将所有未被标记的节点归为新的一组,重复这个过程,依然从编号最小的记录开始(本例中它的编号为 7)。这里与首次处理过程的差异是,我们要先根据这个记录的 previous 值,将它“挂到”previous 值指向的记录后面去。这个记录的 previous 值所指节点(本例中它的编号为 4),就是一个分叉节点。递归这个过程直到没有未被标记的节点为止,我们就可以得到类似示意图 2 这样的逻辑关系图了:
|
||||
|
||||
|
||||
|
||||
示意图2
|
||||
|
||||
为了加深理解,我再举一个更加复杂的例子,最终绘制出的逻辑分支图如下:
|
||||
|
||||
|
||||
|
||||
示意图3
|
||||
|
||||
在最开始的时候,也是这样的一串带有编号的平铺的历史记录序列:
|
||||
|
||||
|
||||
|
||||
从最小的记录 1 开始,持续遍历 next 属性,就可以得到第一个分支,并把未标记的节点组成新的一组:
|
||||
|
||||
|
||||
|
||||
从记录数最小的 6 号开始,它的 previous 指向记录 2,于是要把 6 号记录挂到 2 号记录下,然后遍历所有 next 值,得到另一个分支:
|
||||
|
||||
|
||||
|
||||
重复这个过程,找到最小的 9 号记录,挂到 3 号记录下面,遍历它的 next 后剩余 11、12 号两个记录,把 11 号记录挂到 7 号下,发现 11 号记录的 next 值就是 12 号。这样一来,所有的历史记录就都被挂到正确的位置去了,算法终止。最终得到了示意图 3 这个逻辑关系图。
|
||||
|
||||
这是一个时间复杂度为 O(n) 的算法,看起来性能不怎么样,但是 Awade 为了避免历史记录无限膨胀,做了一个最大记录数的限制(我们限制最大 600 个记录),会自动清理掉过老的历史记录。这样就可以确保这个算法的时间复杂度是常数级了。
|
||||
|
||||
我将示意图 3 称为历史记录树,你可以将这图画出来给应用开发人员看,这样可以帮助他们管理好当前所有编辑历史记录的逻辑关系。同时,你还可以在这颗历史记录树上增加一些操作,比如选择一个节点后,一步跳转过去。还可以提供历史记录删除的功能,在删除选中的节点时,它右侧(编号比它大)的所有记录都需要一起删除。
|
||||
|
||||
历史记录树,再加上树节点的选择、删除等操作,就共同组成了历史记录的可视化管理功能。你可以看到,这个过程中,我们用了许多诸如分支、链表这样的晦涩术语,但最终呈现在界面上的,却只有一棵树,以及两三个按钮,这是非常友好的。
|
||||
|
||||
接下来我们说说如果你后台用了 Git,该如何实现分支可视化管理。
|
||||
|
||||
其实,UI 部分,你完全可以使用 Awade 的解决方案。因为 Git 本身就有极强的分支管理能力,所以实现起来就更简单了。你可以通过 git branch 命令列出当前所有分支,再使用 git merge-base branch1 branch2 这个命令找出两个分支共同的 commit。这个共同的 commit 就是分叉点,如示意图 3 中编号为 2、3、7 号记录,这样就可以画出类似示意图 3 的修改记录逻辑分支图了。
|
||||
|
||||
编辑历史分支的合并
|
||||
|
||||
最后,我们再看看编辑历史的分支如何进行合并的问题。
|
||||
|
||||
Git 本身就有分支合并的功能,我们直接使用 git rebase 命令就可以了。如果采用 Awade 的方案,你就还需要有进一步的说明。你可以回顾一下上一讲的多人协作的实现,多人协作的自动合并过程,与编辑历史分支合并的过程,是异曲同工的。
|
||||
|
||||
多人协作的合并目标,是同一个应用数据在两个不同人手里的两个版本,而编辑历史分支合并的目标,是同一个应用数据,在同一个人名下的两个不同的版本。都是同一个应用数据的两个不同版本的合并过程,本质上合并过程是一致的。因此,你直接采用我们上一讲给出的方法就可以实现编辑历史分支的合并了。
|
||||
|
||||
总结
|
||||
|
||||
今天这一讲,我们主要解决了低代码编辑器的编辑历史功能。从最基本的撤销、重做这样的编辑历史功能开始,再到编辑历史多分支的实现,多分支编辑历史对低代码编辑器这样的开发工具来说,是一个很有必要的功能。这不仅是因为低代码平台需要有代码托管能力,更是因为多历史分支可以帮助应用团队更好地开发复杂度更高的 App。从这个角度来讲:编辑历史功能至少在这些方面可以发挥作用:
|
||||
|
||||
|
||||
降低应用团队试错的成本,他们可以大胆地对 App 进行探索性地修改,在获得成果之后,将探索成果合并到主分支上,否则直接丢弃即可;
|
||||
帮助应用团队进行一站式的应用版本管理,可以将有的分支作为发版分支,有的分支作为系统测试分支,有的分支作为 Dev 分支。
|
||||
|
||||
|
||||
我们这一讲采用了两个相互独立的后台实现方法来实现编辑历史,一个是 Awade 采用的基于文件 IO 的方式,一个是基于 Git 的方式(其背后是基于文件数据库)。这两种实现方案都可以以比较简单、高性能的方式达到后台编辑历史管理的目的。
|
||||
|
||||
其中 Git 的解决方案,我们需要考虑它是基于 GPLv2.0 的版本分发协议,需要关注商业合规,而 Awade 采用的则是纯自研的方式,具有完全知识产权,没有这方面的担忧。
|
||||
|
||||
无论后台采用哪种方式实现,从应用开发人员的角度看,在前端的可视化分支管理功能的实现上,Awade 和 Git 这两个方案没有区别,操作方式也基本一样,不需要区分后台是如何实现的。
|
||||
|
||||
但有一点需要注意,如果后台采用的是 Git,切勿掉入可视化管理功能为了 Git 而 Git 的误区。目前致力于可视化 Git 命令行的解决方案很多,但这些方案都立足于替代 Git 的命令行,而非解决编辑器的历史管理能力,在我看来,这些解决方案都不适用于这一讲需要解决的问题。
|
||||
|
||||
思考题
|
||||
|
||||
支持多分支编辑历史的功能,是低代码编辑器具有代码托管能力的一种体现,多分支编辑历史这个功能也当然需要承担代码托管的更多功能。除了帮助应用试错和版本管理之外,你认为多分支编辑历史这个功能还需要承担其他哪些代码托管的功能?如何实现?
|
||||
|
||||
欢迎在评论区留下你的见解。我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
147
专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md
Normal file
147
专栏/说透低代码/15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?.md
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15|低代码平台应该优先覆盖应用研发生命周期中的哪些功能?
|
||||
今天我们来说说低代码平台除了开发能力之外还需要什么能力。
|
||||
|
||||
我们专栏的常规更新部分,到现在已经更新到尾声了。前面好几讲的内容,我们都在关注低代码平台的开发能力。对低代码平台来说,开发能力当然是最重要的一种能力,没有之一。毫不夸张地说,开发能力直接决定了低代码平台的综合能力上限。
|
||||
|
||||
但低代码平台不能一味追究开发能力,也需要关注开发能力之外的能力。开发能力之外的其他能力决定了低代码平台的总体能力下限。在低代码平台发展初中期,我们当然是需要坚持优先发展开发能力,但是低代码平台有了一定成熟度之后,就需要开始关注并适当发展其他的能力了。
|
||||
|
||||
|
||||
|
||||
那么低代码平台除了开发能力之外,还需要发展哪些能力呢?
|
||||
|
||||
我们可以以 App 开发的生命周期为线索来寻找这些功能。以开发能力为中心,它的左侧为需求端功能,右侧为交付端的功能,上方为生产环境管理功能,下方为资产管理功能:
|
||||
|
||||
|
||||
|
||||
我们简单分析一下这张图。在需求端,一般有两种协同方式:
|
||||
|
||||
|
||||
产品需求→UX 设计团队→业务开发团队:一般在 App 初创时走这个流程;
|
||||
产品需求→业务开发团队:一般在 App 迭代过程走这个流程。
|
||||
|
||||
|
||||
低代码至少可以在这两个环节上发挥作用。对于第一种,UX 设计团队输出的是设计稿,低代码平台可以自动将设计稿转为可用代码,这个功能也就是 D2C 功能。对于第二种,低代码平台可以提供数量众多的模板和业务组件,给业务团队“抄作业”。
|
||||
|
||||
在交付端,App 上线或者交付之前,最重要的一个环节就是测试了,自动化测试是效率最高、最可靠的测试方式,那么低代码平台能否在 App 的自动化测试方面有所作为呢?另外,App 上线之后,想要获知用户对 App 的使用情况、App 的运行状况,就需要事先植入埋点,用于采集数据,然后要有办法拿到埋点数据,并提供分析功能。
|
||||
|
||||
在生产环境管理端,低代码平台至少可以在运行时自动部署方面提供帮助,特别是一些容器化的运行时,每个 App 上线之前,都需要制作和配置镜像,蓝图、Dockerfile,以及其他配置文件林林总总可能有数十个配置项需要填写,任何一个填错就会导致部署失败。一些重要的 App 上线后还有一个灰度发布的过程,需要进行灰度配置,甚至紧急回退等功能,也可以集成到低代码平台上。
|
||||
|
||||
资产管理端是将 App 作为一种业务资产来看待,包括代码自动评审、入库等,与代码托管相关的工作,甚至还可以覆盖 App 的自动化版本管理等与办公自动化相关的一些功能。这些都可以集成到低代码平台上,实现 App 从开发到管理的全生命周期管理能力。
|
||||
|
||||
这样一分析,你看,其实低代码平台在 App 开发的全生命周期中,能做的事情还有很多。这些能做的事情,也不是全部都要我们原创。我们可以把许多传统编码开发过程中需要人工完成的事情,实现一定程度的自动化,然后集成到低代码平台上,实现一站式管理,这样就已经可以发挥许多作用了。即使无法做到全自动,哪怕实现了半自动化,也是不小的进步。
|
||||
|
||||
这一讲的内容特别多,甚至我们还能在很多方向上展开聊聊,每块儿单独成一讲。所以我想着,索性将这讲作为一个提纲来总体分析,给你一个大致方向。后续在动态更新部分,我还会根据我的经验和同学们的需要与反馈,针对性挑选相应的功能,展开聊聊它们的具体实现。
|
||||
|
||||
需求端
|
||||
|
||||
需求端的 D2C 功能和模板能力,都是非常有用的功能。其中 D2C(Design to Code)功能是 UX 设计稿转代码的功能,这是一种很酷的能力,但是它的优先级远没有模板功能高。模板是 App 的半成品,或者是 App 中常用的、有代表性的一个部分。
|
||||
|
||||
模板最大的意义和价值就在于,应用团队可以快速地抄作业,不用从零开始开发。当积累了一定数量的模板后,再对它们进行分门别类,应用就可以根据自己的目标 App 挑选接近的模板,然后在模板的基础上继续将其演进为一个完整 App。从这个角度看,模板和纯编码模式下的脚手架差不多,但比纯编码的脚手架更优秀的是,这是一个可视化的、所见即所得的脚手架。
|
||||
|
||||
对任何具有开发功能的工具来说,模板有很大的意义。毕竟,开发的过程就是探索的过程,你想一下,如果你是一个缺乏经验的开发者,即使你手里握着低代码这样的先进生产力工具,但当你面对一片空白的画布的时候,依然会无所适从,不知道从何着手吧?模板就是一种极佳的破冰解决方案,给了开发者一个探索的方向。
|
||||
|
||||
而且,模板除了可以起到给应用团队抄作业的功能外,还有一个价值:模板天然就是一种学习的教材,它是一种样板实现范例。应用开发人员拿到一个模板之后,依葫芦画瓢,逐渐摸索,就能逐渐学会使用低代码平台。根据我的经验,大家不爱看使用手册,更喜欢直接上手摸索、试错。
|
||||
|
||||
利用代码直接识别 UX 设计稿,并生成可用的代码的技术(简称为 D2C),在过去两三年里,也得到了长足的发展,各大互联网巨头都纷纷推出各自的解决方案。实现的方法多种多样,眼花缭乱,简单的就是计算机视觉技术,再复杂一点就是 AI 图像识别。之所以要搞得这么复杂,门槛这么高,是因为现在的 D2C 工具都自成一体,它不仅需要识别 UX 设计稿,还需要将识别到的设计稿转为对人类友好的代码。
|
||||
|
||||
不知道你有没有从 D2C 实现的关键步骤里发现了什么?你看,生成代码这样的功能,不就是低代码平台擅长做的事情吗?这个功能,我们已经在专栏里多次讨论到了呀!那么,在低代码平台上来实现 D2C 功能,会不会比传统的 D2C 解决方案要简单许多呢?
|
||||
|
||||
事实也确实如此!我们只需要从 UX 设计稿里识别出足够多的信息,“喂”给低代码编译器,低代码编译器就可以把代码给生成出来了。
|
||||
|
||||
那么哪些信息是“足够多”的信息呢?我们以通用场景为例,必须得到的信息是设计稿中各个组件的种类、位置、尺寸这几个值,其他可选信息有文本,颜色,图标等。
|
||||
|
||||
其中组件的类型是最关键的信息。有了组件的类型,我们仅仅根据 UX 设计规范,就可以获得这个组件的大量预设数据了,而 UX 设计规范是静态的,任何设计稿都是它的一个实例而已。因此,只需要从设计稿里读出组件类型,无须其他数据,我们就可以把这个组件的大部分信息推断出来了。
|
||||
|
||||
不过,位置和尺寸是 UX 规范给不了的,这两个信息决定了生成出来的 UI 是否可用,因此我们必须从设计稿中读取出来。其他的信息就都是可选的了,不会影响最终成败,但如果我们能从设计稿里识别出更多的其他的信息,也能提升自动化率。
|
||||
|
||||
所以说,在低代码平台上实现 D2C 的功能,虽然不能说有多简单,但是也不至于需要用上计算机视觉甚至 AI 这样的高精尖技术。在动态更新部分,我们会有专门一讲,讲解如何采用“平民化”的技术为低代码平台增加 D2C 能力。
|
||||
|
||||
交付端
|
||||
|
||||
自动化测试是软件交付过程中一个非常有价值的手段,而 UI 的自动化测试,是软件自动化测试皇冠上的明珠。
|
||||
|
||||
UI 自动化测试的成本很高,这一点不是因为测试用例代码难写,主要是测试用例代码调试成本高,测试用例代码的维护成本更高。这样的特点往往会让参与 UI 自动化测试的人笑着进去,哭着出来。刚开始的时候,轻轻松松十来行代码就可以驱动浏览器完成被测 Demo 页的自动化操作,很酷,但是一旦被测页面复杂起来之后,特别是与鼠标相关的操作多起来之后,人工轻而易举就可以实现的操作,调试用例时却要花大力气才能完成。
|
||||
|
||||
然后你坚持不懈,好不容易跑通了几个功能点的测试用例调试,结果还没跑上几天,就发现测试用例跑不过了。调试一番后,才发现有人修改了页面的一个 class 名或者调整了 dom 结构,导致用例的 selector 找不到 DOM 节点了。这是导致用例跑不过的大多数原因。
|
||||
|
||||
而且日常 Web 页面的开发,就充斥着大量这样的操作,会导致测试用例代码常常莫名其妙跑不过。这样的问题主要看需求紧不紧张。需求不紧张的时候,调试调试就可以解决了,但在需求紧张起来时,你猜第一个被砍掉的代码是哪些?显然是测试用例代码,说好过几天再修复,然后就没有然后了。
|
||||
|
||||
这就是 UI 自动化测试的现实和困境。那低代码平台能为此做点啥吗?
|
||||
|
||||
低代码平台对整个 App 的 UI 结构和交互过程了如指掌,甚至比开发人员自己还更了解这个 App 的 UI 和交互。虽然低代码平台没有开发人员智能,但开发人员能写得出来 UI 的自动化测试用例,低代码平台应该也能生成出一些基本的来。
|
||||
|
||||
而能自动生成测试用例,也就意味着低代码平台有能力自动维护这些用例代码,不至于 UI 上有点风吹草动就导致用例跑不过。这样看来,人工编写 UI 自动化测试用例的两大痛点,都可以被解决了。
|
||||
|
||||
那么解决这个问题的思路是啥呢?其实也很简单,就是通过跟踪 UI 上的事件整理出被测 App 的功能点,加上低代码对 App 交互过程的了解,我们就可以生成模拟人工操作的代码了,从而也就可以自动生成自动化测试代码了。这部分更详细的方法,我会在动态更新部分专门介绍。
|
||||
|
||||
App 上线之后,那我们得知道 App 的使用情况、App 的运行状况,以及异常情况等信息,这都需要事先植入埋点,用于采集数据,然后我们还得要有办法拿到埋点数据,并提供分析功能。这个做法在纯代码模式下已经很成熟了,所以,低代码平台也不需要再次发明新方法,只要提供自动化程度更高的植入埋点能力和自动分析数据的功能,就可以帮助没有经验的人获取到所需的数据,用来改进 App 了。
|
||||
|
||||
生产环境管理
|
||||
|
||||
除了前面说的需求端和交付端外,低代码平台还要能在生产环境的运维上,提供一定的自动化能力。
|
||||
|
||||
在这方面,主流的低代码平台有两种差异比较大的解决方案:第一种是低代码平台将开发和运行环境合一,直接将开发好的 App“一键”推送给运行时,自动生成一个 URL 对外提供业务价值;第二种是开发环境和运行环境物理隔离,彻底解耦。
|
||||
|
||||
我们这个专栏介绍的低代码平台不严格区分这两种模式,不过 Awade 采用的是第二种,将开发和运行时隔离的方式。
|
||||
|
||||
开发和运行时隔离的方式的好处是可分可合,按需处理。低代码平台可以再独立开发一个运行时与开发时环境对接,实现第一种方式的一样的效果,也可以不提供运行环境,由业务单位自行解决运行环境的问题。
|
||||
|
||||
无论是内置运行时,还是业务单位自建运行时,低代码平台都应该提供自动部署方面的能力。如果是容器化的运行时,我们可以直接提供应用运行时镜像,在镜像中直接把蓝图、Dockerfile,以及其他配置信息都自动处理好,应用团队拿到镜像后就可以直接部署,或者由低代码平台自动推送给自带的服务器上直接部署。如果是物理机环境,我们可以打包所需的各种依赖,直接生成所需的各种脚本,做到应用解压后直接一个 run.sh 就搞定的效果。
|
||||
|
||||
无论是集成式运行时,还是独立式运行时,低代码平台都可以为 App 上线时的灰度发布提供能力,实现可视化的灰度发布策略选择,可视化一键紧急版本回退等主要功能。当然,如果灰度发布已经是一个成熟系统了,那我们就可以考虑将它集成到低代码平台之上,作为低代码平台在生产环境管理方面的能力之一。
|
||||
|
||||
而且,如果我们已经可以提供可视化灰度策略,内置多种不同的策略,根据灰度功能的特性,由业务团队选定特定的灰度发布策略,那当然也就可以支持发布策略的自定义配置了,包括:
|
||||
|
||||
|
||||
基本策略配置:包括用户规模、覆盖功能、回滚策略、新旧系统部署策略等;
|
||||
用户画像配置:包括用户特征、年龄、数量、地理、终端、常用功能、友好度、净值度等,根据手里的用户画像数据而定;
|
||||
分流规则配置:这部分比较灵活,多以手工修改配置文件(如 nginx.conf)或者运维脚本为主,容易出错,可视化集成后收益较高。
|
||||
|
||||
|
||||
最后还有一点,低代码平台内置植入埋点数据的功能,也可以和灰度发布策略功能配合使用,埋点数据可以收集到用户信息、运行时信息等,这些都可以作为灰度发布的策略支撑数据。
|
||||
|
||||
资产管理
|
||||
|
||||
最后我们再来说一下资产管理方面的问题。App 是一种资产,它可以统一托管在低代码平台中,低代码平台提供可视化的代码托管服务,这样可以降低非技术人员在使用 Git 时可能会碰到的困难。
|
||||
|
||||
Git 作为一个专业的源码版本管理工具,只提供 MML(人机命令)接口,这对许多非技术线的低代码用户来说,太过专业。如果代码合入过程出了问题,特别是有冲突时,非技术线的用户往往束手无策。虽然现在已经有许多 Git 可视化工具了,但也是要求用户理解 Git 的基本逻辑和概念,不然也不可能用得好。
|
||||
|
||||
低代码平台上的代码托管功能,不需要完整复刻 Git 的各种能力,只需要把代码托管和 App 的工程管理融合在一起即可。
|
||||
|
||||
比如,在应用开发者打开应用工程时,自动执行 git pull,在退出应用工程时,自动执行 git commit 提交所做的修改。甚至还可以依托于 gitlab 或者 gerrit 这样的工具,自动就本次修改发起代码走查流程,将当前所做修改推送给管理员进行代码走查。此时 gitlab 或者 gerrit 还可以触发 DevOps 流水线,对当前的修改进行自动构建和测试等一系列操作,然后才进入人工走查阶段。而这一系列操作,都可以由低代码平台在后台静默执行。
|
||||
|
||||
这个过程看起来很复杂,但是其实大多数动作都可以由 DevOps 流水线来执行,低代码平台需要做的,只是正确地配置好流水线任务,以及触发 DevOps 流水线。像 gitlab 或者 gerrit 这种一站式代码托管工具,本身就有非常完善的 API,几乎所有操作只需要一个后台 shell 命令就可以触发。其中,gitlab是一个开源软件,社区版是免费的,gerrit是一个商业软件。
|
||||
|
||||
App 的版本也可以作为一种资产来对待,一般的软件企业肯定已有 App 版本管理系统了,低代码平台可以打通 App 版本管理系统,从而实现一站式的 App 版本发布和更新。这方面离我们专栏太远了,而且没有统一的对接方法,这里就只提出目标,但实现上就不再深入了。
|
||||
|
||||
总结
|
||||
|
||||
低代码平台不能只关注开发能力,开发能力固然是低代码平台最重要的能力,即使它定义了低代码平台的能力上限,但是也不能忽略其他能力。这一讲我从 App 开发生命周期的角度,从 4 个维度整理出了低代码平台的其他能力,这些能力共同定义了低代码平台的能力下限。这两种能力相辅相成、均衡发展才能从整体上推动低代码平台综合能力的发展。
|
||||
|
||||
|
||||
|
||||
我们再次回顾一下这张雷达图,水平方向上是从需求到交付,以研发能力作为主线,从 D2C 到模板和业务组件,再到开发,再到自动化测试,再到运行时数据采集和分析等几个能力,是应用开发过程中非常重要的几个环节,这是低代码平台应该着重关注的几个着力点,应该优先发展水平线上的这几个能力。
|
||||
|
||||
垂直方向上主要是从管理的角度来看低代码的功能的,从生产环境管理到资产管理,一共有灰度发布、自动化部署、版本管理、代码托管这几个着力点。相对研发能力这根主线来说,管理线上的能力优先级相对较低,如果你的低代码平台主要定位是面向内部使用,你甚至可以不需要发展管理线上的能力,但如果你的低代码平台有需要部署到客户现场的话,那么管理线上的这几个能力就不能无视了,这个情况下它们也是必须的。
|
||||
|
||||
最后我们再回顾一下能力示意图,相信你会有更系统的认知:
|
||||
|
||||
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
研发能力线上,除了这讲列出的几个能力之外,在你的场景中,还有哪些能力是同等重要的?
|
||||
除了研发能力和管理能力线,你认为还有其他的维度吗?
|
||||
|
||||
|
||||
欢迎在留言区里留下你的看法。我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
262
专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md
Normal file
262
专栏/说透低代码/16|扩展与定制:如何实现插件系统并形成生态圈?.md
Normal file
@ -0,0 +1,262 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16|扩展与定制:如何实现插件系统并形成生态圈?
|
||||
我们已经在专栏中多次提到插件这个词,那么插件到底怎么来实现呢?今天我们就来系统地梳理一下。
|
||||
|
||||
在【第 9 讲】中,我们解决了低代码编辑器的属性编辑器与 Web 组件的紧耦合问题,而且在【第 12 讲】的获取组件个性化数据的方法中,我们也采用了类似的思路,实现了应用定制化的动作与低代码平台松耦合的效果。核心功能与扩展功能的松耦合架构,是实现插件的关键基础。所以,我们可以将这两讲采用的方法进行归纳和抽象,形成一个允许应用团队在更大范围内定制和扩展的能力,我将这个能力称为插件系统。这就是我们今天这讲要解决的任务。
|
||||
|
||||
特别是【第 9 讲】中,我们细化到代码层面,进行一步步地设计和解耦,最终用一套代码架构同时支撑低代码平台内部实现和外部扩展。你可以复习一下这一部分,能帮助你更好地理解今天的内容。
|
||||
|
||||
在我看来,对于一个通用型的低代码平台来说,插件系统是一个非常重要的功能,它能够解决通用型低代码平台的许多问题。我们一步步来分析,先看看通用型低代码平台都有哪些弊端。
|
||||
|
||||
通用型低代码平台的弊端有哪些?
|
||||
|
||||
我们之前已经说过很多发展通用型平台的好处了,不过凡事都有代价。如果站在业务开发(即平台的用户)的角度来看,通用带来的问题主要包括这几个方面:
|
||||
|
||||
|
||||
使用门槛居高不下;
|
||||
效率无法最大化,高不成低不就;
|
||||
平台过于“高冷”和“挑剔”;
|
||||
容易与业务需求耦合,逐渐腐化;
|
||||
平台容易积压需求,造成不满。
|
||||
|
||||
|
||||
我们简单分析一下这几个问题。首先要明确的是,效率和赋能(降低门槛)是建设低代码平台的两大目标。如果我们的低代码平台达不到这个目标,那它必然不能算作是一个成功的低代码平台。即使我们把效率和赋能中的其中一项做到极致,也很难说已经获得了成功。
|
||||
|
||||
不过,从通用到具体场景,我们还需要做很多的额外工作,而且这些定制化工作的难度和工作量可能还不低,或者是开发过程比较麻烦。总之,刚刚接触低代码平台的用户和低技能者是很难驾驭这个过程的。
|
||||
|
||||
当然,我们可以通过内置模板的方式解决配置方面的问题,但是深度定制部分的工作,往往通过简单的配置是无法解决的,所以模板也不行。那这方面要怎么处理呢?
|
||||
|
||||
深度定制这部分的工作和应用开发的效率紧密相关,实施难度很大。我们回顾下[第 8 讲]的内容,当时我们在讲解应用开发三部曲的布局篇时说到,网格布局器是一个通用的布局器,能用来开发表单类 App,也可以用来开发 Dashboard 类 App 和其他多数分析类的 App。但是,网格布局在各个场景下的效率表现有很大的不同,它在精细化 UI 的布局中的效率更高,但是在做表单布局时的效率却会比较低。
|
||||
|
||||
从这个例子中,我们可以看到:为特定场景提供定制化的能力,是提升效率的一个非常有效且直接的方法。
|
||||
|
||||
所以说,通用型的平台实现的任何需求,都需要充分考虑各种各样应用场景下的情况。在应用团队看来我们就非常高冷了,应用团队把一个小需求、小修改提给平台,往往都需要漫长的等待和评估,而且,许多在应用团队看来理所当然的需求,却会被平台团队给拒绝掉。
|
||||
|
||||
当然,只有在低代码平台团队非常强势的时候,它才能做到如此高冷和挑剔。但当低代码平台没有这样的资本的时候,事情则会朝另一个方向演进:与业务逐渐耦合,最终失去通用性。
|
||||
|
||||
如果扩展性不够,但又不够强势时,迫于压力,我们难免会使用 if else 来解决,if 只能爽一时,但一个 if 一个坑,用不了多久,要么就 if 不下去,要么就由于 if 的情况考虑不足与应用业务产生硬耦合。当第一个 if 出现的时候,低代码平台的架构腐化就开始了,当再也 if 不下去的时候,已经病入膏肓。
|
||||
|
||||
如果低代码平台的代码架构,已经有一定的扩展能力,能从架构上隔离开业务定制性的需求和通用性需求的话,就可以避免与业务功能产生强耦合了。但这样还不够,由于应用团队无法直接参与需求的定制,这会导致应用的需求都积压到低代码平台团队中,漫长的交付周期要么把平台团队搞得精疲力尽,要么无法按期交付,导致业务团队的不满。
|
||||
|
||||
我们再换个角度,站在低代码平台角度看这些通用性的弊端,主要问题是需要有一个良好的代码架构和演进策略;其次是需要尽早规划尽早实施,越往后推历史负担越大;再者是实现难度比较大,任何修改都要能满足通用性和定制性解耦。虽然这些问题也不少,但是这些都是平台内部的问题,属于内部矛盾,解决起来相对容易。今天这讲就不深入讨论这个方向上的问题了。
|
||||
|
||||
那么,为什么说插件系统能够解决这些弊端呢?
|
||||
|
||||
首先,插件非常廉价。就凭这一点,插件就可以解决通用型低代码平台的各种弊端。廉价意味着开发成本低,也意味着可以有庞大的数量。数量庞大的插件可以像细沙一样,填满低代码平台各个大功能的覆盖盲区。
|
||||
|
||||
和平台团队接到需求时的各种瞻前顾后不同,用插件实现需求,就一句话:干就完了。也因为廉价,插件的试错成本比较低,搞错了大不了推倒重来。我们的许多需求可以先以插件的形式提供、试错,验证过后,再融入低代码平台中去。
|
||||
|
||||
廉价也意味着应用团队也可以参与,这样就能挡住不少原本应该提交给平台团队的需求。同时,鞋子好不好,只有脚知道,应用需要啥功能,应用团队自己最清楚。所以,这些自己为自己量身定做的插件也可以大幅提升应用开发的效率。
|
||||
|
||||
插件系统的设计与实现
|
||||
|
||||
那么,现在我们具体来说说如何设计和实现一个插件系统。我们可以从 SDK 的提取方法、可扩展的功能建议、插件二次开发和 manifest 设计,以及插件生命周期管理这几个方面来考虑。
|
||||
|
||||
SDK 的提取方法
|
||||
|
||||
啥是 SDK 呢?SDK 的全称是 Software Development Kit,也就是软件开发套件。在这一讲里,SDK 的作用是提供一套实现插件的框架和必要的辅助功能。
|
||||
|
||||
在设计一个插件系统的时候,低代码平台要先定义好哪些需要扩展的功能,并在内部事先预留好扩展点。而 SDK 的一个主要作用,就是帮助二次开发者更方便地找到这些扩展点。而且,SDK 需要提供必要的类型定义和功能,这些能够大幅降低在扩展点上做开发的难度,从而帮助二次开发者更快、更容易地做出一个插件来。
|
||||
|
||||
通常来说,SDK 主要包括:
|
||||
|
||||
|
||||
接口和类型定义。这部分代码量可能还占不到 SDK 包所有代码的 1/10,但却是最重要的一部分。这些接口和类型就是插件的架构和框架,它们勾勒了整个插件系统的轮廓和概貌;
|
||||
基类的定义。扩展点通用部分的实现,需要我们尽可能完整地将各个功能实现出来,只留下尽可能少的抽象方法。而二次开发者主要的工作,就是补全所有的抽象方法、覆盖必要的父类方法;
|
||||
调试工具和构建 & 部署脚本。这是二次开发过程必须的专用工具,包括调试器、构建 & 部署的脚本、脚手架等。如果没有这些工具,二次开发基本就无法继续了,所以我们必须优先实现和提供;
|
||||
辅助性、功能性工具类。这些就是工具包,封装了常用的功能,目的是降低二次开发难度,提升二次开发效率。它们是辅助性的,没有它们也不会对大局产生多大影响。因此我们可以降低这部分的优先级,在资源允许之后,或者根据二次开发人员的所问所需,针对性地逐渐提供。
|
||||
|
||||
|
||||
那么,我们应该如何设计一个扩展点,以及哪些代码需要挪到 SDK 中呢?
|
||||
|
||||
这里你可以复习一下【第 9 讲】,我通过在低代码的属性编辑器上做扩展的方式,非常详细地给出了如何设计扩展点以及哪些代码需要放到 SDK 中的方案。同时,你也可以复习一下【第 12 讲】,这是另一个案例,采用类似的方法,你就可以按照自己的需要,设计出新的扩展点来了。
|
||||
|
||||
那么,有哪些功能适合开放出去给应用扩展呢?
|
||||
|
||||
可扩展的功能
|
||||
|
||||
总的来说,我们至少有数据与数据模型、自定义组件、自定义交互动作等等这些扩展点。我们展开分析看看。
|
||||
|
||||
扩展点一:数据与数据模型
|
||||
|
||||
数据存取是存量系统与低代码平台之间最主要的对接方式,这也是插件系统主要需要解决的问题。
|
||||
|
||||
我们都知道,App 的职能可以分为两种:生产数据和消费数据,低代码平台自身能处理掉一部分数据,但是绝非全部,特别是对存量系统的数据的使用,是一个刚需。
|
||||
|
||||
但是喜新厌旧是人之常情,在企业里往往也是这样的。有了新系统,老系统的流量会逐渐切换到新系统,然后进入只读状态,但老系统里的数据是有价值的,可能由于数据结构、数据量等这样那样的原因导致无法将数据移植到新系统中,往往就必须新老系统并行一段时间(而且这个时间往往会很久),此时,我们就可以给老系统做一个插件用来与低代码平台做对接,是一个很好的选择。等以后老系统彻底下线了,直接把插件拿掉就行了,不会有残留。
|
||||
|
||||
还有另一种情况,也是我现在面对的情况,低代码平台与存量系统是共存关系,老系统数量众多,且依然继续在演进,不可能被低代码平台顶替。而低代码平台也不可能不顾一切地融入到某个存量系统(这样会大大降低低代码平台的价值),于是就出现了一个低代码平台需要对接许多存量系统的局面。而且每个存量系统都有自己的一套获取数据机制和迥异的数据模型。这样的情况下,唯有针对各个存量系统打造一个专用插件用来获取数据、提取数据模型这一条路可走。
|
||||
|
||||
扩展点二:自定义组件
|
||||
|
||||
自定义组件是低代码平台最主要的扩展点之一。一般来说,低代码平台内置的组件集都是通用的、常见的,而应用单位自行封装的业务组件,虽然通用性差,但在它适用的那一亩三分地里,价值很高,所以我们需要允许业务团队开发和封装他们适用的业务组件。
|
||||
|
||||
另外,再牛的内置组件集也会有功能盲区,在特定场合下,应用团队可以利用这个扩展点来为低代码平台添加新的组件。当然了,作为低代码平台兜底策略的一部分,低代码平台应该要有一种能力能在应用团队无须封装插件的前提下,直接调用原生 API,快速使用第三方库。
|
||||
|
||||
扩展点三:自定义交互动作
|
||||
|
||||
我们前面也说过,可视化编程是可视化开发模式中最难的一个环节,因为可视化编程中,我们需要编排大量的逻辑。
|
||||
|
||||
业务组件也有类似场景,比如业务团队内部会积累一些程式化的交互动作。如果我们要用通用动作编排出这些逻辑,需要填写大量复杂、不好维护的参数。这时,我们就可以将这些逻辑封装成自定义交互动作,只暴露出若干输入框作为参数,这样一来,自定义动作卡的使用体验往往就会好许多。
|
||||
|
||||
除了封装复杂逻辑之外,自定义交互动作还可以对存量的复杂系统的用法进行场景化归纳,再根据归纳到的使用场景设计相应的参数。动作的使用者只要选定一个预设场景,正确填写所需参数后,低代码平台就可以按预设自动生成相应的代码了。
|
||||
|
||||
比如,我所在的产品线有一个 WebGIS 系统,它有上百个 API。有一个深度使用这个 GIS 系统的业务团队把它在产品线中的用法归纳了一下,整理出了栅格展示、小区展示、热力图等用法,然后将各个用法封装成一个个自定义动作,每个动作必填的参数很少,主要是在配置如何查询数据。完成了后,即使对这个 GIS 系统不熟悉的人,也能快速地在 GIS 上渲染出所需的图层和数据。
|
||||
|
||||
其他扩展点:导出、登录等
|
||||
|
||||
最后,我们这里再列举一下其他可做插件的扩展点。
|
||||
|
||||
首先,我们在导出应用数据时,如果有扩展点,就可以对导出的原始应用数据做一些转换,转为其他平台或者其他用途的数据。我给你介绍下我碰到过的两个实际场景:第一个是将应用数据一键导出成支持多种运行平台应用包,有的是物理机运行时,有的是 Docker 虚拟机运行时;另一个是将在线 Web 应用直接导出为离线报告,比如 Word、PDF 等格式。我们现在就有应用团队正在研究如何导出有交互能力的离线 Web 应用包。
|
||||
|
||||
另外,如果你的低代码平台需要被其他多个系统纳管,或者要部署到客户的系统中,那很可能需要支持多种不同场景的单点登录功能。这个时候,针对每个系统制作登录插件,按需登录,就是一个非常好的做法。
|
||||
|
||||
插件二次开发和 manifest 设计
|
||||
|
||||
除了前面说的 SDK 的提取和可扩展点的设置外,在插件系统的设计和实现上,我们还需要考虑插件二次开发和 manifest 设计的问题。
|
||||
|
||||
和多数其他系统的插件一样,我们需要有一个 manifest 文件来描述插件的信息,基本信息包括:插件的名字、插件的版本、所用 SDK 的主版本,以及插件功能描述等静态描述信息。
|
||||
|
||||
但更关键的是,Schema 必须给出这个插件实现了哪些扩展点、各个扩展点所在的路径,还有各个扩展点的个性化配置信息等。举个例子,如果我们添加了自定义组件,那可能需要为每个新增的组件配置一个图标。
|
||||
|
||||
比如下面这个插件 manifest 是我们的低代码平台 Awade 正在使用的:
|
||||
|
||||
{
|
||||
"name": "datahub",
|
||||
"module": "DataHubModule",
|
||||
"path": "web/dist/@awade/plugin",
|
||||
"serviceInfo": {
|
||||
"services": [
|
||||
{
|
||||
"label": "DataHub",
|
||||
"name": "datahub",
|
||||
"remoteData": {
|
||||
"class": "DataHubRemoteData", "import": "web/src/lib/components/datahub/remote.data-type"
|
||||
},
|
||||
"renderer": {
|
||||
"class": "DataHubRemoteDataRenderer", "import": "web/src/lib/components/datahub/remote.data-renderer"
|
||||
},
|
||||
"initData": {
|
||||
"style": {
|
||||
"dataReviserHeight": "calc(90vh - 410px)", "paramBoxHeight": "calc(90vh - 475px)", "configModalHeight": "calc(90vh - 405px)"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"actionInfo": {
|
||||
"actions": [
|
||||
{
|
||||
"category": "事件与数据",
|
||||
"type": "datahub",
|
||||
"action": {
|
||||
"class": "DatahubAction", "import": "web/src/lib/components/datahub/action-type"
|
||||
},
|
||||
"renderer": {
|
||||
"class": "DatahubActionRenderer", "import": "web/src/lib/components/datahub/action-renderer"
|
||||
},
|
||||
"initData": {
|
||||
"style": {
|
||||
"padding": "10px 10px 0 10px",
|
||||
"dataReviserBottom": "10px",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadatas": [{
|
||||
"selector": "plx-table",
|
||||
"class": "PaletxTable",
|
||||
"import": "web/src/lib/components/table/index",
|
||||
"category": "dataDisplay",
|
||||
"label": "Paletx Table",
|
||||
"desc": "Paletx Table",
|
||||
"icon": "assets/icon/plugin-paletx-pro/table.svg"
|
||||
},
|
||||
{
|
||||
"selector": "plx-badge",
|
||||
"class": "PaletxBadge",
|
||||
"import": "web/src/lib/components/status/index",
|
||||
"category": "dataDisplay",
|
||||
"label": "Paletx Badge",
|
||||
"desc": "Paletx Badge",
|
||||
"icon": "assets/icon/plugin-paletx-pro/status.svg"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
插件系统在拿到一个插件包之后,首先就要读取这个文件,通过它来获取插件的所有信息。因此,这个文件在插件包里的位置和名字必须固定,比如就放在插件包的根目录下,命名为 manifest.json 就可以了。
|
||||
|
||||
而二次开发的最主要工作,就是在安装好了 SDK 包之后,按照平台的规范,正确编写各个扩展点的代码。插件的开发工作一般不会特别难,但万事开头难,因此我建议你的平台可以根据不同的扩展点给出一些 Demo 插件,这样应用团队就可以在对应的 Demo 插件包的基础上,依葫芦画瓢完成剩余的工作。
|
||||
|
||||
注意,给出的 Demo 插件一定要是可以安装和使用的,否则它的价值就大打折扣了。
|
||||
|
||||
按照我的经验来说,编写的图文文档一般没人看,多数人还是喜欢直接照抄。在照抄的同时有不明白的地方,会直接来问,没几个人有耐心去搜索文档。所以,你可以把一些常见问题的解决方案,直接通过注释的形式,写在 Demo 插件的示例代码中。必要时,还可以在注释中放上一个超链接,导航到更详细的图文文档中去,这样的效果最好。
|
||||
|
||||
插件生命周期管理
|
||||
|
||||
最后,我们还要关注插件的生命周期管理的问题。一个插件的生命周期,大概有这些主要阶段:上传、安装、激活、使用、去激活、迭代更新、卸载。插件系统需要在各个阶段提供对应的通道和工具,支持插件更新和切换自己的状态。
|
||||
|
||||
上传比较简单。我们直接在开发平台上开放一个插件上传通道,这样开发者就可以将他的插件上传到系统中来了。
|
||||
|
||||
接下来,插件系统就需要对插件包做静态校验,读取 manifest.json 文件,并检查所有必要的配置项是否合法有效。校验通过之后,就可以把插件安装到插件系统中来了。这个过程主要是文件拷贝,在我们的实践中,主要是 JSBundle、NPM 包以及其他静态文件的拷贝,插件系统此时不会去使用或执行相关的代码。完成之后,插件后台管理器会给界面推送一个安装完成的提示,收到这个提示之后,应用就可以激活这个插件了。
|
||||
|
||||
插件激活时,插件系统就会实际使用和执行插件包里的代码了,所以这个操作对低代码平台的安全性会构成一定的风险。
|
||||
|
||||
需要说明的是,我这里并未对系统的安全性做特别的关注,因为我们现在主要面对内网用户,没有面向不确定的公众开放,因此我们没有将插件系统的安全性提到特别高的优先级,目前只做到单一插件死掉不影响系统和其他插件的最低程度,不考虑插件开发者有破坏系统的主观恶意。因为在完全实名的前提下,我们通过日志是很容易抓住破坏者的。
|
||||
|
||||
如果你的系统需要向不确定的公众开放,那么系统的安全将是一个非常重要的议题,需要重点关注。
|
||||
|
||||
具体激活插件时要执行哪些操作,取决于低代码编译器的架构,一般包含前端和后端两部分。前端的部分要把 JSBundle 从服务器下载到浏览器,然后 eval 一下就可以了,后端部分则是执行插件包的初始化代码,把 NPM 包的入口注册到插件包的功能入口上。
|
||||
|
||||
可以看到,这两部分都是有安全风险的:前端的激活容易遭受 XSS 攻击,后端在执行插件的初始化脚本时,有可能会执行到恶意代码。但我这里暂时没有防范的经验可以分享,如果你有相关的经验,欢迎在评论区分享给我们。
|
||||
|
||||
插件激活的时候,插件系统会把插件所提供的各个功能植入到低代码平台的各个环节,比如组件列表中增加对应的业务组件,动作列表中增加对应的自定义动作,获取数据的功能列表里增加对应的数据获取通道,等等。这些功能都植入好了后,我们就只需要等着应用开发人员按需使用就好了。
|
||||
|
||||
与激活和安装的流程相反,插件去激活和卸载的过程需要我们在相应的功能入口处删去植入的能力,删除插件相关的文件、包等内容。
|
||||
|
||||
最后,如果有余力,你还可以开发一个插件分发平台,用于集中管理插件,包括新增与删除插件、插件的在线自动发现、插件的版本升级,等等。如果你资源有限,又很需要这个功能,也不需要从零开始搭建,可以复用已有的功能,比如制品库和内部 NPM 镜像等。如果你们有内部网盘、论坛等,只要能托管文件,也可以加以利用,实在不行,搞个 FTP 服务器也成。
|
||||
|
||||
生态圈只是一个副产品
|
||||
|
||||
根据我们前面的分析,存量系统通过插件,可以将其数据与业务流程和低代码平台相连,应用团队通过插件,可以把业务组件、业务模板、方法和低代码平台连接。当连接的节点逐渐增多之后,你会发现原本相互隔离的人和数据之间,间接地产生了连接。
|
||||
|
||||
数据与数据之间、人与数据之间、人与人之间相互打通,形成了一个圈子。随着加入成员越来越多,每个成员可以从圈子里获得更多收益,同时也会吸引更多的成员加入,这是一个正向的增强回路。
|
||||
|
||||
|
||||
|
||||
这就是一个生态圈。一般生态这样字眼,会给人一种比较虚的感觉。但是我们这讲所谈及的插件系统是非常务实的,都是从实用以及如何解决存量系统之间的关系着手,也都是扎根于这样的目的。因为,我相信,在任何正常的企业中,低代码平台都绝不是第一个系统。在它之前,必然已经有形形色色的系统在跑在用。
|
||||
|
||||
低代码平台不能仅仅是另一个新系统,而是要成为企业的核心,通过插件连通各个存量系统,贯通数据,让存量数据创造更大的价值。从这个角度看,低代码平台实际上起到的是中台的作用。这一点我在第一讲也说过,低代码平台的演进线路有相当一部分与中台是同向,甚至是重叠的,这两者可以、也必须放在一起考虑。
|
||||
|
||||
虽然我们不着重谈生态圈这样的虚头巴脑的内容,但是,依托于插件系统,实际上低代码平台和所有存量系统之间,自然而然就形成了一个生态。低代码平台能帮助存量系统发挥更大价值,而存量则一步步将低代码平台推到企业的核心。连通的系统越多,低代码平台对企业的价值就越大、越重要,这是一种良性共生关系。
|
||||
|
||||
在和其他系统打成一片的同时,又能独善其身,不与任何系统耦合,只有插件系统才有可能达到这样的目的。
|
||||
|
||||
总结
|
||||
|
||||
今天这讲,我们从全流程设计插件系统的角度,分析了对插件系统的各个环节。其中插件系统的核心功能,也就是 SDK 的设计和功能,我们在【第 9 讲】和【第 12 讲】里已经做了非常详细的阐述,而插件系统的其他功能,都是围绕着 SDK 的架构设计和功能来打造的。
|
||||
|
||||
为了方便你更好地理解这些内容,我这里贴了一张 Awade 的插件系统架构关系图。我们一边分析,一边回顾这些知识点。
|
||||
|
||||
|
||||
|
||||
你可以看到,在编译器内核之上,是一层协议层。它是编译器对外的抽象,所有的扩展点都是由协议层来定义和约束的。SDK 则是建立在协议层之上的,它提供了编译器协议的默认实现,以及所有扩展点的基类的定义。这些基类不仅能在减轻二次开发的难度的同时,更重要的是也约束了插件必须遵守的编译协议,插件的二次开发只能按照协议所画出的套路来实现。
|
||||
|
||||
SDK 之上,就是各个插件了。从架构图上可以看到,内置功能和插件一样,也必须要遵守编译器协议。因此,从架构角度来说,内置功能与插件是平起平坐的,这样才能确保插件具有充分的扩展能力。当然,这只是从架构角度的设计,实际上插件能有多大能耐,还是取决于你的 SDK。这样就可以在实现层面上,保持内置功能的优势。
|
||||
|
||||
结合【第 9 讲】和【第 12 讲】的内容以及上面的架构关系,相信你已经可以掌握 SDK 的架构和设计的方法了。在掌握了 SDK 的架构方法之后,紧接着就需要考虑有哪些功能可以作为插件进行扩展。我认为插件系统可以在数据与模型、自定义组件、自定义动作等部位发挥显著的作用。其中,最重要的是数据与模型,通用型低代码平台要处理好存量系统的数据与模型的关系,插件是必须具备的能力。
|
||||
|
||||
思考题
|
||||
|
||||
我们假设,在你日常工作所要接触的各个系统和团队中,有这样一个插件系统可以打通所有的存量业务的数据。在这个前提下,互通的数据能创造出多少新的业务价值出来?
|
||||
|
||||
欢迎在评论区分享你的想法。我们动态更新部分再见。
|
||||
|
||||
|
||||
|
||||
|
130
专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md
Normal file
130
专栏/说透低代码/17|兼容性问题:如何有效发现兼容性问题?.md
Normal file
@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17|兼容性问题:如何有效发现兼容性问题?
|
||||
久别重逢,我想先问你一个问题:实现一个基础平台,技术上最难的事情是啥?
|
||||
|
||||
面对这样的问题,相信你可以毫不犹豫列出许多难题,而且可能理由都非常充分。我给出的答案可能不太一样,我认为兼容性才是最难的。
|
||||
|
||||
兼容性问题,与其他问题相比,多了一个时间维度。也就是说,难度再大的技术难题始终都是一时的,解决了就是解决了,时间维度可以无视,但兼容性问题却不是这样的。它难就难在它是一个随着时间递增的包袱,滚雪球似的,终有一天这个负担会越来越大,直至压垮一个团队(无论技术多强,无论人力多少)。
|
||||
|
||||
低代码平台当然也有兼容性负担,不但有,而且非常重。设想一下,平台团队合入了一个 change,导致昨天还好好的 App 今天就跑不动了,这种事情搁谁那都说不过去。从我推广低代码平台的经验来看,兼容性是劝退应用团队的一个重要因素之一,甚至与适用性问题并列。毫不夸张地说,能否妥善解决兼容性问题,决定了低代码平台是否能走得长远。
|
||||
|
||||
要解决(或者规避)兼容性问题,可以分为两个主要步骤:发现和治理。今天这一讲我们先来聊聊如何发现兼容性问题,主要和你介绍下我正在用的三个方法,下一讲我们再从技术上聊聊如何有效治理兼容性问题。
|
||||
|
||||
代码走查
|
||||
|
||||
代码走查的作用有很多,再怎么强调都不为过,兼容性问题的发现,是代码走查的作用之一。
|
||||
|
||||
而代码走查机制的核心要素(没有之一)就是评审员,一名合规的评审员应该具备这样的素质:
|
||||
|
||||
|
||||
深刻掌握被评审的子系统所用的语言、工具、第三方库等的各方面性质,对它们的长短处了如指掌;
|
||||
深刻理解被评审子系统的全貌,大到架构、关键流程,细到各个功能的实现,最好是原创作者,次好是长期维护者;
|
||||
最好是专职的,而不是临时抓来的人头,如果是临时参与评审,Leader 需要预留充分的时间,并计入业绩。
|
||||
|
||||
|
||||
代码走查的另一个要素是要避免自我走查。不能让代码作者既当运动员又当裁判员,这很好理解,但并非所有执行代码走查的团队都能做到。
|
||||
|
||||
这一点其实是在说,要有一套管理机制(甚至是文化)来促使变更的代码能被切实有效地阅读和思考。这点展开会是一个很大的话题,而且还是一个管理层面的话题,不是我们专栏的关注点,有机会我们再在别的场合聊聊。
|
||||
|
||||
那为啥开发人员发现不了自己的代码带来的破坏性变更,反而是需要别人来发现呢?有一句古诗你肯定听过:横看成岭侧成峰,远近高低各不同。
|
||||
|
||||
资历再深的研发人员,在修改低代码平台代码的时候,总会陷入细节,往往会专注自己眼下的问题,而对所做修改可能会波及到的部分考虑不足,所谓一叶障目,不见森林。而代码变更所带来的影响,往往会在某个意想不到的角落产生破坏性变更(即不兼容修改)。这是人之常情,我们只能承认这是一个无法避免的事实。
|
||||
|
||||
如果说资深研发人员未考虑到他修改的波及是一种不该发生的无意失误,对于资历较浅的研发人员所做的代码修改的波及,则不能用失误来解释,而是限于对系统的肤浅理解,他们根本就无法预知他们所做的修改可能会产生哪些不良影响,这些不良影响中的一部分,会体现为系统的破坏性变更。
|
||||
|
||||
但专业评审员在走查代码时,不仅会关注所修改的代码本身,还会自然而然地把这份修改放到系统全貌的上下文来看,于是,许多代码作者看不到的波及,会很自然地被发现。
|
||||
|
||||
DevOps 流水线
|
||||
|
||||
代码走查是一种兼容性问题的主动发现手段,这个方法对评审员的经验和能力有强依赖。我们都知道,在软件研发领域,最不可靠的因素就是人的因素了,因此代码走查的方式是不可靠的,仅凭代码走查手段也是难以发现所有兼容性问题的。
|
||||
|
||||
DevOps 流水线是提升软件研发效能的一个非常重要,甚至是必要的手段。它主要采用形式多样的自动化检查脚本,来发现软件研发过程中包括兼容性问题在内的各种问题,从而提升软件研发效能和质量。相对代码走查来说,DevOps 是一种被动式的兼容性问题发现手段。
|
||||
|
||||
我们可以在 DevOps 流水线上加入各种专门用于发现兼容性问题的脚本,在代码走查之前就把大多数低级的、已知的兼容性问题找出来。评审员则利用其知识和对系统的深刻理解,发现深层次的兼容性问题。
|
||||
|
||||
值得提示的是,在评审员发现了某个兼容问题之后,我们可以进一步思考是否可以将这个问题的发现过程脚本化。一旦脚本化,我们基本就可以杜绝相同问题再发生了,可以点滴提升流水线的可靠性。毕竟,同样的问题,评审员下次有可能看走眼,但脚本则不会。
|
||||
|
||||
那么,具体有哪些切实可行的兼容问题检测方法可以植入到 DevOps 流水线里呢?
|
||||
|
||||
首先是 UT/FT。
|
||||
|
||||
理论上,如果每个函数每个功能特性的输入输出在代码修改前后都能保持一致,我们就可以认为系统是完全兼容的。但这只是理论,在实操过程中,代码行、函数(类),条件分支的覆盖率不可能做到 100%,但我们起码可以确保有 UT/FT 用例覆盖的那部分是能向后兼容的。
|
||||
|
||||
UT/FT 成本其实挺高的,为控制成本,我们采用的是这样的做法:
|
||||
|
||||
|
||||
新模块新功能在初创时,评审员会要求各个功能点都要有一定比例的测试用例代码;
|
||||
功能特性在后续迭代过程中,如果在某个函数上踩到坑了,我们就会针对这个函数补充对应的用例代码,避免再次踩坑。
|
||||
|
||||
|
||||
这是一个比较经济的方法,在不需要很高的覆盖率前提下,让用例代码最大发挥价值,你可以借鉴一下。
|
||||
|
||||
其次是各种代码扫描工具。
|
||||
|
||||
代码扫描工具可以很好地弥补 UT/FT 用例覆盖率不足留下的空白地带。虽然这些代码漏洞扫描工具一般都集成了数量不少的扫描规则,被这些规则命中的代码不一定都有问题,但出问题的概率会大幅增加,所以引入并重视这些漏洞扫描工具的报告,尽可能地将报告里的所有潜在问题项都改掉,也可以避免一定数量的兼容性问题。
|
||||
|
||||
需要预警一下的是,这些漏洞扫描工具往往是收费的,而且扫描工具的安装比较费事(虽然厂家会提供技术支持),扫描过程耗时耗 CPU。另外你第一次看扫描报告的时候,要先有心理准备,数以千计的乃至上万条漏洞报告很容易让你对自己的编码技术产生怀疑。
|
||||
|
||||
最后是静态检查。
|
||||
|
||||
看到这个点,也许你会马上想到各种 lint 工具,但我们团队并没有用。lint 工具确实可以减少一定的工作,但凡事都有代价,我认为使用现成的 lint 工具会被它的能力给框住,再考虑到全员的学习成本,实际是得不偿失的。
|
||||
|
||||
根据我们的经验,兼容性问题有一个特点,就是你永远无法预知它会在哪儿、以哪种方式出现。在代码走查的部分我说了,我们习惯了将兼容性问题的排查过程脚本化,兼容性问题的特点也就导致了检测脚本必须有多种不同的形式。
|
||||
|
||||
我们的静态检查脚本的形式可谓是不拘一格,有配置文件一致性检查,也有用正则暴力对源码进行检查的,正则搞不定时,还用 AST 解析深度检查的。能用 shell 脚本完成检查,就不会用语言,shell 搞不定或太慢的,再用其他方法(一般是 node.js)编写脚本来检查。一般的 lint 工具做不到这么灵活。
|
||||
|
||||
尽量将已知的兼容性问题脚本化是很重要的,这样可以避免在同一个坑里掉两次。这一点不仅是为了减少兼容性问题的发生,更主要的是协作方面的意义。一个问题第一次出现时,应用团队往往会抱以理解的态度,但如果同一个问题一而再、再而三地出现,对方就会很不耐烦了。
|
||||
|
||||
利用实际用户数据
|
||||
|
||||
先说明一下,这里的用户数据就是实际的 App 工程配置数据。在 UT/FT,或者 ST 时,构造被测数据往往是一个非常麻烦且令人头疼的过程,而且构造出来的被测数据往往不真实或者太简单,测不出问题来。那我们为啥不直接用户数据来测试呢?而且,对于低代码平台来说,用户数据天然就受到平台的管理,从库里复制一点数据来测试,简直不要太容易。
|
||||
|
||||
复制数据容易,但要找到质量较高的被测数据,则需要花一番功夫。
|
||||
|
||||
在我们的系统里,有许多 App 工程数据是开发人员的实验性工程(我估计这是一个普遍现象),这些工程是没有验证的价值的,需要排除掉。我采用了两个条件来排除这些工程。
|
||||
|
||||
第一个是根据基础指标来筛选,我采用了工程的编辑次数,模块数量等两个维度作为基础指标。根据经验,我设定了编辑次数大于 300 次,且模块数量至少为 2 的条件,作为筛选指标。你可以根据你的实际情况适当调整数值,或者按需增减其他维度。
|
||||
|
||||
不过,根据基础指标筛选到的 App 工程数据,仍然可能包含有问题的数据,此时我们可以引入第二个条件,来对它们进行更加细致的检验。我增加了低代码编译器的检验,只有能通过低代码编译器的编译的 App 工程数据才能进入下一个环节,用来校验新的修改是否有破坏性变更。
|
||||
|
||||
对于那些对没有更改生成的 App 代码的修改,我们可以直接使用筛选后的 App 工程数据进行校验。方法很简单,就是用修改前的编译器编译生成的代码与修改后的编译器生成的代码做比较,如果有修改,则表示可能会有问题,应该由专家评估是否是有破坏性变更。
|
||||
|
||||
对于那些会修改 App 代码的变更,则会麻烦许多。虽然也可以采用比较源码的方式来比对,但会比较麻烦,甚至需要将生成的代码解析为 AST(抽象语法树)之后再一一遍历比较,测试用例的开发难度较大、可维护性不好,所以我们团队没有采用。
|
||||
|
||||
我们使用的是 UI 自动化测试的方法,也就是把 App 直接跑起来,然后自动操作 UI 到某个状态,最后截图比较修改前后的差异。比较截图时,不是严格按照 100% 比较的,而是设定了一个 90% 相似的阈值,低于这个阈值就认为 UI 不匹配,从而报错。一般来说,一旦出现兼容性问题,UI 的差异会非常大,比如取不到数据,或者状态压根就打不开等。
|
||||
|
||||
这里需要补充一点儿背景,不然你很可能会觉得,采用 UI 自动化测试的方式难度更大,甚至压根就不可行。
|
||||
|
||||
这个背景就是我们的低代码平台 Awade 提供了自动生成 App 的 UI 自动化测试代码的能力(专栏的后续部分我会给出具体如何实现),只需开发人员配置待测功能点,以及预期数据即可。根据这些信息以及 App 工程数据的其他信息,就可以生成出 UI 自动化测试用例代码了。
|
||||
|
||||
在做兼容性测试的时候,我会先找有做过功能点自动化测试的 App,然后将这些 App 的自动化测试用例跑起来,然后在关键节点抓图比对。即使 App 所配置的 UI 自动化测试是错误的也没关系,因为只要待测修改的代码也能触发相同的错误就行啦。
|
||||
|
||||
虽然你现在不一定有条件利用 UI 自动化测试的方法来做兼容性测试,但至少可以对编译器做测试。即使是只做到对编译器的测试,只要你利用实际用户数据来测试,就能发现许多兼容性问题了,并且这些问题都是其他方式发现不了的。人工走查的方式不可能做到如此细致,也无法替代基于已有 App 工程数据测试所发现的兼容性问题。
|
||||
|
||||
小结
|
||||
|
||||
这一讲我介绍了多种我正在用的发现兼容性问题的方法,这些方法多数可以被直接借鉴。我们之所以要无所不用其极地从各个角度来发现兼容问题,是因为兼容性问题是一种比任何功能性问题(如功能缺失、bug 等)都更加麻烦的问题。
|
||||
|
||||
一方面,兼容性问题是一个低代码平台所有问题中最为劝退的一种问题。兼容性问题会给应用团队带来额外的对齐工作,而且这些工作往往是应用团队计划外的、被动式的工作。即使应用团队没有做任何修改(哪怕他们的 App 已经冻结版本),但是为了能够正常运行,他们不得不花额外的资源来对齐,这样的次数如果很多,他们必将萌生退意。
|
||||
|
||||
另一方面,没有被及时发现和处理的兼容性问题,会让平台团队陷入一种奇特的“怎么改都是错的”的境地,即使是那些非致命的兼容性问题,都会引起这样的后果。比如我曾经处理过一个 UI 安全边距的问题,就让我陷入了这样的境地。
|
||||
|
||||
某天我收到一个 bug:一个 App 的安全边距消失了。我们很快就查到是大概两个月前的一个修改导致了平台生成的代码没有自动加上安全边距,这就是一个典型的非致命兼容性问题。你有没有发现,此时此刻,无论我改掉这个 bug 还是不改,都会“得罪”一些人:恢复安全边距的话,这两个月里新增的 App 都将出现安全边距过大的问题;不恢复安全边距的话,两个月前创建的 App 都将没有安全边距。
|
||||
|
||||
之所以需要从不同角度切入来发现兼容问题,就和常见的一次性医用口罩的生产是一个道理。普通的纺织布是不能用于生产一次性医用口罩的,因为纺织布是由经线纬线规规矩矩交织而成的,两根线之间会更容易留下缝隙,病毒会很容易从这个缝隙进入口腔。而熔喷布则是把塑料融化后乱七八糟喷在基材上,只要满足一定的厚度要求,就不会留下缝隙了。同理,我们必须从各种差异很大的角度切入,采用差异很大的方法,尽可能多地发现兼容性问题。
|
||||
|
||||
但兼容性问题的发现是解决的第一步,下一讲我会详细介绍如何妥善处理兼容性问题。
|
||||
|
||||
思考题
|
||||
|
||||
相信你应该会有简单或复杂的 DevOps 流水线辅助你的日常开发,你现在正在跑的流水线任务里,有哪些任务是可以协助你发现兼容问题的?有哪些是稍加改造就可以用于发现兼容问题的呢?
|
||||
|
||||
欢迎在评论区留下你的看法。下一讲你不会等待那么久,我会尽快更新。
|
||||
|
||||
|
||||
|
||||
|
278
专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md
Normal file
278
专栏/说透低代码/18|兼容性问题:如何有效解决兼容性问题?.md
Normal file
@ -0,0 +1,278 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
18|兼容性问题:如何有效解决兼容性问题?
|
||||
上一讲我们提到,在软件的演进过程中,特别是在低代码平台这样的庞大软件工程中,兼容性问题,也就是破坏性变更是无法避免的。只要软件的代码有改动,就有可能引入破坏性,因此破坏性变更与软件的演进之间如影随形、不可分割。
|
||||
|
||||
破坏性变更所造成的后果,有可能微乎其微,也有可能是致命的。会造成严重后果的破坏性变更往往会受到重点“照顾”从而被妥善解决,那些没那么大破坏力的破坏性变更,一旦泄露到线上,不仅会造成体验问题,还有可能造成小范围功能不可用。
|
||||
|
||||
这一讲我们就承接上一讲的内容,重点讲讲如何妥善处理兼容性问题。当然,发现破坏性变更是解决它的第一步,具体我已经在上一讲中详细介绍了,所以我建议你在继续这一讲的学习之前,先回顾一下上一讲的内容。在讲具体如何解决之前,我们先来分析下低代码平台中一般会有哪些类型的破坏性变更。
|
||||
|
||||
有哪些类型的破坏性变更?
|
||||
|
||||
Schema 数据结构的变更是低代码平台演进过程中最常见的破坏性变更。
|
||||
|
||||
我们都知道,低代码平台往往会采用结构化的数据来保存开发者在平台上所作的配置,这些配置数据就是 Schema 数据。Schema 的数据结构往往是随着低代码平台的功能迭代同步发生变化的,典型如某个字段一开始只要一个简单值类型就够用,后来扩展为一个包含多个属性的对象以适应日益复杂的功能;又如从一个单值扩展为一个数组,或者反之,从一个数组简化为一个单值;再如多个复杂字段的各个属性的拆分与再组合。
|
||||
|
||||
总之,Schema 数据结构的变更中,基本上除了新增属性外的任何修改都大概率会引入破坏性。根据我长期迭代的经验来说,Schema 数据结构的变更大约贡献了 50% 破坏性问题。并且,这个比例在低代码平台功能建设初期会更高,随着低代码平台的成熟度的增加而逐渐下降。
|
||||
|
||||
除此之外,我们再来看看常见的模版文件更新。作为一个开发平台,在生成一些程式化内容的时候,使用模板文件进行替换是一个常用手段,比如下面是一个简单的 index.html 文件的模板:
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${title}</title>
|
||||
<script type="text/javascript" src="${scriptHref}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${cssHref}">
|
||||
</head>
|
||||
<body>
|
||||
${someContent}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
可以看到,模板里有一些 {scriptHref}等格式的文本,这些就是占位符。这个模板通常是这样使用的:处理程序或将它读入到内存中,并使用直接替换的方式,将所有的占位符替换为实际值,然后将替换后的内容写入到指定位置去,这个过程简称为模板的实例化。注意,实例化过程,模板原件不会被修改。
|
||||
|
||||
但是随着低代码平台的演进,模板的内容难免要更新,此时问题就来了:在模板更新前就已经实例化所得的文件内容是不会更新的。也就是说,已被实例化出来的文件不能用了,需要强制对齐。所以,模板文件的更新,也会引入破坏性。根据我的经验,这个类型的破坏性大概占 20%。
|
||||
|
||||
不过这些都是平台内部的破坏性,那平台外部导致的破坏性会有哪些呢?
|
||||
|
||||
显然,现代的软件底层都是由一堆第三方软件堆起来的,低代码平台也是如此。任何第三方库的升级,特别是跨大版本升级,都有可能引入破坏性,比如从 Vue2 升级到 Vue3,或者从 Angular8.x 升级到 Angular10.x 等。低代码平台所用的组件集如果有破坏性变更,基本上都会直接传递给应用工程。
|
||||
|
||||
与前端第三方库升级会带来破坏性一样,服务端 API 的演进也会带来破坏性。而且由于这些 API 往往是兄弟单位内部自研,相比开源库来说,自研功能在兼容性方面的考虑和投入会少很多,甚至完全不考虑兼容性,在上线前能够想起来发一个强制对齐说明邮件就算做得很到位了。
|
||||
|
||||
这些外部环境或依赖所引入的破坏性变更大概占了 20%。总体来说,低代码平台内部演进所产生破坏性变更占了绝大多数。
|
||||
|
||||
if else 方案
|
||||
|
||||
在上一讲以及这一讲的前面部分,我们完成了解决破坏性的第一步:了解和发现它们,接下来要做的就是解决它们了。低代码平台迭代的过程,任何修改都有可能引入破坏性,一个最容易想到的办法是大量使用 if else。
|
||||
|
||||
我们先来看下图,这是一个没有任何破坏性的低代码平台的迭代过程,相当丝滑:
|
||||
|
||||
|
||||
|
||||
当发生了第一个破坏性(记为 A)的时候,这根直线就会产生一个分叉:
|
||||
|
||||
|
||||
|
||||
那么,这个分叉点当前输入的数据是来自破坏性发生之前,还是在破坏性发生之后呢?显然不同情况下对数据处理方法是不同的(哪怕是细微的)。识别起来很简单,伪代码大概是这样的:
|
||||
|
||||
if (before(A)) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
这里的 before(A) 是用来判断数据是否来自破坏性变更之前的,需要根据破坏性 A 的特征来编写 before(A) 的逻辑。一般来说,多数破坏性都有显著的特征,因此它的实现难度不大。为了叙述方便,橙色线条表示破坏性发生之后,而蓝色线条表示破坏性发生之前,以下都采用这个规则。
|
||||
|
||||
接下来继续迭代,我们又引入了一个新的破坏性变更,记为 B,此时又会出现一个分叉:
|
||||
|
||||
|
||||
|
||||
这时情况变得复杂了一些,但看起来,再加一个 if else 还是可以搞得定。接下来再发生新的破坏性 C,也一样处理:
|
||||
|
||||
|
||||
|
||||
到这里,你可能已经发现一些规律了,似乎只要判断一个数据是来自哪些破坏性之后,就可以彻底解决破坏性问题了。
|
||||
|
||||
真的是这样吗?思考一下,破坏性变更 B 在处理破坏性 A 之前产生的数据(图中蓝色分支)时,它的判断条件和在破坏性 A 之后产生的数据(图中橙色分支)的判断条件是一样的吗?答案是:在蓝色和橙色分支下的判断逻辑很有可能是不同的!
|
||||
|
||||
|
||||
|
||||
甚至,破坏性 B 在蓝色分支下是否还存在都是一个疑问。同理,在橙色分支下没有破坏性的修改,在蓝色分支下是否依然没有破坏性,也是一个疑问。
|
||||
|
||||
基于这两个疑问,我们可以这样认为,上图中的B和 B’实际上不是同一个破坏性,而是两个相互独立,互不相干的破坏性,因此,在引入的时间上,破坏性B和 B’也没有关联(有可能 B 早于 B’发生,反之亦然)。所以上面这个图需要做一些修改才能更贴近实际情况:
|
||||
|
||||
|
||||
|
||||
看到这里,你是否觉得这个问题一下子就变得非常难了。第一个破坏性出现时,用一个 if else 就可以搞定了(2 个情况),当第二个出现时,判断逻辑就有了 2 个条件(4 个情况),第三个破坏性出现时,判断逻辑就有了 3 个条件(8 个情况)。这个负担是随着破坏性个数为幂爆炸式增长的,大量使用 if else 是无法解决问题的。
|
||||
|
||||
先别气馁,根据前文的分析,虽然图中的蓝色和橙色分支发生破坏性的时机和判断方式没有必然的关联,但这一点反而带来了好消息,我们不难得到这样的推论:蓝色和橙色这两种情况的处理方法却是等效的。也就是说,假设有一个方法可以妥善处理橙色分支的破坏性,那么这个方法也可以直接应用到蓝色分支上,从而解决蓝色分支上的破坏性问题。
|
||||
|
||||
这就是解决问题的突破口,但此时我们还无法继续展开。在介绍新方案之前,我们还有一个重要的事情要做,那就是标记破坏性变更。
|
||||
|
||||
之所以我们要先对破坏性变更做标记,是因为我们在拿到任意一份用户数据时,需要通过标记快速找到这份数据里已经包含了哪些破坏性变更,这样才能想办法将它们逐个妥善解决。反之,如果不先做标记,在拿到一份用户数据时,我们就必须编写大量的 if else 逻辑来检测这份数据包含了哪些破坏性变更,根据前面的分析过程,这是不可行的。
|
||||
|
||||
标记破坏性变更
|
||||
|
||||
破坏性的引入显然与时间有关。所以,我们在这个图上引入一个时间维度,会有助于解决问题,我们再把各个破坏性投影到时间轴上,得到这样一个图:
|
||||
|
||||
|
||||
|
||||
看起来,我们可以使用时间戳来标记破坏性。这里的关键是,在拿到任何一份应用数据之后,如果能知道这份应用数据是啥时候生成的(如图中的灰色箭头),再与这个时间戳做一下比对,我们很容易就可以知道这份应用数据已经包含了哪些破坏性变更,未包含哪些破坏性变更了。
|
||||
|
||||
但是,时间戳并不是完美的解决方案,一份代码只要没有修改,那么、在任何时候,基于这些代码所构建的版本做出的破坏性的标记,也必然是没有变化的,但是在不同时刻进行构建,时间戳却是不一样的,这就产生了矛盾。
|
||||
|
||||
那么,有没有一种具有时间戳的优点,又能避开它缺点的方法呢?
|
||||
|
||||
我们会很自然地想到低代码平台的版本号。首先版本号随时间递增的性质与时间戳相似,其次,如果代码不做任何修改,版本号也就不会改变,这个特性就可以绕过使用时间戳的短板了。
|
||||
|
||||
但其实版本号也不是解决这个问题的完美解决方案,使用版本号来标记破坏性变更,会有一个小副作用:一旦某个修改带有破坏性,就必须基于该修改发布一个新版本。但是软件的版本号一般是有规划的,随意发布版本有时候并不可行。
|
||||
|
||||
不过,要解决这个问题也不难,我们可以定义一个内部版本号,与对外发布的版本号分开就可以了。这个内部的版本号可以使用一个递增的数字就够了,每构建一次就 +1,无需设计得太复杂。有许多软件的研发过程,天然就会有内部构建号,此时我们可以直接使用内部构建号来标记破坏性变更。比如我正在用的 Windows 的内部版本号是:
|
||||
|
||||
|
||||
|
||||
再如我正在用的 Sublime,也有类似机制,左侧的是对外的版本号,右侧是它的内部构建号:
|
||||
|
||||
|
||||
|
||||
不过,在我们的低代码平台 Awade 里,我们并没有采用内部版本号,而是直接使用正式版本号来标记破坏性变更。因为对一个在线软件来说,它的版本号实际上并不重要,开发者任何时候刷新浏览器都有可能拿到新版本,所以大家并不关注版本号是多少。
|
||||
|
||||
如何解决破坏性?
|
||||
|
||||
好了,有了前面的铺垫,我们现在终于可以给出解决方案了,核心思想就是化整为零。这时我们再看下前面那张图:
|
||||
|
||||
|
||||
|
||||
这张图看上去会这么复杂,就是因为当第一个破坏性变更出现的时候,我们对它置之不理导致的。如果每出现一个破坏性,我们都立即进行版本的一致性处理,那么这张图上的情况就不会出现了。
|
||||
|
||||
具体做法是这样的:每当出现一个破坏性变更的时候,我们都配套给它编写一个处理器,这个处理器的作用是消除掉这一个破坏性。这里关键是破坏性变更对应的处理器的实现,处理器的输入是变更前的用户数据,处理器的输出是将用户数据调整为符合破坏性变更后的数据格式。这样说比较抽象,我们通过下面这张图来进一步解释。
|
||||
|
||||
|
||||
|
||||
和前面其他图一样,圆圈 A 代表一个破坏性。不难理解:只有在当用户数据是版本 V1 时,破坏性 A 才存在,当用户数据是 V2 时,破坏性 A 对这份数据来说是不存在的,这一点非常重要。而前面我们说过,我们会给破坏性 A 配套编写一个处理器,这个处理器的作用,就是需要将格式为 V1 的用户数据,升级为 V2 格式的用户数据。所以,对于破坏性 A 的处理器,它的输入是 V1 格式的数据,输出是 V2 格式的数据。
|
||||
|
||||
当有新的破坏性出现的时候,我们重复这个过程,就可以实现从 V2 升级到 V3,V3 升级到 V4 了:
|
||||
|
||||
|
||||
|
||||
每一个处理器都只要关注处理一个破坏性,而无需关注其他破坏性,一个破坏性的内容是确定的,因此处理逻辑也是确定的,因此很容易将一个处理器做得非常健壮。
|
||||
|
||||
不过,不同的用户数据的有可能是在不同时间使用不同版本号的低代码编辑器来创建的,这意味着不同的用户数据可能会有不同的版本号。比如下图的灰色箭头,代表着某一笔用户数据创建的时刻:
|
||||
|
||||
|
||||
|
||||
显然,在箭头左边的破坏性对他来说是没有影响的,但右侧的 B/C 破坏性是有影响的。所以,对于这笔用户数据,我们需要按照顺序,依次执行破坏性 B 的处理器,再执行破坏性 C 的处理器,这样,这笔用户数据就可以被稳妥地升级到最新版本了。
|
||||
|
||||
通过这个方法,我们就可以做到将任意老的用户数据,稳妥、可靠地升级到最新版了,这就是化整为零的含义。所以,一个多年前做了最后一次编辑的应用工程,即使在这么长时间内低代码平台不断迭代,引入任意数量的破坏性变更,但这个应用工程也可以随时被打开再次编辑。
|
||||
|
||||
有了通用的解决思路后,我们再针对前面说过的三类最常见的破坏性变更,具体分析下各个类型的破坏性变更如何处理。
|
||||
|
||||
Schema 数据结构的变更
|
||||
|
||||
我们先来看 Schema 数据结构的变更造成的破坏性,案例如下:
|
||||
|
||||
|
||||
|
||||
这是典型的迭代过程产生的破坏性的例子。一开始某个属性是一个简单值,后来扩展为一个复杂的对象。如果不采用处理器来做一致性处理,那么所有使用这个属性的代码都必须要永久做 if else:
|
||||
|
||||
if (typeof v.property == 'string') {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
但如果采用这一讲的方法,我们只需要在处理器里直接把 property 属性的结构从字符串扩展为一个对象即可,处理的过程需要给 type 只设置一个当前的默认值就好了。
|
||||
|
||||
我们再来看另一个例子:
|
||||
|
||||
|
||||
|
||||
这里,某个功能在迭代的过程被简化或者裁剪,把 type2 和 type3 都裁剪掉了,只留 type1,且 property 的值的结构也随着被简化。在这个情况下,处理器就可以直接把结构简化,这样 V2 及以后的代码就可以“忘记”property 属性曾经是一个数组了。
|
||||
|
||||
Schema 数据结构的变更可以举的例子非常多,但基本大同小异,要么就是结构变复杂了,要么就是结构变简单了。实际上,任意的结构调整,都是可以采用处理器来完成一致性处理,从而使得使用对应数据的代码无需 if else,从而大大简化代码。
|
||||
|
||||
模板文件的变更
|
||||
|
||||
随着低代码平台的演进,模板文件内容发生变化是很正常的,比如前文的 index.html 的例子,有可能会增加新的脚本引入语句:
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${title}</title>
|
||||
<script type="text/javascript" src="${scriptHref}"></script>
|
||||
<!-- 增加了这一行 -->
|
||||
<script type="text/javascript" src="${newHref}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${cssHref}">
|
||||
</head>
|
||||
<body>
|
||||
${someContent}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
处理器消除这样的破坏性变更就更简单了。对这类问题,基本都是只要把新的模板拷贝到对应位置去即可。当然,这类破坏性变更往往会伴随着前面介绍的“Schema 数据结构的变更”,此时,一个处理器里,就需要同时处理多种不同类型的破坏性类型了。
|
||||
|
||||
外部破坏性变更
|
||||
|
||||
在 Pro Code 纯代码开发模式下,外部破坏性问题只能手工解决,但在低代码平台,多数情况下,我们也可以做到悄无声息地把外部破坏性变更给解决掉。
|
||||
|
||||
比如低代码平台所依赖的按钮组件的 API 出现了破坏性变更,按钮的文本从原来的 text 改为了 label:
|
||||
|
||||
<!-- 原来的用法 -->
|
||||
<jigsaw-button text="这是一个按钮"></jigsaw-button>
|
||||
<!-- 现在的用法 -->
|
||||
<jigsaw-button label="这是一个按钮"></jigsaw-button>
|
||||
|
||||
|
||||
在收到强制变更说明之后,我们只要为此开发一个处理器,找到 App 里所有带有 text 属性的按钮,并自动将属性名从 text 改为 label 即可。
|
||||
|
||||
注意,虽然这种破坏性是在组件,但实际应该修改的是 App 的 Schema 数据。这其实非常好理解,对低代码平台来说,App 的 Schema 数据就是它的源码,低代码平台在消除外部破坏性所做的动作与纯代码所要做的动作相似,都是需要修改各自模式下的源码。
|
||||
|
||||
这个例子比较简单,但无论再怎么复杂的第三方库的破坏性的处理,都是类似的思路和做法。比如,你需要写一个升级器将 App 使用的 Vue 从 2.x 升级到 3.x,这是一个复杂的过程,但思路和这个例子一致:先实验或者阅读破坏性变更说明书了解到需要手工做哪些调整,然后找到 App 的 Schema 数据里的对应位置,做相应调整。
|
||||
|
||||
我用这个方法自动将 Angular(这是我在用的技术栈)从 4.x 升级到 8.x,后来又从 8.x 升级到现在的 9.x 版。Angular 的大版本升级更加麻烦,Webpack、Typescript、RXJS 等各种第三方库也要一起调整,但这个方法在两次升级过程中都表现得很好。
|
||||
|
||||
服务端 API 的破坏性变更是家常便饭,特别是第三方数据源的破坏性变更,非常不可控。这类破坏性变更的处理方式与前端第三方库的解决方式如出一辙。比如一个 Rest 服务返回的数据结构,以前的结构是一个二维数组:
|
||||
|
||||
[
|
||||
[11, 12, 13, 14],
|
||||
[21, 22, 23, 24],
|
||||
[31, 32, 33, 34],
|
||||
[41, 42, 43, 44]
|
||||
]
|
||||
|
||||
|
||||
某天突然发生变化了,变成了:
|
||||
|
||||
{
|
||||
header: ['Header1', 'Header2', 'Header3', 'Header4'],
|
||||
data: [
|
||||
[11, 12, 13, 14],
|
||||
[21, 22, 23, 24],
|
||||
[31, 32, 33, 34],
|
||||
[41, 42, 43, 44]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
你看,这里的结构改成了一个 json 对象,且增加了 header 属性,原来的数据被挪到 data 属性中了。编写处理器处理这样的变更,方法和前面介绍的例子别无二致,直接在 App 的 Schema 里筛选出所有使用到这个服务的地方,把处理的逻辑做一些调整即可。
|
||||
|
||||
那么,有没有确实无法解决的破坏性问题呢?
|
||||
|
||||
有的,在我们研发低代码平台的几年里,曾碰到过三四次这样的情况。比如最初的一次是我们重写了编辑器的底层逻辑,导致老版本的 Schema 相比新的 Schema 缺少了非常多的属性,我们尝试一一给出经验值,但效果仍达不到预期。
|
||||
|
||||
再如最近的一次是因为 UX 设计规范有大幅调整,引入了大量的 CSS 方面的破坏性,由于细节太多,几乎不可能枚举完整,我们把能想到的细节都一一用处理器消除了,但实践表明,依然有 30% 左右的 CSS 细节泄露出去了。
|
||||
|
||||
对于这类破坏性,我们采用的解决方法简单粗暴:升级大版本,并对外宣布这个破坏性无解,需要手工处理(我们会给出详细的处理方法,以及配置专人协助升级),一般在低代码平台做重大版本升级时,会采用这个方法。但这样做需要非常谨慎,要尽量避免,并且要与各个应用团队达成共识。
|
||||
|
||||
小结
|
||||
|
||||
根据我的经验,常见的破坏性变更有 Schema 数据结构的变更、模板文件的变更、外部破坏性变更等,这几个类型基本占据了我碰到的破坏性变更总数的 90% 左右。
|
||||
|
||||
虽然单个破坏性变更所产生的负面影响(兼容性问题)一般很有限,但兼容性问题也很麻烦,其他难度再高的技术问题,都是一时的,解决了就解决了,兼容性问题最大的难点在于它产生的负担会随着时间不断累积。这一讲前面的这个图,可以很好地说明这一点:
|
||||
|
||||
|
||||
|
||||
任何一个破坏性都会导致处理用户数据的逻辑产生分叉,每一个分叉则有可能使得之前已有的判断逻辑翻倍,最终导致判断逻辑复杂到再也无法修改的境地。
|
||||
|
||||
根据我的经验,大概平均每 40~60 个修改会引入一个破坏性,一年能累计 20~30 个破坏性变更。所以如果没有一个有效的方法来妥善解决兼容性问题,任何团队都将迟早被它压垮。最直观的表现是代码没人敢改,没人知道为啥正确,也没人知道为啥出错。
|
||||
|
||||
这一讲给出的办法是,将兼容性问题化整为零来处理,每个破坏性变更的发现,都必须开发一个配套的处理器,用于将兼容性发生前的用户数据升级为兼容性发生之后的格式,这样就可以及时地处理掉破坏性。而且,更关键的是,一个处理器与一个破坏性修改是配套的,因此处理器只需要专注于处理 1 个破坏性即可,这就可以让处理器的实现足够简单,足够健壮,足够好维护。
|
||||
|
||||
要妥善解决破坏性问题的前提是需要能及时、稳妥地发现它们,这不只是一个技术问题,需要技术 + 管理两个手段双管齐下,无所不用其极地从各个角度采用各种形式来发现,具体的经验我一讲总结在了上一讲里了,希望你能回顾一下上一讲,并结合自身团队的经验,总结一套适合自身的发现破坏性问题的方法论。
|
||||
|
||||
思考题
|
||||
|
||||
你曾经碰到过的印象最深的兼容性问题是啥?它造成了啥后果?最终又是如何被解决的?
|
||||
|
||||
欢迎在评论区里留下你的故事。我们下一讲再见。
|
||||
|
||||
|
||||
|
||||
|
85
专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md
Normal file
85
专栏/说透低代码/总结与展望|低代码之路,我们才刚刚开始.md
Normal file
@ -0,0 +1,85 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
总结与展望|低代码之路,我们才刚刚开始
|
||||
好快,这个专栏的常规更新部分到这里就正式结束了。恭喜你打败了学习的惰性,一路坚持到这里。现在,我们是时候停下来稍微总结一番,以便更好地开启后续的学习。
|
||||
|
||||
创建好了这篇文章的空白文档后,我的脑袋和新建的文档一样空白,思绪一下子飞跃到了四年前,我决定先和你分享下我是如何与低代码结缘的。听听我的故事,希望它能给你树立一些信心。
|
||||
|
||||
如果非要给我的低代码之旅设定一个明确的起点,那应该是 4 年前的 3 月,在我收到那封低代码相关的封闭研讨邮件的时候。但在这之前,我就建设低代码工具这个问题,与其他兄弟单位有过接触。受限于资源,双方决定采用分工合作的方式,我们负责 Web 组件,对方负责低代码引擎。
|
||||
|
||||
那时我们在成都组织了多次探讨,我现在许多关于低代码的设计理念,都是在这些探讨过程逐渐形成的。但可惜双方理念相差较大,最终没能走到一起。对方无法接受我先通用再具体的方案,执意要“务实”地对具体场景提供支持,因为这样“风险小、见效快”。
|
||||
|
||||
后来再收到这封闭关研讨的邮件时,我就明白老板这是下决心要自己搞了。
|
||||
|
||||
随着研讨越发地深入,我越觉得这个事情难度巨大,甚至,我还一度因畏难情绪和其他因素萌生了退意,但看着这个我当时花了近两年给老板画出来的饼,已经在我嘴边了,只要一张嘴就可以咬上一口,最终我还是选择了坚持。我至今仍在庆幸当时的选择。
|
||||
|
||||
从最开始设计 Awade 的编译器时,担心纯可视化搞不定,选择让编译器能够生成出人类可读、可改的代码;到后来慢慢走上正轨,解决了开发过程中大大小小无数的难题;再到现在,脚下的低代码之路已经逐渐变得平坦且宽阔。从最开始受到的各种质疑,到现在,进度紧急时,经理们会特地点名必须使用 Awade 以确保其交付进度是可控的。
|
||||
|
||||
一时间,感慨万千。
|
||||
|
||||
最后的事实证明,先通用、再具体才是正路。通用性创造的是未来的可能性,虽然早期道路坎坷艰辛,但我们还是坚持下来了,路是越来越平坦的。反之,如果你过于着眼于眼下,看似务实,实际上更容易因架构设计的韧性不足,导致路越走越窄,举步维艰。我一直非常注重竞品调研,我搜遍公司内外各种竞品,做了很多调研,后来成都那个团队的产品也没有再出现在我的竞品清单中。
|
||||
|
||||
在这个专栏的交流群中,我看到很多同学都是一线低代码平台的设计者和实现者。可能很多和那时候的我一样,或是正绞尽脑汁给领导画饼,或是正迷茫在架构策略的抉择中,又或者是正受困于某个技术难题,不知如何向前。
|
||||
|
||||
我非常希望这个专栏能帮助你解决眼下的困境,也非常希望你能坚持一下,再坚持一下,走到最后。如果你在此过程中有啥困惑或难题,欢迎来找我。
|
||||
|
||||
课程设计的背面
|
||||
|
||||
时间拉回今年春节前,极客时间团队的小伙伴开始和我接触,邀请我来写低代码这专栏。我当时的第一想法是非常兴奋,不谦虚地说,我是国内最早开始“吃”低代码这只“螃蟹”的一批人,坚持到现在,已经超过 4 年了。在此过程中我积累了大量的实战经验,极客时间是整理和展示我这些经验的一个很好的平台。
|
||||
|
||||
兴奋之余,我又感觉非常为难:具体的内容应该如何编排呢?毕竟,低代码技术是一个综合性非常高的话题,低代码涉及的方面非常多,可以讲的内容也非常多。我们可以从实际应用的角度来讲,也可以从系统架构实现的方向来讲。同时不同的方向上还可以有不同的侧重,不同的方向和侧重,面对的人群还不一样,适用的内容也不一样。
|
||||
|
||||
所以,我花了非常多的时间纠结到底怎么挑选内容。最终,我选定了低代码平台的架构和实现这个大的讲解方向,同时偏重于系统架构的设计,忽略掉过细的实现细节。
|
||||
|
||||
那么,在低代码的架构和实现这个大方向下,要选择哪些功能和模块进行讲解呢?
|
||||
|
||||
经过一番精挑细选后,我决定围绕着低代码编辑器这个功能来打造这门课。低代码编辑器是低代码平台最关键的功能。它的能力和实现的质量,直接决定了低代码平台的能力和质量。虽然低代码平台综合性很高,可以做的功能点众多,在各家企业里落地时,大家都各有侧重,有的先解决服务端侧,有的先解决前端侧的问题。但无论你侧重建设哪个端的功能,低代码编辑器都是绕不过去的一个功能点。
|
||||
|
||||
这个专栏的常规更新部分的主体内容,就是按照这样的考虑来设计和编写的。
|
||||
|
||||
但是,我们当然不可能一上来就单刀直入,直接剖析低代码编辑器,还是要遵循学习知识的一般认知流程。所以我将内容分成了三大部分,先从认知基础与架构策略切入,着重介绍了低代码的演进策略和低代码编译器与编辑器之间的关系。
|
||||
|
||||
我希望你在开工之前先想好路怎么走,这比啥都重要。你要先想清楚你负责的子系统与上下游子系统的关系,若保持强耦合,对方值得托付吗?还是留个活扣,给自己以后多一个选择?特别是,你不要急着编码,一旦开始写代码,就容易陷入细节,不能自拔了。写代码永远是最容易的一件事,前提是你真的想清楚了。
|
||||
|
||||
接下来的第二、第三部分是我们常规更新的重点内容:低代码编辑器的架构和实现,以及低代码平台的拓展。这些内容都比较硬核,因为我们很难用短短几千字讲清楚一个架构知识点,而且抛开业务谈架构都是在耍流氓,你可以看到,每一讲中我都非常注重业务场景的讲解,也针对不同的业务场景给出了一些建议。你可以结合自己的实际情况辩证地看。
|
||||
|
||||
我注意到,前两天交流群里有同学提到,每一讲的文字量都很大。其实,我之前写文字稿时就注意到了这个问题,我往往需要用目标篇幅的 1.5 倍左右才能完成一讲的内容,常常才写完第一个小标题时就发现已经用掉了大半的目标篇幅了,不得不回头再精简内容,但总是觉得,少了这块不行,少了那块也不行。
|
||||
|
||||
即使我“精打细算”地使用好每一讲的篇幅,但你应该也可以发现,有些内容依然是蜻蜓点水般一带而过了,没能讲透,特别是第三部分的两讲更是如此。这些没涉及、没展开、没讲透的内容,就要留着在这个专栏的动态更新部分才能展开了。
|
||||
|
||||
动态更新是极客时间的一种创新,特别适合学习低代码这样综合性高、内容繁复多样的知识。和常规更新阶段事先敲定内容的学习形式不同,动态更新阶段中,你可以一边学习,一边和我互动,我会针对你的学习状况和需求,动态设计和组织剩下的内容,达到定制化的学习效果。
|
||||
|
||||
动态更新内容,将由你来定制
|
||||
|
||||
那么动态更新部分,将会有哪些内容呢?
|
||||
|
||||
首先,这一阶段最主要的是把在常规更新部分里的那些“等有机会…”“下次再说…”的部分补齐。你可以将这个课程看成是一棵树。常规更新部分是大树的树干和部分枝丫,而动态更新部分是沿着树干生长出的各个重要枝丫;
|
||||
|
||||
其次,这一阶段我们要尝试跳出以低代码编辑器为中心的思路,尝试围绕其他角度,来完成低代码平台的架构和实现。因为低代码编辑器并不是低代码平台的全部,它只是一个核心部件,低代码平台还有其他许多有价值的功能需要建设;
|
||||
|
||||
最后,我们还会对业界进行持续跟踪和观察,包括但不限于开源社区、调查机构、甚至还有一些竞争对手的资料,但这个角度的内容篇幅不会很大。
|
||||
|
||||
动态更新内容的大方向,将和常规更新的内容保持基本一致,我将继续保持在低代码平台的架构和实现这个方向上进行讲解,不会做方向性的大幅度调整。
|
||||
|
||||
不过,此时此刻,动态更新的内容还没完全定型,你我可以一起来设计剩余的内容。欢迎你把想要学习的内容写在评论区、交流群,或者这个【调查问卷】中,我们一起来设计剩余的内容。
|
||||
|
||||
正如在开篇词中向你承诺的那样,动态更新部分将有 20 讲左右的内容,大致以每个季度至少一讲的频率,每年更新 5 讲,持续 4 年。衷心邀请你和我一起完成这场“长跑”,在接下来的学习中,不仅紧追低代码的前沿“脉动”,更重要的是在逐渐深入的学习和实践中,完善低代码的知识体系,提升自己的架构能力。
|
||||
|
||||
这里先预告一下,接下来动态更新阶段的第一篇文章将在今年 7 月更新,一定要记得回来学习呀,我也会在交流群里通知你的!
|
||||
|
||||
写在最后
|
||||
|
||||
最后,我还想跟你说个题外话。有可能你曾经去词典里搜索过“awade”这个单词,想必是一无所获。因为这是一个我自创的单词,它来自“Anyone can be A Web Application Developer Exper”这句话的首字母,翻译过来就是人人都是 Web 应用开发专家。
|
||||
|
||||
这就是 Awade 的使命,也是当时在研讨和设计 Awade 过程中,大家共同的愿望。作为一个低代码平台的设计和实现者,我们希望在它的帮助和赋能下,人人都可以成为一个 Web 应用的开发专家,而不是仅仅只有那些掌握了 Web 研发技术的职业开发人员才能成为专家。
|
||||
|
||||
现在,我把 Awade 的架构和实现方法整理出来了,希望你通过对这个专栏的学习,也能设计出一个低门槛、高效率的低代码开发平台 ,赋能它的使用者,让大家都成为 Web 应用的开发专家,为业务实现真正的降本增效。
|
||||
|
||||
探索低代码之路,我们才刚刚开始,就让我们一起继续研究,探索出更好的低代码实现之路吧!
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user