让你的 react 代码跑在 svelte 引擎上
背景
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[1],细化的这些数据类型在我们做转换推断有一定帮助,所以我们并没有使用 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 组件(本文没有具体展开)。
后续
将运行时的 jsx 百分百准确编译成静态的 html 是不可能的,弱类型语言的变量追踪是不可靠的,非原生逻辑控制语法也无法一一在编译器中枚举;目前在转换工作中还遗留很多编译相关问题,但这些问题可以通过一些插件来补充以致逐渐完善。
在大型项目中包体积的现状并不乐观,Svelte 借助着运行前编译将整个 framework 按需打包可以有效减少包体积,但编译产物本身是没有优势的,当一个页面 UI 交互越复杂,编译产物会越大,加上对 framework 依赖越多,整体包体积的优势就荡然无存了;除此之外,我们转换器为了抹平差异又给编译增加了一定复杂度,所以在编译产物体积的优化上还存在很大的空间。
无性能不前端,在性能方面的数据我们还是缺失的,但也从一些三方文章了解到整体 Svelte 性能并不是瓶颈,而且从理论上来说借助编译来实现数据驱动 DOM 是简洁高效的,脱离 Virtual DOM 理论上也让内存表现更优异;但我们还是会单独去看性能一块的具体表现。
运行前编译的思路不单止应用于 framework,component 同样受用,而且收益颇大。
References
[1]
babel-parser: https://babeljs.io/docs/en/babel-parser#output
评论