作者:闲鱼技术——羲凡
背景
Svelte UI框架是一款类似react、vue一样的UI框架,有兴趣的同学可以自行查阅。Svelte有着很多不一样的特质,其中我们最为关注的是它的运行前编译。像更简洁灵活的响应式写法、更小的运行包体积这些都得益于运行前编译;但同样也存在它的局限性,Svelte无法像React一样做到高度灵活的模板嵌套、封装以及独立上下文。早期在小项目中我们也体验过,在包体积上收益颇大;但目前整个淘系前端还是有着自己统一的一套基于React的Rax framework,如果说需要将Svelte的收益快速落地最好的方式肯定不是重构所有的业务与生态。
主体思路
要实现dsl层无感换引擎也就只有在ast层做转换,所以我们从开始的思路就是React ast -> Svelte ast,为了保证这一思路的清晰我们暂且把框架的特性(hook、函数jsx)丢掉,保留最干净的三部分jsx、js、css进行转换。主体思路如下
JSX转换
jsx可以说是这两个UI框架最大差异的体现;React的jsx是运行时,原汁原味保留着原生js运行时的灵活性,而Svelte的html是运行前编译,意味着运行前就会敲定html中的逻辑与依赖变量。我暂且将jsx本身拆分为三部分:element节点
、attribute属性
、mustache模板系统
、函数JSX
element与attribute
这部分主要是常规的节点属性映射能力
mustache模板系统
在React的模板系统中可以有以下几种可能:
1、变量输出 { xxx }
2、与运算jsx输出 { xx && <view> }
3、三元运算jsx输出 { xx ? <n> : <m> }
4、函数调用jsx输出 { jsxFn() }
其中第1种情况不需要处理,Svelte的模板系统也是能够支持常规的变量输出;而第4种属于函数jsx;第2、3种可以转换成Svelte提供的{#if ...}语法糖。
至此,简单jsx的ast就能编译成svelte html的ast。
函数JSX
函数jsx背后折射的是js运行时,意味着可以封装独立的运行上下文(scope),可以做到环境变量隔离。先来看看一个常规的函数jsx,再进一步抽象成三部分:上下文
、逻辑控制
、JSX
。
其中JSX部分的转换可以复用上文讲到的element与attribute部分。我们重点来看上下文
与逻辑控制
的转换。
上下文(scope)
尝试使用函数模拟出完整的执行上下文
优化调用次数
逻辑控制
这里我指的逻辑控制是控制jsx输出的逻辑(非jsx输出的控制逻辑不需要关心),在一个jsx函数中,逻辑控制语句可有、可无、可组合、可嵌套,在js中大致常用的逻辑控制语句大致有以下几种:
1、if else
2、switch case
3、for/while/do while
4、return is ? <viewa> : <viewb>
5、return is && <view>
根据Svelte提供的可编译html逻辑控制语法糖来做个映射:
逻辑控制语句转换过程中较为麻烦的是通盘的识别,例如下面例子,我们怎么识别出 if (!gVari3)
和 if (!gVari4)
是属于jsx的逻辑控制语句?这件事就好比这段代码交给一个开发者来重构,他是怎么判别的;所以为了让结果准确必须拉通全盘来识别。
转换大致结果
看着产物有些恐怖,极其膨胀,但这也给后面优化留够了空间。
JS转换
js层面的差异主要来自两方面:hook体系(不考虑类)、ecma ast差异
hook体系
hook体系的api更多是为了纯函数组件注入状态与生命周期的,在这两点上Svelte给出的方案是截然不同的,得益于运行前编译,Svelte编译器扫描了所有与UI相关的状态并注入黑魔法,使得状态的使用就是变量申明赋值那么简单,基本上开发者不需要太关心所谓的副作用;所以有一些hook接口在Svelte框架上显得是有些多余的。
但鉴于hook接口较多,我们在转换过程选择了内置svelte-hooks来简化转换逻辑,svelte-hooks是基于svelte且对标react hook来实现的一套hook接口,在使用上基本保持一样。
ecma ast差异
babel提供的parse是基于estree,但同时在基础上细化出一些类型,具体可以差异可以参考这里babel-parser,细化的这些数据类型在我们做转换推断有一定帮助,所以我们并没有使用babel提供estree插件,而且在转换之后的ast再进行一次差异抹平。
CSS转换
css转换相比上面两部分的转换就要简单很多,React的样式是标准的css,Svelte编写的样式也是标准css,不过会在基础上增加了一定的编译能力,可以理解是标准css的超集,可以直接使用。不过为了抹平jsx与Svelte html在自定义组件的类选择器上的差异,我们还是在编译阶段做了一些转换,这里就不展开了。大致流程如下
结果
一个重构过的业务(只含UI相关逻辑)
细化后的体积差异
客观对比:编译产物是要比正常打包要大的,从前面的函数jsx编译思路来看是要膨胀的,在预期内;拉开差距的是framework与依赖的UI Component,Rax是集团内部基于react进行简化后的framework,如果使用React framework做比较这个差距会进一步的拉大;而UI Component这部分的差异是得益于我们参照运行前编译的思路重新设计了一套可在编译阶段根据具体使用情况来按需打包的轻量UI组件(本文没有具体展开)。
后续
1、将运行时的jsx百分百准确编译成静态的html是不可能的,弱类型语言的变量追踪是不可靠的,非原生逻辑控制语法也无法一一在编译器中枚举;目前在转换工作中还遗留很多编译相关问题,但这些问题可以通过一些插件来补充以致逐渐完善。
2、在大型项目中包体积的现状并不乐观,Svelte借助着运行前编译将整个framework按需打包可以有效减少包体积,但编译产物本身是没有优势的,当一个页面UI交互越复杂,编译产物会越大,加上对framework依赖越多,整体包体积的优势就荡然无存了;除此之外,我们转换器为了抹平差异又给编译增加了一定复杂度,所以在编译产物体积的优化上还存在很大的空间。
3、无性能不前端,在性能方面的数据我们还是缺失的,但也从一些三方文章了解到整体Svelte性能并不是瓶颈,而且从理论上来说借助编译来实现数据驱动DOM是简洁高效的,脱离Virtual DOM理论上也让内存表现更优异;但我们还是会单独去看性能一块的具体表现。
4、运行前编译的思路不单止应用于framework,component同样受用,而且收益颇大。