作者 | shuan feng,携程高级前端开发工程师,关注性能优化、低代码、svelte等领域。
一、技术调研
最近几年,前端框架层出不穷。近两年,前端圈又出了一个新宠:Svelte。作者是 Rich Harris,也就是 Ractive, Rollup 和 Buble的作者,前端界的“轮子哥”。
通过静态编译减少框架运行时的代码量。一个 Svelte 组件编译之后,所有需要的运行时代码都包含在里面了,除了引入这个组件本身,你不需要再额外引入一个所谓的框架运行时!
在Github上拥有 5w 多的 star!
在最新的State of JS 2021和Stack Overflow Survey 2021的排名情况中,也一定程度上反映了它的火热程度。
在早前知乎的如何看待 svelte 这个前端框架?问题下面,Vue的作者尤雨溪也对其做出了极高的评价:
去它的官网看一下:
官网上清楚的表明了三大特性:
- Write less code
- No virtual DOM
- Truly reactive
1.1 Write less code
顾名思义,是指实现相同的功能,Svelte的代码最少。这一点会在后面的示例中有所体现。
1.2 No virtual DOM
Svelte的实现没有利用虚拟DOM,要知道Vue和React的实现都是利用了虚拟DOM的,而且虚拟DOM不是一直都很高效的吗?
Virtual DOM 不是一直都很高效的吗?
其实 Virtual DOM高效是一个误解。说 Virtual DOM 高效的一个理由就是它不会直接操作原生的 DOM 节点,因为这个很消耗性能。当组件状态变化时,它会通过某些 diff 算法去计算出本次数据更新真实的视图变化,然后只改变需要改变的 DOM 节点。
用过 React 的同学可能都会体会到 React 并没有想象中那么高效,框架有时候会做很多无用功,这体现在很多组件会被“无缘无故”进行重渲染(re-render)。所谓的 re-render 是你定义的 class Component 的 render 方法被重新执行,或者你的组件函数被重新执行。
组件被重渲染是因为 Vitual DOM 的高效是建立在 diff 算法上的,而要有 diff 一定要将组件重渲染才能知道组件的新状态和旧状态有没有发生改变,从而才能计算出哪些 DOM 需要被更新。
正是因为框架本身很难避免无用的渲染,React 才允许你使用一些诸如 shouldComponentUpdate,PureComponent 和 useMemo 的 API 去告诉框架哪些组件不需要被重渲染,可是这也就引入了很多模板代码。
那么如何解决 Vitual DOM 算法低效的问题呢?最有效的解决方案就是不用 Virtual DOM!
1.3 Truly reactive
第三点真正的响应式,上面也提到了前端框架要解决的首要问题就是:当数据发生改变的时候相应的 DOM 节点会被更新,这个就是reactive。
我们先来看下Vue和React分别是如何实现响应式的。
React reactive
通过useState定义countdown变量,在useEffect中通过setInterval使其每秒减一,然后在视图同步更新。这背后实现的原理是什么呢?
React 开发者使用 JSX 语法来编写代码,JSX 会被编译成 ReactElement,运行时生成抽象的 Virtual DOM。
然后在每次重新 render 时,React 会重新对比前后两次 Virtual DOM,如果不需要更新则不作任何处理;如果只是 HTML 属性变更,那反映到 DOM 节点上就是调用该节点的 setAttribute 方法;如果是 DOM 类型变更、key 变了或者是在新的 Virtual DOM 中找不到,则会执行相应的删除/新增 DOM 操作。
Vue reactive
用Vue实现同样的功能。Vue背后又是如何实现响应式的呢?
大致过程是编译过程中收集依赖,基于 Proxy(3.x) ,defineProperty(2.x) 的 getter,setter 实现在数据变更时通知 Watcher。
像Vue和React这种实现响应式的方式会带来什么问题呢?
- diff 机制为 runtime 带来负担
- 开发者需自行优化性能
- useMemo
- useCallback
- React.memo
- ...
那么Svelte又是如何实现响应式的呢?
Svelte reactive
其实作为一个框架要解决的问题是当数据发生改变的时候相应的 DOM 节点会被更新(reactive),Virtual DOM 需要比较新老组件的状态才能达到这个目的,而更加高效的办法其实是数据变化的时候直接更新对应的 DOM 节点。
这就是Svelte采用的办法。Svelte会在代码编译的时候将每一个状态的改变转换为对应DOM节点的操作,从而在组件状态变化的时候快速高效地对DOM节点进行更新。
深入了解后,发现它是采用了 Compiler-as-framework 的理念,将框架的概念放在编译时而不是运行时。你编写的应用代码在用诸如 Webpack 或 Rollup 等工具打包的时候会被直接转换为 JavaScript 对 DOM 节点的原生操作,从而让 bundle.js 不包含框架的 runtime。
那么 Svelte 到底可以将 bundle size 减少多少呢?以下是 RealWorld 这个项目的统计:
由上面的图表可以看出实现相同功能的应用,Svelte的bundle size大小是Vue的1/4,是React的1/20!单纯从这个数据来看,Svelte这个框架对bundle size的优化真的很大。
看到这么强有力的数据支撑,不得不说真的很动心了!
二、项目落地
为了验证Svelte在营销 h5 落地的可能,我们选择了口罩机项目:
上图是口罩机项目的设计稿,不难看出,核心逻辑不是很复杂,这也是我们选用它作为Svelte尝试的原因。
首先项目的基础结构是基于svelte-webpack-starter创建的,集成了TypeScript、SCSS、Babel以及Webpack5。但这个基础模板都只进行了简单的支持,像项目中用到的一些图片、字体等需要单独使用loader去处理。
启动项目,熟悉的hello world:
这里看下核心的
当然开发环境使用webpack有时不得不说体验不太好,每次都要好几秒,我们就用Vite来替代了,基本都是秒开:
Vite的配置也比较简单:
2.1 组件结构差异
和 React 组件不同的是,Svelte 的代码更像是以前我们在写 HTML、CSS 和 JavaScript时一样(这点和Vue很像)。
所有的 JavaScript 代码都位于 Svelte 文件顶部的 <script></script> 标签当中。然后是 HTML 代码,你还可以在 <style></style> 标签中编写样式代码。组件中的样式代码只对当前组件有效。这意味着在组件中为 <div> 标签编写的样式不会影响到其他组件中的 <div> 元素。
2.2 生命周期
Svelte 组件的生命周期有不少,主要用到的还是 onMount、 onDestoy、beforeUpdate、afterUpdate,onMount 的设计和 useEffect 的设计差不多,如果返回一个函数,返回的函数将会在组件销毁后执行,和 onDestoy 一样:
2.3 初始状态
接下来是对初始状态的定义:
我们发现代码在对变量更新的时候并没有使用类似React的setState方法, 而是直接对变量进行了赋值操作。仅仅是对变量进行了赋值就可以引发视图的变化, 很显然是数据响应的, 这也正是Svelte的truly reactive的体现。
2.4 条件判断
项目中使用了很多的条件判断,React由于使用了JSX,所以可以直接使用JS中的条件控制语句,而模板是需要单独设计条件控制语法的。比如Vue中使用了v-if。
Svelte中则是采用了{#if conditions}、{:else if}、{/if},属于Svelte对于HTML的增强。
上面代码中有这么一行:
$: buttonText = isTextShown ? 'Show less' : 'Show more'
buttonText依赖了变量isTextShown,依赖项变更时触发运算,类似Vue中的computed,这里的Svelte使用了$:关键字来声明computed变量。
这又是什么黑科技呢?这里使用的是 Statements and declarations 语法,冒号:前可以是任意合法变量字符。
2.5 数据双向绑定
项目中有很多地方需要实现双向绑定。我们知道React是单向数据流,所以要手动去触发变量更新。而Svelte和Vue都是双向数据流。
Svelte通过bind关键字来完成类似v-model的双向绑定。
2.6 列表循环
项目中同样使用了很多列表循环渲染。Svelte使用 {#each items as item}{/each} 来实现列表循环渲染,这里的item可以通过解构赋值,拿到item里面的值。
不得不说有点像ejs
2.7 父子属性传递
父子属性传递时,不同于React中的props,Svelte 使用 export 关键字将变量声明标记为属性,export 并不是传统 ES6 的那个导出,而是一种语法糖写法。
注意只有 export let 才是声明属性
2.8 跨组件通讯(状态管理)
既然提到了父子组件通讯,那就不得不提跨组件通讯,或者是状态管理。这也一直是前端框架中比较关注的部分,Svelte 框架中自己实现了 store,无需安装单独的状态管理库。你可以定义一个 writable store, 然后在不同的组件之间进行读取和更新:
每个 writable store 其实是一个 object, 在需要用到这个值的组件里可以 subscribe 他的变化,然后更新到自己组件里的状态。在另一个组件里可以调用 set和update 更新这个状态的值。
2.9 路由
Svelte 目前没有提供官方路由组件,不过可以在社区中找到:
- svelte-routing
- svelte-spa-router
svelte-routing和react-router-dom 的使用方式很像:
而svelte-spa-router更像vue-router一点:
2.10 UI
项目中也用到了组件库,通常react项目一般都会采用NFES UI,但毕竟是react component,在Svelte中并不适用。我们尝试在社区中寻找合适的Svelte UI库,查看了Svelte Material UI、Carbon Components Svelte等,但都不能完全满足我们的需求,只能自己去重写了(只用到了几个组件,重写成本不算很大)。
2.11 单元测试
单元测试用的是@testing-library/svelte:
基本用法和React是很类似的。
业务代码迁移完毕,接着就是对原有功能case的逐一验证。
为了验证单单使用Svelte进行开发的效果,我们没有进行其他的优化,发布了一版只包含Svelte的代码到产线,来看下bundle size(未做gzip前)和lighthouse评分情况:
除此之外,我们遵循lighthouse给出的改进建议,对Performance、Accessibility和SEO做了更进一步的优化改进:
Performance的提升主要得益于图片格式支持webp以及一些资源的延迟加载,Accessibility和SEO的提升主要是对meta标签的调整。
三、实践总结
通过这次技改,我们对Svelte有了一些全新的认知。
整体来说,Svelte 继前端三大框架之后推陈出新,以一种新的思路实现了响应式。
因其起步时间不算很长,国内使用程度仍然偏少,目前来说其生态还不够完备。
但这不能掩盖其优势:足够“轻”。Svelte非常适合用来做活动页,因为活动页一般没有很复杂的交互,以渲染和事件绑定为主。正如文章最开始说的,一个简单的活动页却要用React那么重的框架多少有点委屈自己。所以对于一些营销团队,想在bundle size上有较大的突破的话,Svelte是绝对可以作为你的备选方案的。
另外现在社区对于Svelte还有一个很好的用法是使用它去做Web Component,好处也很明显:
- 使用框架开发,更容易维护
- 无框架依赖,可实现跨框架使用
- 体积小
所以对于想实现跨框架组件复用的团队,用Svelte去做Web Component也是一个很好的选择。