查看原文
其他

技术干货 | 泛前端跨端方案的设计与实践

陈名扬 极客人生THE GEEKS 2022-09-09

小编推荐:跨端技术作为泛前端领域前沿技术发展的重要方向之一,目前业界存在多种设计思路,前端和客户端分别有各自的框架和实现思路。

本文从泛前端的角度阐述前端和客户端各个框架及其背后的技术原理和设计思路,并从中分析前端跨端方案和客户端跨端方案融合成为完整跨端方案的实现原理。



1.

目前状况


2019年接近尾声,回顾过去的这一年的前沿前端技术发展趋势,无论是集团内部还是业界都在跨端方向上前进了一大步。跨端技术在价值层面不存在太大争议,一套代码运行多端毫无疑问是前端开发者的福音,而各端产品体验一致性也足以说服业务方来支持这项技术的落地。价值部分十分清晰,剩下的就是各个技术方案以及各自方案成熟度的问题。

目前来看,在业界各个跨端框架还处于百家争鸣的状态,目前还没有哪个框架能一统天下,这也恰恰说明了目前还没有出现有绝对优势的跨端方案,大家的设计思路要么大同小异,要么各有优劣。下面就从我基于个人的粗浅认知简单阐述一下目前主流跨端方案的设计思路,如有谬误之处欢迎指正。



2.

前端跨端设计思路


▌ 主流跨端原理

说到跨端,首先要明确跨端的端指的是什么,对前端来说原先只有web一个端,但是今年来各个平台的小程序如雨后春笋,这才催生了跨端技术的快速发展。所以对前端来说,跨端主要需要跨”N个平台小程序“以及web端,而对客户端来说,跨的就是iOS和Android两端。对一个前端跨端框架来说,最直观的做法就是转换,需要做的是把一套框架的项目转换成多个平台的原生语法能正确识别的项目。

所以核心就是转换。那么如果每个平台的语法差异十分巨大,转化工作将异常艰难,而值得庆幸的是微信小程序选择了一套和vue框架十分相似的语法,而其他平台的小程序则借鉴了微信的成熟经验,虽然不完全相同,但都选择了类vue的语法。对一个完整的前端项目来说,无论是何种形式的平台项目,大致都可分为template,style,script这三部分,那么转换所对应的目标指的就是分别针对这三个部分的转换。

转换必然是在编译过程中处理的,一切编译过程都依赖webpack强大的loader来实现,loader配合正则可以做一些简单的规则转换,而配合上ast则能完成大部分的复杂语法转换。从具体三个部分来看,template主要需要做的是标签的转换以及标签属性的差异转换;script则主要需要完成一些api以及生命周期等部分的转换;相对来说style部分可能是最简单的了,如果没有用到css预处理工具的话甚至不需要太多处理,原因是各平台小程序都直接照搬了css的所有语法,所以几乎无需额外转换。(当然这是在前端领域才有的便利,如果跨端需要支持到客户端语法的转换,由于两者style语法的差异化,可能需要自己实现一遍浏览器的包括css选择器,继承,覆盖等等一系列能力,是个大坑)。
刚才提到,转换的过程都是在编译的时候处理的,那是否意味着完成了编译过程跨端方案就是成熟的呢?其实不然,理论上完成了编译过程确实能输出目标端的指定语法的项目,但考虑到各平台原生dsl语法功能完善度不同,甚至有些语法十分简陋,不支持绝大部分高级的vue框架特性,如模板指令,状态管理等等。而对一个成熟的框架而言,绝对有必要提供这些高级特性且抹平各端差异使之成为通用框架语法。显然这部分功能不可能在编译环节完成,唯一可实现的方案就是在项目运行的时候有专门的一部分逻辑来提供,这部分逻辑我们称之为runtime。
到现在为止一个初步可用的跨端框架有了雏形,但还有一个重要问题需要解决:各端的兼容性差异以及业务差异如何处理?也就是需要业务代码自己来处理的差异如何支持?当然简单来说可以给业务代码输入环境变量,让业务代码自己实现if else逻辑。但如果这么做的话会导致各端兼容代码互相杂糅,如在微信小程序代码包里会有支付宝小程序的兼容处理,无论从性能上还是从包大小上都是会被诟病的一点。所以更加合理的方式是在编译层就直接过滤掉多余端的兼容代码,如chameleon的interface-loader,我们称这个机制为多态。
以上就是主流跨端框架的核心流程设计,除此之外,还有cli,编辑器插件,日志管理,项目管理工具等边边角角的东西还需要完善。
▌ 其他前端跨端方案

以上是chameleon框架的主要设计思路,其实和绝大多数其他框架的思路大同小异。以mpx框架为例,虽然从形式上看起来差异比较大,但本质上都是各部分的编译配合运行时来实现的跨端。我们看到mpx转支付宝小程序是拿微信小程序版本直接转支付宝,而chameleon并不是,看上去似乎有差异。
我们来看下chameleon的编译过程:开发者用chameleon语法(cml-dsl)书写源代码,然后编译生成各个端的包和运行时。其实mpx也一样,唯一不同的是mpx的dsl和微信小程序高度类似,几乎完全兼容微信小程序原生语法,所以可以简单理解为mpx的dsl采用了微信小程序的dsl,他们打出其他端的过程本质是类似的。

业界其他方案也基本大同小异,但是前段时间我看到了腾讯前端技术大会上分享的微信开发社区团队的方案kbone思路完全不同。暂时先不论两者方案的优劣,来看看他们的思路是怎样的。
首先我们看一般的框架是通过什么实现各自的组件对应到浏览器的渲染的,答案是无论是否有virtual dom,对于各自的组件最终都会通过调用dom接口的方式来出发浏览器dom树的变更,从而触发浏览器渲染。那么本质上,如果有能实现一套类似机制能将组件变化实时反馈到视图层触发小程序渲染,那么也就意味着只要在小程序和项目之间加一层这个渲染机制,那么几乎所有的web项目能无缝迁移过来,迁移成本非常低,而且开发者也不需要多学一套dsl,直接用vue就行。
当然如果小程序的渲染机制和浏览器一致的话,那可能很多框架都会选择这个方案了,但事实显然并非如此。小程序的视图层和逻辑层是相互独立的两个线程,这也就决定了小程序设计之初就不会在逻辑线程里抛出操作视图层的接口。我推测很多人到这一步就放弃这个方案了,然而让很多人没想到的是:虽然没有直接操作dom树的接口,但是逻辑层可以通过修改自定义组件来触发视图线程的重新渲染。
这里有两个关键点,一是可以在逻辑层构建虚拟dom树,然后挂载到自定义组件上,这点可行性没有问题;二是自定义组件要对应虚拟dom树则需要满足各种不同可能性的dom变化,如增加节点,删除节点等,而自定义组件恰好能递归调用,这意味着一个自定义组件理论上是可以对应一棵dom树的任意变化。



3.

前端与客户端跨端的结合


上文介绍了客户端跨端方案的各种思路,下面主要着重介绍一下前端与客户端之间结合的技术思路。其实各自看完前端跨端思路和客户端跨端思路,可以发现两者的背景和技术环境差异都很大,所以大多数情况下各自都只是在各自的领域里发挥作用,但是既然目标是跨端,必然需要考虑能真正实现一套代码所有端运行的可行性。乍看之下,原先各自领域内的跨端都已经困难重重,现在要统一一套方案能兼容两个背景和技术环境完全不同的领域似乎更加不可行。的确,完全架空来设计当然非常困难,但是如果借用各自领域内已经成熟的跨端方案,然后以桥接编译的方式来进行兼容,似乎也没那么复杂了。

▌ Chameleon x Hummer

以我们近期的工作chameleon到hummer的结合为例,其实代表了前端主流方案和客户端主流方案的结合。思路如下:chameleon作为前端的跨端方案,以类vue的DSL为目标进行编译是被证明可行的,但Hummer作为客户端的跨端框架,语法介于客户端与前端之间,chameleon自身的能力并不支持简单编译过去的能力。

怎么办?其实也不复杂,只要中间加入一个中间层的DSL,这层DSL作为chameleon和Hummer之间的桥梁自身语法是类vue的,而且有着自己的loader和runtimer,它存在的唯一目的就是将自己编译生成目标Hummer的语法,而chameleon-loader负责编译到这层DSL,那么整个链路就清晰了:
chameleon拓展一个新端(Hummer),在指定Hummer端的时候将源文件按照中间层DSL的语法编译输出,然后再经过hummer-loader编译生成Hummer语法并将hummer-runtime集成进去。得到Hummer目标代码之后再由Hummer通过自身JScore运行在各自环境。

所以其中最关键工作在于从中间层DSL编译到目标Hummer的过程,DSL层是类vue语法,这个和前端跨端编译原理一致,需要有三个部分进行编译。
  • 对template部分来说,hummer提供了一些基础组件,hummer-loader要做的就是将DSL对应的标签转换成hummer组件的声明方式,而且对于chameleon支持的以及常用的模板指令都需要dsl层来实现。

  • 而对于script部分来说,js语法是JScore所能识别的,这里不需要对js语法进行转换,但同样会有api以及生命周期匹配的问题,对于chameleon所支持的生命周期以及api方法,同样需要hummer提供原生方法支持。

  • 最后是style层,这个原本在前端跨端方案中最为简单的部分,则可能是DSL整个方案中最烦人的部分。

由于在客户端并不存在css语法,所以Hummer语法也不支持大部分css通用能力的解析,例如选择器,样式的继承和覆盖等等。这些在前端方案中原本由浏览器来解析并渲染的工作需要由DSL层来重新实现一遍。
举个例子,我们首先需要支持类的选择器,这也就意味着每定义一个类的样式,就需要遍历整棵dom树来依次添加上去,如果类被remove了,同样需要找到受影响的节点把对应的样式删除,而Hummer并不支持样式的删除,所以需要定义默认样式来进行替换。
除此之外,继承和覆盖的实现也都十分复杂,尤其是配合v-for等模板指令同时渲染dom和样式的时候就更加复杂,中间会有大量细节问题。chameleon与Hummer的结合思路大致如上所述,目前整个方案已经完全跑通,剩下的就是实际场景的落地。
▌ Chameleon x Flutter
但是由于上文中提到Flutter可能才是最佳的客户端跨端方案,而Flutter不再有JScore,不再能解析JS语法,那目前的整个方案可能不再可行,所以作为chameleon的建设者还是需要思索chameleon与Flutter是否有结合的可行性
答案也是肯定的,但是方案可能和目前的样子完全不同。由于Dart无法解析JS语法,所以任何JS buddle的输出都不具有实际意义,所以输出到Flutter的buddle语言层面一定是Dart,所以我们可以同时选择Dart语言和chameleon的框架语法来做输出
但问题是目前JS还不具备编译生成Dart的能力,所以开发者需要直接使用Dart语言和chameleon框架语法来进行开发,而chameleon为了解析Dart语法,则需要对chameleon框架用Dart进行完全重构。这样就能彻底解决chameleon与Flutter的结合问题,但同时又带来了原有前端JS跨端的兼容问题,毕竟总有喜欢JS且不需要进行客户端跨端的用户存在。所以这时候还需要利用Dart能转译成JS的特性来转译成JS版本提供出来,虽然提供两个版本,但本质上需要维护的框架源码其实只有Dart一份。
以上就是目前所有跨端方案及其之间结合的几种思路的简单介绍。跨端的未来属于谁,目前谁也说不清楚,但我们每个人都需要对目前所有的方案以及未来可能的发展方向有所了解,才能未雨绸缪,不至于被变革的浪潮远远甩在身后。
--------- PUHUI TECH ---------
本文作者

-


陈名扬

滴滴 |资深软件开发工程师 

2013年毕业于浙江大学,现任滴滴代驾前端技术部负责人,兼任团队内前端跨端FTO,跨端FT今年下半年完成了chameleon框架,与Hummer框架在跨端能力上的融合打通。


编辑 | 钱维

-

推荐阅读

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存