Introduction of Svelte
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后缀的文件是用于构建 Svelte 应用的组件。与.vue文件类似,.svelte文件包括三部分:HTML, style, script, 其中 style 和 script 是可选的。
<script module lang="ts">
</script>
<script lang="ts">
</script>
<style>
// Style
</style>
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

我们也可以使用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>
如果我们不想在 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进行深度监听数据时,返回的是一个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 可以捕捉到一些基本的数据类型的变化并返回,如果要进行复杂的数据处理,需要用到$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>

$effect
$effect 函数会追踪那些state和derived发生变化,然后重新执行对应的函数。(类似于 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>

可以看到当点击按钮 state 发生变化后,先前声明的 effect 函数会进行调用。
需要注意的是,$effect 中只能监听函数内同步读取的依赖,异步读取的数据不会被追踪,例如 await 和 setTimeout 中的数据
<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>

顾名思义,$effect.pre会在 DOM 更新之前调用该 effect 函数。
方法会返回一个布尔值, 用于查看当前组件是否存在追踪上下文
<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函数,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
- 与
Vue 和 React 类似,$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
<script lang="ts">
let count = $state(0)
let num = $derived(count * 2)
$inspect(num)
</script>
<button onclick={() => count++} >Add</button>

$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>

$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>
需要注意的是:
export default defineConfig({
plugins: [
svelte({
compilerOptions: {
customElement: true,
},
}),
],
});
Template 表达式
Template 表达式主要用于在.svelte文件中编写动态的HTML。
条件表达式 {#if …}
<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>

从 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}

{#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 …}
<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}
片段 {#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
{@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>

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 元素拥有简单的过度动画:
<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}
<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}
参考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>


<svelte:option>
<svelte:option>提供一个位置来改变每个组件的配置,例如是否允许runes, 定于当前组件的namespace和customElement等。
具体可能的 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);
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>
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");
readonlyTest.set("TestChanged");
get 用于获取 store 的值,也可以用$store代替
import { get, writable } 'svelte/store'
const data = writable(0)
console.log(data)
console.log($data)
console.log(get(data))
生命周期 hooks
svelte组件的生命周期钩子函数只有两部分:创建和销毁。
这是因为svelte认为state的更新与全局组件无关,因此组件的生命周期没有before update和after 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.
<script lang="ts">
import { onMount } from 'svelte'
onMount(() => {
console.log("The component is mounted!");
})
</script>
<script lang="ts">
import { onDestory } from 'svelte'
onDestory(() => {
console.log("The component is being destry");
})
</script>
tick 它返回一个 Promise,当任何 state 被改变后触发的resolve,或在下一个微任务触发resolve。tick可以替代afterUpdate钩子函数
<script lang="ts">
import { tick } from 'svelte'
$effect.pre(() => {
console.log('组件更新');
tick().then(() => {
console.log('组件更新了');
});
});
</script>