类比 Vue3 学习 Svelte


前言

我对 Svelte 这个前端框架早有耳闻,但是之前一直没有上手学习过它。最近我花了点时间看了看官方文档,浅浅地了解了这个号称 “无虚拟 DOM 且非常快速的框架”。接下来我通过将类比 Vue 来介绍 Svelte 的基础语法和功能。

Svelte封面

组件的基本结构

在 Vue3 中,一个组件的基本结构如下所示:

<script lang="ts" setup>
  /* 这是脚本部分 */
</script>

<template>
  <p>这是模板部分</p>
</template>

<style scope>
  /* 这是样式部分 */
</style>

在 Svelte 中,组件的结构也差不多如此,只不过没有 <template>,而是更加接近原生 .html 文件:

<script lang="ts">
  /* 这是脚本部分 */
</script>

<style>
  /* 这是样式部分 */
</style>

<p>这是模板部分</p>

组件的模板部分并不需要在类似于 <template> 之类的标签中定义。

Svelte 的样式是默认隔离的(跟 Vue 中的 style scope 类似),如果需要让样式影响子组件,则需要使用 :global() 选择器:

<style>
  p :global(div) {
    /* 需要共享的样式 */
  }
</style>

Svelte 语法

模板

Svelte 使用诸如 {#if condition} 之类的控制流语句来实现条件渲染和列表渲染等特性,跟 Vue 的指令有一定的区别,只不过也并不算晦涩。

文本插值

跟 Vue 中的 {{}} 类似,Svelte中通过 {} 来在模板中插入变量:

<script>
  let count = 20;
</script>

<p>Count is {count}</p>

等同于 Vue 中的:

<script setup>
  let count = 20;
</script>

<template>
  <p>Count is {{count}}</p>
</template>

比较特别的是,Svelte 的文本插值也能用在属性中,例如:

<script>
  const url = 'www.example.com';
</script>

<a src='https://{url}'>例子</a>

这里的 src 属性会被拼接为 https://www.example.com

条件渲染

在 Vue 中,我们通过 v-ifv-else-ifv-else 指令来实现条件渲染,而在 Svelte 中,则使用 ${#if}...{:else if}...{:else}...{/if} 来实现:

<script>
  let count = 20;
</script>

{#if count < 10}
  <p>Count 小于 10</p>
{:else if count === 10}
  <p>Count 等于 10</p>
{:else}
  <p>Count 大于 10</p>
{/if}

等同于 Vue 中的:

<script setup>
  let count = 20;
</script>

<template>
  <p v-if="count < 10">Count 小于 10</p>
  <p v-else-if="count === 10">Count 等于 10</p>
  <p v-else >Count 大于 10</p>
</template>

列表渲染

在 Vue 中,我们使用 v-for 指令来实现列表渲染,而在 Svelte 中,可以使用 {#each}...{/each} 流程控制语句来实现:

<script>
  const list = [1, 2, 3, 4];
</script>

{#each list as item, index (index)}
  <p>第 {index} 个元素的内容是 {item}</p>
{/each}

{#each} 语句的语法是 {#each 列表 as 元素, 元素索引 (元素的 key)},这跟 Vue 中 v-for="元素, 元素索引 in 列表" 的语法有一定区别。

以上代码等同于 Vue 中的:

<script setup>
  const list = [1, 2, 3, 4];
</script>

<template>
    <p v-for="item, index in list" :key="index">
        第 {{ index }} 个元素的内容是 {{ item }}
    </p>
</template>

等待块

Svelte 的这个特性比较特别,在 Vue 中并没有完全与之功能相等的特性。{#await}...{:then}...{:catch}...{/await} 允许你去等待一个异步操作完成,并在等待、成功和失败的时候渲染不同的 DOM:

<script>
  const promise = Promise.resolve(666);
</script>

{#await promise}
  <p>等待中...</p>
{:then number}
  <p>数字是:{number}</p>
{:catch error}
  <p>发生错误:{error}</p>
{/await}

可以看到 promise 中的数据是可以传递到 :then 块中的,而捕获到的错误又可以传递到 :catch 块中。

当然,如果不需要处理 等待中 的状态,也可以直接写成:

<script>
  const promise = Promise.resolve(666);
</script>

{#await promise then number}
  <p>数字是:{number}</p>
{:catch error}
  <p>发生错误:{error}</p>
{/await}

在 Vue 中我们可以使用 <Suspense> 来实现类似的操作,但是不如这个方便,尤其是在处理异常的时候。

可复用代码片段

Svelte 允许我们使用 {#snippet fn(...args)}...{/snippet} 定义一个可以被重复使用的代码片段,并允许我们使用 {@render fn(...args)} 来在任意位置渲染这段代码。其中 fn() 是这个代码片段的名字,而 args 则是代码片段渲染时可传入的参数:

<script></script>

{#snippet man(name, age)}
  <p>名字:{name} | 年龄:{age}</p>
{/#snippet}

<div>
  {@render man('mike', 20)}
  {@render man('john', 23)}
  {@render man('soda', 10)}
</div>

Svelte 用它来实现类似于 Vue 的插槽机制:通过在参数中传入代码片段,组件会将传入的代码片段渲染在指定的地方。

<MyComponents slot1={man} />

组件的子组件将被自动收集为一个名为 chilren 的代码片段:

//--- main.svelte.js ---//
<script>
  import Component from './Component.svelte.js';
</script>

<div>
  <Component>
    <div>我是 children 代码片段</div>
  </Component>
</div>

//--- Component.svelte.js ---//
<script>
  const { children } = $props();  // 从参数中获取children
</script>

<div>
  <!-- 渲染 children -->
  {@render children()}
</div>

单向数据绑定

在 Vue 中,使用 :prop="value" 前缀来绑定数据,用 @event="fn" 前缀来绑定方法。而在 Svelte 中,使用 prop={value} 来绑定属性和组件的方法属性,使用 onevent={fn} 来绑定 DOM 操作:

<script>
  import MyComponent from './MyCompoent.svelte.js';
  let value = 20;
</script>

<MyComponent 
  value={value} 
  onclick={() => alert('click')}
/>

以上代码同于 Vue 中的:

<script setup>
  import MyComponent from './MyCompoent.vue';
  let value = 20;
</script>

<template>	
  <MyComponent 
    :value="value"
    @click="() => alert('click')"
  />
</template>

如果变量名与属性名相同,也可以直接把 value={value} 简写为 {value}

如果所有的数据都在一个对象内,且对象属性的名字与组件属性名字一一对应,则可以直接使用展开运算符 ... 直接装入全部属性,这称为事件传播

<script>
  import MyComponent from './MyCompoent.svelte.js';
  const props = {
    value: 20,
    onclick: () => alert('click')
  };
</script>

<MyComponent {...props} />

双向绑定

在 Vue 中,我们通过 v-model="value"v-model:target="value" 来实现对数据的双向绑定,而在 Svelte 中,我们可以通过 bind:target={value} 来实现双向绑定:

<script>
  let value = $state(0); // 创建一个响应式数据
</script>

<form>
  <input bind:value={value} placeholder="请输入..." /> 
</form>

这等同于 Vue 中的:

<script setup>
  const value = ref(0);
</script>

<template>	
  <form>
    <input v-model="value" placeholder="请输入..." />
  </form>
</template>

通过 bind: 当然也可以绑定其他属性,甚至我们可以绑定 DOM 的属性(比如 clientWidth 之类的)。而在 Vue 中v-model 受到的限制就比较多。

对于使用了 contenteditable 的元素,我们可以直接绑定它的 innerHTML 属性:

<p 
   contenteditable
   bind:innerHTML={content}
>
  这是内容
</p>

在 Vue 中,我们可以给模板的 class 属性传入一个字符串、一个数组或是一个 Record<string, boolean> 类型的对象来控制元素具有哪些类,而在 Svelte 中同样如此:

<p class="my-class-1 my-class-2">
  字符串
</p>

<p class={['my-class-1', { 'my-class-2': true }]}>
  带有控制对象的数组
</p>

<p class={{
  'my-class-1': true,
  'my-class-2': false
}}>
  控制对象
</p>

这等同于 Vue 中的:

<template>
  <p class="my-class-1 my-class-2">
    字符串
  </p>

  <p :class="['my-class-1', { 'my-class-2': true }]">
    带有控制对象的数组
  </p>

  <p :class="{
    'my-class-1': true,
    'my-class-2': false
  }">
    控制对象
  </p>
</template>

基本上完全一致。

指令

Svelte 中提供了简单的指令系统。没有太多的钩子,Svelte 的指令是一个普通函数,其函数签名很简单:第一个参数为 node —— 即绑定的元素 DOM;第二个参数为 args —— 即 use:directive={args} 中传入的参数 args。使用 Svelte 指令可以对目标 DOM 元素做指定处理。

由于 DOM 需要在挂载阶段才能操作,因此指令函数中具体的绑定操作需要在 $effect() 方法(类似于 Vue 的 onMounted())中执行:

/// action.svelte.js
export function myDirective(node, args) {
  // 定义一些函数...
  $effect(() => {
  	// 具体的 DOM 操作
  });
}

然后我们在模板中可以使用 use: 来对元素应用指令:

<script>
  import myDirective from './action.svelte.js';
</script>

<p use:myDirective={/*...*/}>
  使用指令
</p>

这点与 Vue 中的 v-directive="/* ... */" 有异曲同工之妙。

样式指令

Svelte 允许我们直接通过指令的形式修改元素的样式属性,这也是 Svelte 的特点之一:

<script>
  const width = 20;
</script>

<button
  style:color="red"
  style:--width="{width}rem"
>
  Click
</button>

这部分的模板相当于:

<button
  style="color: red; --width: {width}rem;"
>
  Click
</button>

通过 style:prop="value" 的形式,可以直接修改元素的样式。

Svelte 允许我们用一种简单的方法修改组件的 CSS 变量,那就是直接用 --prop="value" 的语法进行修改:

<script>
  import MyComponent from './MyComponent.svelte.js';
  const width = 20;
</script>

<MyComponent --width="{width}rem" />

过渡指令

在 Vue 中,我们可以使用 <transition><transition-group> 组件对元素的创建销毁和变化添加过渡动画,而在 Svelte 中,我们可以使用 transition: 指令来指定元素的过渡效果:

<script>
  import { fade } from 'svelte/transition';
</script>

<p transition:fade>
  Fades in and out
</p>

p 元素被添加或删除的时候,会产生对应的 fade 动画。

对于有参数的动画,我们也可以在后面添加参数:

<script>
  import { fly } from 'svelte/transition';
</script>

<p transition:fly={{ y: 200, duration: 2000 }}>
  Flies in and out
</p>

如果你想要出入的动画不一样,可以用 in:out: 指令代替 transition: 指令:

<script>
  import { fade, fly } from 'svelte/transition';
</script>

<p 
  in:fly={{ y: 200, duration: 2000 }} 
  out:fade
>
  Flies in and out
</p>

API

响应式数据

Svelte 中可以使用 $state 函数来创建一个响应式变量:

/// 原始值
let count = $state(0);
count = 20;
count++;  // 可以直接赋值修改

/// 对象
let obj = $state({ a: 20 });
obj.a = 30;

类似于 Vue 中 refreactive 的结合体:

/// 原始值
let count = ref(0);
count.value = 20;
count.value++;

/// 对象
let obj = reactive({ a: 20 });
obj.a = 30;

由于 Svelte 的响应式数据都是属于编译时响应式的,并不依赖于 Proxy API(Vue 使用的),因此我们是能够直接在模板中直接修改变量值的。在编译的时候编译器会自动识别对响应式数据做的操作,并生成对应的更新操作。

如果数据是一个对象,$state 会将对象内所有的数据全部转化为响应式数据。如果我们只想创建不想这样子,可以使用 $state.raw 来创建(类似于 Vue 中的 shallowRef ):

let obj = $state.raw({ a: 20 });
obj.a = 30;			// 不触发更新
obj = { a: 30 };	// 会触发更新

类似于 Vue 中的 :ref="el" ,我们可以通过 bind:this={el} 来获取到元素的 DOM,并赋予 el

<script>
  let el = $state(null); // 创建一个响应式变量
  
  // $effect 类似于 onMounted
  $effect(() => {
    // 操作 DOM
  });
</script>

<p bind:this={el}>获取我</p>

计算属性

在 Svelte 中我们可以用 $derived 来定义一个计算属性:

const a = $state(0), b = $state(0);
const sum = $derived(() => a + b);

类似于 Vue 中的 computed

const a = ref(0), b = ref(0);
const sum = computed(() => a.value + b.value);

打印响应式数据

比较有意思的是,Svelte 中的响应式数据都是 不可复制的,这就以为这它无法被 console.log 之类的调试函数直接打印。

Svelte 为我们提供了一个专门用于打印响应式变量的 API —— $state.snapshot$inspect

let count = $state(0);

console.log($state.snapshot(count));	// => 0

$inspect(count);	// => init 0
count = 1;			// => update 1

// 指定用什么函数打印
$inspect(count).with(console.error);	// > init(0)	> ...

可见 $inspect 在某些情况下可能更适合追踪调试数据。

副作用函数

有点类似于 Vue 的 watch ,Svelte 的 $effect 允许我们创建内容发生变化时重新调用的函数,只不过 Svelte 官方并不推荐经常用它,称它为 “最后的手段” 😂。

let elapsed = $state(0);
let interval = $state(1000);

$effect(() => {
  const timer = setInterval(() => {
    elapsed += 1;
  }, interval);
  
  return () => clearInterval(timer);
});

跟 React 中的 useEffect 一样,$effect 返回一个 cleanup 函数,当 $effect重新调用(一般发生在组件重渲染的时候)时会调用这个函数以清楚上次调用产生的副作用。

$effect 将会在元素挂载的时候调用,因此它可以在脱围机制中起到类似于 Vue 中 onMounted 函数的作用(见上文 指令 一节)。

共享响应式变量

类似于 Vue 的 refreactive,Svelte 中的响应式变量也是可以专门放在一个文件里导出,然后被多个组件共享的,只不过这必须要求导出的数据是一个响应式对象,并且存放响应式变量的文件名必须以 .svelte.{ts|js} 结尾:

// data.svelte.js
export const counter = $state({
  value: 20
});

总结

不得不说 Svelte 的一些语法构想确实是不错的,但是并没有给我一种想要转向他的冲动。 相比于 Svelte,我会更加愿意使用 Vue 和 React 这些的大型主流框架来进行我的项目开发。至于静态网页,我就用 AstroJS 啦,SSG 还是很不错的!

这篇文章我通过类比 Vue 介绍了 Svelte 的基本语法和 API。希望看完对你也能够有一些帮助。😄