都快2020年,你还没听说过SvelteJS?

React, Vue和Angular差不多占据了Web开发的大部分江山,可是最近半年Svelte开始逐渐吸引越来越多人的眼球。这个Svelte框架到底有什么过人之处呢?本文将会为大家分析一下Svelte火起来的原因,并且通过使用Svelte去搭建一个简单的书店应用(bookshop)来帮助大家快速入门这门框架。

Svelte为什么会火?

要想知道Svelte为什么会火,首先得看看React和Vue这些框架存在什么问题。

big runtime - 大的运行时

React和Vue都是基于runtime的框架。所谓基于runtime的框架就是框架本身的代码也会被打包到最终的bundle.js并被发送到用户浏览器。当用户在你的页面进行各种操作改变组件的状态时,框架的runtime会根据新的组件状态(state)计算(diff)出哪些DOM节点需要被更新,从而更新视图。那么这些runtime代码到底有多大呢,可以看一些社区的统计数据:

Name Size
Ember 2.2.0 435K
Ember 1.13.8 486K
Angular 2 566K
Angular 2 + Rx 766K
Angular 1.4.5 143K
Vue 2.4.2 58.8K
Inferno 1.2.2 48K
Preact 7.2.0 16K
React 0.14.5 + React DOM 133K
React 0.14.5 + React DOM + Redux 139K
React 16.2.0 + React DOM 97.5K

从上面的表格可以看出常用的框架中,最小的Vue都有58k,React更有97.5k。换句话说如果你使用了React作为开发的框架,即使你的业务代码很简单,你的首屏bundle size都要100k起步。当然100k不算很大,可是事物都是相对的,相对于大型的管理系统来说100k肯定不算什么,可是对于那些首屏加载时间敏感的应用(例如淘宝,京东主页),100k的bundle size在一些网络环境不好的情况或者手机端真的会影响用户体验。那么如何减少框架的runtime代码大小呢?要想减少runtime代码的最有效的方法就是压根不用runtime。其实回想一下Web开发的历史,很早之前在用Jquery和Bootstrap一把梭的时候,我们的代码不就是不包含runtime的吗?当数据变化时直接通过JavaScript去改变原生DOM节点,没有框架那一系列diff和调度(React Fiber)的过程。这时你可能会问,要减少bundle size真的要回到那个刀耕火种的时代吗?有没有那种既可以让我用接近React和Vue的语法编写代码,同时又不包含框架runtime的办法。这恰恰就是Svelte要做的东西,它采用了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的优化真的很大。

低效的Virtual DOM Diff

什么?Virtual DOM不是一直都很高效的吗?其实Virtual DOM高效是一个误解。说Virtual DOM高效的一个理由就是它不会直接操作原生的DOM节点,因为这个很消耗性能。当组件状态变化时它会通过某些diff算法去计算出本次数据更新真实的视图变化,然后只改变“需要改变”的DOM节点。用过React的人可能都会体会到React并没有想象中那么高效,框架有时候会做很多无用功,这体现在很多组件会被“无缘无故”进行重渲染(re-render)。注意这里说的re-render和对原生DOM进行操作是两码事!所谓的re-render是你定义的class Component的render方法被重新执行,或者你的组件函数被重新执行。组件被重渲染是因为Vitual DOM的高效是建立在diff算法上的,而要有diff一定要将组件重渲染才能知道组件的新状态和旧状态有没有发生改变,从而才能计算出哪些DOM需要被更新。你可能会说React Fiber不是出来了吗,这个应该不是问题了吧?其实Fiber这个架构解决的问题是不让组件的重渲染和reconcile的过程阻塞主线程的执行,组件重渲染的问题依然存在,而且据反馈,React Hooks出来后组件的重渲染更加频繁了。正是因为框架本身很难避免无用的渲染,React才允许你使用一些诸如shouldComponentUpdate,PureComponent和useMemo的API去告诉框架哪些组件不需要被重渲染,可是这也就引入了很多模板代码(boilerplate)。如果大家想了解更多关于Virtual DOM存在的问题,可以看一下virtual dom is pure overhead这篇文章。

那么如何解决Vitual DOM算法低效的问题呢?最有效的解决方案就是不用Virtual DOM!其实作为一个框架要解决的问题是当数据发生改变的时候相应的DOM节点会被更新(reactive),Virtual DOM需要比较新老组件的状态才能达到这个目的,而更加高效的办法其实是数据变化的时候直接更新对应的DOM节点

if (changed.name) {
  text.data = name;
}

这就是Svelte采用的办法。Svelte会在代码编译的时候将每一个状态的改变转换为对应DOM节点的操作,从而在组件状态变化的时候快速高效地对DOM节点进行更新。根据js framework benchmark的统计,Svelte在对一些大列表操作的时候性能比React和Vue都要好。

什么是Svelte?

Svelte是由RollupJs的作者Rich Harris编写的编译型框架,没了解过RollupJs的同学可以去它官网了解一下,它其实是一个类似于Webpack的打包工具。Svelte这个框架具有以下特点:

  • 和React,Vue等现代Web框架的用法很相似,它可以允许开发者快速开发出具有流畅用户体验的Web应用。
  • 不使用Virtual DOM,也不是一个runtime的库。
  • 基于Compiler as framework的理念,会在编译的时候将你的应用转换为原生的DOM操作。
  • 默认就支持类似于CSS modules的CSS scope功能,让你避免CSS样式冲突的困扰。
  • 原生支持CSS animation。
  • 极其容易的组件状态管理(state management),减少开发者的模板代码编写(boilerplate less)。
  • 支持反应式定义(Reactive statement)。
  • 极其容易的应用全局状态管理,框架本身自带全局状态,类似于React的Redux和Vue的Vuex。
  • 支持context,避免组件的props drilling。

Svelte这个框架与Vue和React之间最大的区别是它除了管理组件的状态和追踪他们的渲染,还有很多其他有用的功能。例如它原生支持CSS scope和CSS animation。如果你用React或者Vue是需要引入第三方库来实现同样的功能的,而第三方依赖的引入会给开发者增加学习和维护的成本。

用Svelte搭建一个Bookshop应用

接下来我们会从头开始搭建一个基于Svelte框架的简单书店应用bookshop,通过这个demo,希望大家可以理解Svelte的一些基本概念和掌握它的一些基本用法并能够使用Svelte去搭建更加复杂的应用。

应用功能

Bookshop应用支持以下功能:

  • 管理员录入新图书
  • 展示书店图书列表
  • 将图书加到购物车
  • 展示购物车的数据信息

对学习者的技术要求

  • 掌握html,css和javascript的基础用法
  • 有过React或者Vue的相关开发经验最佳

项目的源代码可以在我的github仓库找到。

项目搭建

首先在我们的本地开发环境新建一个项目文件夹:

mkdir svelte-bookshop

接着用svelte官方的脚手架去初始化我们的应用:

npx degit sveltejs/template svelte-bookshop
cd svelte-bookshop

yarn
yarn dev

degit这个命令会将github上面的项目文件直接拷贝到某个本地文件夹,这里使用到的svelte/tempalte模板项目的github地址是这个。以上命令成功运行后,访问http://localhost:5000你会看到如下界面:

生成的代码主要包含以下文件目录结构:

  • rollup.config.js,这个是rollup的配置文件,类似于webpack.config.js,里面指定了项目的入口文件是src/main.js。
  • src文件夹,这个文件夹用来存储我们的项目源代码,现在只有一个项目的主入口文件main.js和一个组件文件App.svelte。
  • public文件夹,这个文件夹是用来存储项目的静态文件(index.html, global.css和favicon.png)和rollup编译生成的静态文件(build文件夹底下的bundle.js和bundle.css以及它们各自的source map)。

接着让我们具体看一下src文件夹底下的各个文件内容

src/App.svelte



Hello {name}!

Visit the Svelte tutorial to learn how to build Svelte apps.

这个文件定义了一个叫做App的Svelte组件,这里要注意App.svelte文件内并没有定义组件的名称,组件的名称是由它的文件名确定的Svelte组件的文件名都是以.svelte结尾的,一个组件文件通常会包含以下三部分内容:

  • Welcome to my online bookstore!

    使用自定义的组件的方法很简单:先在script标签里面导入新定义的组件BookCard,然后将该组件写在App组件的HTML markup里面,语法和JSX一样的。 这时候再查看页面的内容:

    CSS

    BookCard组件虽然出来了,我们得定义一些CSS让它变得更好看一点:

    // src/BookCard.svelte
    
    
    

    title

    price

    description

    给组件定义样式的方法就是新建一个style标签然后把该组件相关的样式写在这个标签内,注意这里的样式只会对组件内的元素有效,不会影响到其他组件的样式的。加完样式后,界面应该会变成这个样子:

    props定义

    书本的具体信息应该是由传入的props指定的。Svelte组件的props要用export来指明,指明的props变量可以直接被组件使用

    // src/BookCard.svelte
    
    
    
    
    

    title

    price

    description

    变量使用

    定义和引入的变量可以在组件的HTML markup中直接使用,具体用法是在Markup中用花括号(curly braces)引用该变量,具体代码时:

    // src/BookCard.svelte
    
    
    
    
    

    {title}

    ${price}

    {description}

    组件参数传递

    然后在父级组件App中,将BookCard需要的参数传给该组件:

    // src/App.svelte
    
    
    
    
    

    Welcome to my online bookstore!

    这时候书本卡片的内容应该是传入的参数了:

    对于组件参数传递,Svelte还提供了以下更加方便的写法:

    // src/App.svelte
    

    Welcome to my online bookstore!

    录入书本信息

    作为一个书店,管理员应该可以录入新的图书,所以我们给App组件添加一个简单的表单来让用户录入数据:

    // src/App.svelte
    
    
    
    
    

    Welcome to my online bookstore!

    Add new book