Svelte:轻量高效的前端框架

2,917 阅读8分钟

前言

自2016年svelte首个版本发布以来已经走过了第八个年头了。 到目前为止已经有80k+的⭐,生态也在逐渐地完善。在目前VueReact提供成熟的前端应用解决方案的情况下依旧能够脱颖而出,svelte也自然有它的过人之处。

实际上,他提供了一个新的方案:将前端项目以“编译”的方式将项目打包,令其脱离“virtual DOM”渲染方式,将JS代码编译成无运行时依赖,直接对DOM操作的优化代码。使其减少运行时代码的体积的同时,也能尽可能减少渲染时间。

本文的大纲如下:

  1. 简单介绍Svelte的基本原理和应用场景
  2. 介绍Svelte的基本语法
  3. 小结

1.Svrlte简介

Svelte 是一个在 web 中构建用户界面的框架。它使用编译器并使用 HTML,CSS 和 Javascript 来编写声明式组件.

Svelte is a framework for building user interfaces on the web. It uses a compiler to turn declarative components written in HTML, CSS and JavaScript...

核心原理

Svelte 是一个新的前端框架,它通过编译器驱动的方式,将组件代码在构建时编译为原生的JavaScript。与React 或 Vue 运行时解析和渲染的方式不同,Svelte 的核心是没有Virtual DOM,而是直接生成对 DOM 操作的代码。

因此,svelte打包出来的代码是无运行时(runtime)依赖的,这意味着编译后可以减少运行时代码的体积。

Svelte编译的基本原理如下:

  • 首先,Svelte将.svelte文件解析为抽象语法树(AST)。

  • 然后通过分析出来的AST,分析模板中的绑定、状态、事件、条件、循环等特性, 生成直接操作 DOM 的 JavaScript 代码。

其中,.svelte后缀的文件是用于构建 Svelte 应用的组件。与.vue文件类似,.svelte文件包括三部分:HTML, style, script, 其中 stylescript 是可选的。

// .svelte file

<script module lang="ts">
    // Script module
</script>

<script lang="ts">
    // Script logic
</script>

<style>
    // Style
</style>

为了直观地使用对比代码体积,这里我使用vite分别创建了一个Vue3和一个Svelte的基础项目并执行打包。可以看到svelte打包出来的代码会明显小于vue打包出来的。

image.png

应用场景

  • 单页应用(SPA):与VueReact相同,svelte可以作为单页应用的框架对项目进行开发,并能提供更高的性能页面渲染。但是对于开发生态而言,目前与前两者固然无法比较。

  • 嵌入式开发:一个合适的应用场景就是嵌入式或物联网的前端开发。对于嵌入式的开发板/芯片而言,很多时候我们需要做的是对项目体积进行极致的压缩。那么,对于无运行时代码包的svelte编译框架而言就是一个非常不错的选择,既能提供类似vuereact相似的功能同时也有较小的代码包体积。

2.基本语法

Rune(s)

Runes 是一个用于在**.svelte**文件中控制 Svelte 编译器的一种特殊符号。

let message = $state("hello");

$state

与 React 的 state 相似,$state 允许我们创建一个响应状态,让 UI 可以动态地响应数据变化。

<script lang="ts">

let count = $state(1)
const onClick = () => count++

</script>

<button onClick={onClick}>
{ count }
</button>

如果声明的stste是一个数组或对象,state 则会返回一个proxy对象


<script lang="ts">
    const obj = $state<{ test: number }>({ test: 1 })

    console.log(obj) // Proxy
</script>

<p>{ obj.test }</p> // 1

image.png

我们也可以使用class关键字来使用$state

<script lang="ts">
  class TestClass {
    num: number = $state(0);
    text: string = $state("");
    constructor(text: string) {
      this.text = text;
    }

    public addCount() {
      this.num++;
    }
  }

  const test = new TestClass("Hello World");
</script>

<button onclick={() => test.addCount()}>Add Count</button>
<p>{test.text} {test.num}</p>
  • $state.raw

如果我们不想在 Array 或 Object 中深度监听数据变化,我们可以用$state.raw方法声明变量。


<script lang="ts">

let test = $state.raw({
    text: "Hello world"
})

test.text = "New Text"

setTimeout(() => {
    test = {
        text: "Text Changed"
    }
}, 2000)

</script>

<p>{test.text}</p>

这种方法可以提高在大型数组和对象中的性能,避免在这样的数据中进行过度的监听。

  • $state.snapshot

$state进行深度监听数据时,返回的是一个Proxy对象,如果我们想获取在某一个时间片段中获取该Proxy对象的静态数据,需要用到$state.snapshot方法。

<script lang="ts">
    const test = $state({ a: 1, b: 2 })
    console.log(test); // Proxy
    console.log($state.snapshot(test)); // { a: 1, b: 2 }
</script>

$derived

类似于 Vue 的computed和 React 的useMemo, $derived可以监听已有的 state 变化而返回经过开发者处理后的数据,例如

<script lang="ts">
  let num = $state(1);
  let multiple = $derived(num * 2);
</script>

<button onclick={() => num++}>add</button>
<p>{num} {multiple}</p>
  • $derived.by

$derived 可以捕捉到一些基本的数据类型的变化并返回,如果要进行复杂的数据处理,需要用到$derived.by方法

<script lang="ts">
let arr = $state([1,2,3,4])
let total = $derived.by(() => arr.reduce((a, b) => a + b, 0))
</script>

<button onclick={() => arr.push(arr.length + 1)}>Push</button>
<p>{ arr.join(" + ") } = {total}</p>

image.png

$effect

$effect 函数会追踪那些statederived发生变化,然后重新执行对应的函数。(类似于 React 中的副作用函数 useEffect) 大多数effects都用于通过 Svelte 本身创建,例如 <h1>{ title }</h1>, 当 title 发生变化时,Svelte 内置的 effect 函数则会调用。 同时,我们也可以通过$effect来自定义 effect 函数。

<script lang="ts">
  let testNum = $state(0)

  $effect(() => {
    console.log("state is changed: ", testNum)

  })

</script>

<button onclick={() => testNum++}>{ testNum }</button>

image.png 可以看到当点击按钮 state 发生变化后,先前声明的 effect 函数会进行调用。

需要注意的是,$effect 中只能监听函数内同步读取的依赖,异步读取的数据不会被追踪,例如 awaitsetTimeout 中的数据

<script lang="ts">
  let testNum = $state(0)
  let testDerive = $state(0)

  $effect(() => {
    setTimeout(() => {
      // 依赖不会被追踪
      testDerive = testNum * 2
    }, 0)
  })

</script>

<button onclick={() => testNum++} >{testNum}</button>
<p>testDerive: {testDerive}</p>

image.png

  • $effect.pre

顾名思义,$effect.pre会在 DOM 更新之前调用该 effect 函数。

  • $effect.tracking

方法会返回一个布尔值, 用于查看当前组件是否存在追踪上下文

<script lang="ts">
  console.log("Current component is tracking:", $effect.tracking()); // false

  $effect(() => {
    console.log("Current component is tracking:", $effect.tracking()) // true
  })
</script>

<p>in template: {$effect.tracking()}</p> <!-- false -->
  • $effect.root

改方法会创建一个非追踪并且不会被自动清除的作用域,适用于手动控制内嵌effect函数,For example:

<script lang="ts">
	let count = $state(0);

	const cleanup = $effect.root(() => {
		$effect(() => {
			console.log(count);
		});

		return () => {
			console.log('effect root cleanup');
		};
	});
</script>

$props

  • VueReact 类似,$props 用于获取传入组件的参数
<script lang="ts">
  const { test }: { test: string } = $props()
</script>
  
<p>This is a props: { test }</p>

$bindable

$props中传入的数据,通常是从父组件到子组件单向的数据流。如果我们想创建一个从子组件往上流动的数据,则需要使用$bindable.

<!-- child.svelte -->
<script lang="ts">
  let { test = $bindable() }: { test: string } = $props()
</script>
<input bind:value={test} />



<!-- Parent Component 父组件 -->

<script lang="ts">
  import Child from './child.svelte'
  let label = $state("Hello")
</script>

<Child bind:test={label} />
<p>{label}</p>

$inspect$inspect(...).with

  • ⚠ 该方法仅在开发时使用

  • 主要用于打印statestate和derived数据的变化,作用是当监听的数据发生变化时,将数据打印到控制台。

<script lang="ts">
  let count = $state(0)
  let num = $derived(count * 2)

  $inspect(num)
</script>

<button onclick={() => count++} >Add</button>

image.png

  • $inspect(...).with 则是用于自定义debug的方法
<script lang="ts">
  let count = $state(0)
  let num = $derived(count * 2)

  $inspect(num).with((type, value) => {
    console.log("custom:", type, value)
  })
</script>

<button onclick={() => count++} >Add</button>

image.png

$host

  • 该rune为子组件提供了一个可以访问宿主元素并调度自定义事件
<svelte:options customElement="my-test" />

<script lang="ts">
	function dispatch(type: string) {
		$host().dispatchEvent(new CustomEvent(type));
	}
</script>

<button onclick={() => dispatch('decrement')}>decrement</button>
<button onclick={() => dispatch('increment')}>increment</button>

<!-- 父组件 -->

<script lang="ts">
  import "./lib/Test.svelte";

  let count = $state(0);

</script>

<main>
  <div class="card">
    <my-test
      ondecrement={() => count -= 1}
      onincrement={() => count += 1}
    ></my-test>
    <p>count: {count}</p>
  </div>
</main>

需要注意的是:

  • 要使用customElement需要在编译配置中加上customElement:true

  • 子组件中的customElement命名必须带有-,例如my-test, my-example


// vite.config.ts
export default defineConfig({
  plugins: [svelte({
    compilerOptions: {
      customElement: true
    }
  })],
})

Template 表达式

Template 表达式主要用于在.svelte文件中编写动态的HTML

条件表达式 {#if ...}

  • 用于控制HTML元素的显示和隐藏
<script lang="ts">
  let isShow = $state(false)
</script>

{#if isShow}
  <p>Show</p>
{/if}
{#else}
  <p>Hide</p>
{/else}

<button onclick={() => isShow = !isShow}>Control</button>

image.png

从Dom结构中可以看到,条件表达式控制是直接改变Dom结构的,并不是简单的display: none隐藏。

遍历表达式 {#each ...}

用于遍历数据,对象,Set和Mac并生成相应的HTML结构

  • {#each expression as [itemName, index] (key)}...{/each}
<script lang="ts">
  const list: {key: string, value: string}[] = [
    {key: "key1", value: "value1"},
    {key: "key2", value: "value2"},
    {key: "key3", value: "value3"},
    {key: "key4", value: "value4"},
    {key: "key5", value: "value5"},
  ]
</script>

{#each list as item (item.key)}
  <p>{item.value}</p>
{/each}

image.png

{#key ...}

{#key}的作用时:当Key值改变时,表达式中的内容会销毁并重新创建。如果表达式中内容为组件, 则组件会被重新实例化

<script lang="ts">
  let num = $state(0)
</script>
<!-- 当num的值发生变化时,表达式中的html将会销毁并重新创建 -->
{#key num}
  <p>{num}</p>
{/key}
<button onclick={() => num++} >Add</button>

{#await ...}

  • 用于处理Promise异步执行的三个状态

  • 表达式:{#await expression}...{:then name}...{:catch name}...{/await}

<script lang="ts">
  function test() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("Success")
      }, 2000)
    })
  }

  const promise = test()

</script>

{#await promise}
	<p>Loading...</p>
{:then value}
	<p>Result: {value}</p>
{:catch err}
	<p>Error: {err}</p>
{/await}
  • 也可以使用以下表达式

    • {#await expression then name} ... {/await}

    • {#await expression catch name} ... {/await}

片段 {#snippet}

#snippet 提供了一个可以在HTML中创建代码片段的方法。

  • 用法: {#snippet name([param1, param2, ... , paramN])}

例如有一个循环生成的HTML组件:

<script lang="ts">
  const list: {key: string, value: string}[] = [
    {key: "key1", value: "value1"},
    {key: "key2", value: "value2"},
    {key: "key3", value: "value3"},
    {key: "key4", value: "value4"},
    {key: "key5", value: "value5"},
  ]
</script>

{#each list as item, index (item.key)}
  {#if index % 2 === 0}
    <p>Even: {item.value}</p>
  {:else}
    <p>Odd: {item.value}</p>
  {/if}
{/each}

利用snippet可以将循环体内的代码抽离出来:

<script lang="ts">
  const list: {key: string, value: string}[] = [
    {key: "key1", value: "value1"},
    {key: "key2", value: "value2"},
    {key: "key3", value: "value3"},
    {key: "key4", value: "value4"},
    {key: "key5", value: "value5"},
  ]
</script>

{#snippet ItemSnip(value: string, index: number)}
  {#if index % 2 === 0}
    <p>Even: {value}</p>
  {:else}
    <p>Odd: {value}</p>
  {/if}
{/snippet}

{#each list as item, index (item.key)}
  {@render ItemSnip(item.value, index)}
{/each}

其中,@render表达式用于渲染输入的snippet组件,下面会介绍该表达式。

将HTML代码抽离成片段,这也意味着,我们可以导出和传入代码片段

导出snippet:

<script lang="ts">
  export {Test}
</script>

{#snippet Test(a: number, b: number)}
  <p>{a * b}</p>
{/snippet}

导入snippet:

<script lang="ts">
  import type { Snippet } from 'svelte';
  const { Test } : {
    Test: Snippet<[number, number]>
  } = $props()
</script>

{@render Test(1, 2)}

{@render}

snippet中已经初步介绍了**@render**, 他的唯一作用是用于渲染snippet

  • 表达式 {@render ...}

{@html}

  • 用于注入html,表达式 {@html content}
<script lang="ts">
  const content = "<h1>Hello World</h1>"
</script>

{@html content}

{@const ...}

  • 用于定义常量,表达式 {@const x = y}

{@debug ... }

  • 类似于console.log,用于在控制台中打印指定数据

bind

vue类似,bind表达式用于改变数据流动的方式。通常来说数据流动时单向的,从父节点到子节点。通过bind表达式,我们可以实现数据从子节点流向父节点。例如:

<script lang="ts">
  let val = $state("")
</script>

<!-- 数据的单向流动,输入的值并不会传到val变量中 -->
<input value={val} />

<!-- input输入的值会动态改变val变量的值 -->
<input bind:value={val} />

具体可用的bind:value的场景,可以参考官网

除此之外,bind还可以与$bindable结合用在父子组件的传参当中。

<!-- 子组件 Test.svelte -->
<script lang="ts">
let { val = $bindable() }: {val: number} = $props()
</script>

<input type="number" bind:value={num} />


<!-- 父组件 -->
<script lang="ts">
import Test from 'Test.svelte'
let parentVal = $state(0)
</script>

<Text bind:val={parentVal} />
<p>Parent Value: {parentVal}</p>

image.png

use

use:xxx可以指定一个Action在一个组件渲染之后执行。

<script lang="ts">
  import type { Action } from 'svelte/action'

  let text = $state("Waiting");
  const myAction: Action<HTMLElement, {tmp: string}> = (node, data) => {
    text = data.tmp
  }
</script>

<div use:myAction={{tmp: "Mounted"}}>
  {text}
</div>

过渡动画

svelte还提供了一些表达式来使HTML元素拥有简单的过度动画:

  • transition, 配合svelte/transition库使用,可以让HTML Element在显示/隐藏时加入动画。
<script lang="ts">
  import { fade } from 'svelte/transition'
  let isShow = $state(false)
</script>

<button onclick={() => isShow = !isShow}>{ isShow ? "Hide" : "Show" }</button>

{#if isShow}
<div transition:fade>Hello World</div>
{/if}
  • inout 用于分别控制显示/隐藏的动画
<script lang="ts">
  import { fade, fly } from 'svelte/transition'
  let isShow = $state(false)
</script>

<button onclick={() => isShow = !isShow}>{ isShow ? "Hide" : "Show" }</button>

{#if isShow}
<div in:fade out:fly={{y: 200}}>Hello World</div>
{/if}
  • animate

参考svelte/animate

特殊元素

svelte提供了一些特殊的元素来处理特定的情况, 例如处理一些边界情况,绑定window事件等

<svelte:boundary>

<svelte:boundary> 允许我们处理一些错误的边界情况,防止整个程序崩溃。

<script lang="ts">
  import { onMount } from 'svelte'
  const resetFunc = $state()
  function handleError(error: string, reset: Function) {
    console.log(error)
    resetFunc = reset
  }

</script>

<svelte:boundary onerror={handleError}>
  <p> Oops, Something went wrong. </p>
</svelte:boundary>

{#if resetFunc !== null}
  <button onclick={resetFunc}>Reset</button>
{/if}

<svelte:window>

该标签允许开发者在window中添加各种事件,例如: onscroll, onkeydown, onclick,该标签只能定义一个。

<script lang="ts">
  function onkeydown(e:KeyboardEvent) {
    console.log(e.keyCode)
  }
</script>
  
<svelte:window {onkeydown}></svelte:window>

<svelte:document>, <svelte:body> 和 <svelte:head>

同理,<svelte:document>, \<svelte:body\> 允许开发者在对应的中添加各种事件,例如visibilitychange, 与<svelte:window>相同,这些标签在项目中只能定义一个。

document可添加事件:

  • activeElement

  • fullscreenElement

  • pointerLockElement

  • visibilityState

其中,<svelte:head> 用于定于网页header信息,例如title, description

<svelte:element>

<svelte:element>用于渲染开发者在不同场景下不确定的HTML标签,例如

<script lang="ts">
  let isH1 = $state(true)
  let current = $derived(isH1 ? "h1" : "hr")
</script>

<svelte:element this={current}>
  This is a text.
</svelte:element>

<button onclick={() => isH1 = !isH1}>change</button>

image.png

image.png

<svelte:option>

<svelte:option>提供一个位置来改变每个组件的配置,例如是否允许runes, 定于当前组件的namespacecustomElement等。

具体可能的option有:

  • <svelte:option runes={true}>, 是否允许当前组件使用runes

  • <svelte:option namespace="test">

  • <svelte:option customElement="my-element">

  • <svelte:option css="inject">, 启用改选项会将所有css以内联的方式写入

Runtime

Store

Svelte中集成了一个Store对象,用于将声明的动态数据在跨组件之间传输。(与Vuex, Pinia, Redux类似)

  • 完整文档: 完整的用法可以参考官方文档

  • Writable: 用于声明一个可以在外部组件改变数据的store

    writable 有三个方法,分别是set, update, 和subscribe

    • set 用于直接设置store的数据

        import { writable } from 'svelte/store'
        const test = writable(1);
        set.set(2);
      
    • update 同样是设置store的方法

        import { writable } from 'svelte/store'
        const test = writable(1);
        test.update((n) => n + 1);
      
    • subscribe 当数据改变时,会触发订阅的方法

        import { writable } from 'svelte/store'
        const test = writable(1, () => {
          console.log(`this is a subscribe logic.`)
        });
      
        test.subscribe((n) => {
          console.log(`subscribe: ${n}`)
        })
      
        test.update((n) => n + 1);
      
// store.ts
import { writable } from 'svelte/store'

export const testData = writable("Test data")
<!-- Test.svelte -->
<script lang="ts">
import { testData } from './store.ts'
</script>

<button onclick={() => testData.set("Changed")}>Change</button>
<!-- Main.svelte -->
<script lang="ts">
import { testData } from './store.ts'
</script>

<p>{$testData}</p>
  • Readable 创建一个外部组件不可更改的store

  • derivedrunes中的$derived类似,传入一个store,并返回一个新的store

import { writable, derived } from 'svelte/store'

export const test = writable(0)

export const testDerived = derived(test, ($test) => {
  return $test * 2
})
  • readonly 传入一个store,返回一个只读的store,但原store不会被改变
import { writable, readonly } from 'svelte/store'

const testWrite = writable("Hello");
const readonlyTest = readonly(testWrite);

testWrite.set("Test Changed"); // success
readonlyTest.set("TestChanged"); // error
  • get 用于获取store的值,也可以用$store代替
import { get, writable } 'svelte/store'

const data = writable(0)

console.log(data) // object

console.log($data) // 0

console.log(get(data)) // 0

生命周期 hooks

svelte组件的生命周期钩子函数只有两部分:创建销毁

这是因为svelte认为state的更新与全局组件无关,因此组件的生命周期没有before updateafter update的部分

Everything in-between — when certain state is updated — is not related to the component as a whole; only the parts that need to react to the state change are notified. This is because under the hood the smallest unit of change is actually not a component, it’s the (render) effects that the component sets up upon component initialization. Consequently, there’s no such thing as a “before update”/"after update” hook.

  • onMount 组件渲染后调用的钩子函数
<script lang="ts">
import { onMount } from 'svelte'

onMount(() => {
  console.log("The component is mounted!");
})
</script>
  • onDestory 组件在销毁前调用的钩子函数
<script lang="ts">
import { onDestory } from 'svelte'

onDestory(() => {
  console.log("The component is being destry");
})
</script>
  • tick 它返回一个Promise,当任何state被改变后触发的resolve,或在下一个微任务触发resolvetick可以替代afterUpdate钩子函数
<script lang="ts">
import { tick } from 'svelte'

	$effect.pre(() => {
		console.log('组件更新');
		tick().then(() => {
				console.log('组件更新了');
		});
	});

</script>

3.小结

  • 本文简单介绍了svelte的基本原理:它是一个前端编译器,将前端的代码编译成无运行时优化DOM操作的前端项目。

  • svelte某种程度上为我们提供了一种新的前端工程化的解决方案,但是与目前成熟的框架相比,它在国内的社区支持度仍然不足。因此,学习和开发的成本可能会偏大。

  • 对于svelte的应用场景,除了高性能的单页应用开发,它也能为嵌入式前端开发提供方案。

  • 最后介绍了svelte的基本语法。

参考

1. Svelte