跳过正文

从头开始学 Svelte 5

·1111 字·6 分钟·
Kre³
作者
Kre³
Doing code and art with ❤
目录

写在前面
#

跟着官方的互动教程在学,然后记录一下关键的知识点防止学完就忘记了。示例直接复制的官方代码。

过完教程就要来重写我的个人主页。

本文章只涉及基础的 Svelte 5,高级用法等我边写边发掘(但是我觉得好像不咋用得到)。

学完才发现我之前写 React 过的都是些什么苦日子


基础格式
#

官方给的示例是先 <script>,后<html>,最后放<style>

  • 语法糖 #1:如果你的变量名和想要传参的名一致,可以直接传这个变量而不需要声明参数名(比如 <img>src,如果你有个变量叫 src 并且同时存储了资源地址,在传给 <img> 的时候无需写 src={src} 而是可以直接传 {src}

Rune && State
#

$state
#

作用是声明这个变量是可变的状态,让 Svelte 能够管理变量状态和组件更新。底层原理为 js 的 Proxy。

$derived
#

也是声明可变变量,只是它是由 $state 计算过来的,作用是方便管理调用链,并且 $derived 也能做缓存(在原始 $state 没有更改的情况下)

$state.snapshot$inspect
#

$state.snapshot 是从 $state 获取原始值,并且传给其他非 Svelte 组件,并且只是当前状态。

如果你想用 console.log,官方给你封装好了一个 $inspect,相当于帮你 snapshot 后再 console.log,也可以指定函数。更方便的是在 production build 中这些代码会自动清除。

<script>
	let numbers = $state([1, 2, 3, 4])
	let total = $derived(numbers.reduce((t, n) => t + n, 0))

	function addNumber() {
		numbers.push(numbers.length + 1)
		console.log($state.snapshot(numbers)) // Look this
	}

  // OR
  $inspect(numbers).with(console.trace)
</script>

$effect
#

自己写状态更新逻辑。如果官方的 DOM event 无法满足的情况下最后使用这个方法。直接修改 $state 或者引用的 $state 有修改,Svelte 会帮你监测更改并更新组件/自动运行 $effect

Return() 可以帮你做一些清理工作,会在这个 effect 再次执行前执行。

如果这个 effect 不依赖任何 $state,将只会在这个组件加载的时候运行一次。

$effect 不能在 server side rendering 中运行。

<script>
	let elapsed = $state(0)
	let interval = $state(1000)
  $effect(() => {  // 如果 interval 被修改会触发
	const id = setInterval(() => {
		elapsed += 1
	}, interval)

	return () => {  // 在这个 effect 重新运行前会执行,为了清理不需要的 setInterval
		clearInterval(id)
	}
})
</script>

<button onclick={() => interval /= 2}>speed up</button>
<button onclick={() => interval *= 2}>slow down</button>

<p>elapsed: {elapsed}</p>

$state outside Components
#

你可以把 $state 放在外部文件作为一个共享状态。注意的是 export 的变量必须为 const (即 immutable),并且修改时不能直接替换整个变量。能够修改这个 const Object中的内容物。

同时注意这个 js/ts 文件必须声明为 Svelte 内容物。

shared.svelte.js

export const counter = $state({ // 这里使用了 state
	count: 0
})

Button.svelte

<script>
	import { counter } from './shared.svelte.js';
</script>

<button onclick={() => counter.count += 1}>
	clicks: {counter.count}
</button>

$props
#

在子组件中声明传参。可以传很多个参。

可以解压并且设置默认值。

Child.svelte

<script>
  let {answer, writer = 'kyree'} = $props() // 设置 writer 的默认值为 'Kyree'
</script>

<p>The answer is {answer}, written by {writer}.</p>

App.svelte

<script>
	import Nested from './Child.svelte'
</script>

<Child answer={42}/>

如果你很懒也可以把一个对应好的 Object 解压后全部一股脑传给子组件。

<script>
	import PackageInfo from './PackageInfo.svelte';

	const pkg = {
		name: 'svelte',
		version: 5,
		description: 'blazing fast',
		website: 'https://svelte.dev'
	};
</script>

<PackageInfo
 {...pkg}
/>

Template
#

if && else if && else
#

{#if count > 10}
	<p>{count} is greater than 10</p>
{:else if count < 5}
	<p>{count} is less than 5</p>
{:else}
	<p>{count} is between 5 and 10</p>
{/if}

each
#

其中第二个参数 i 可以作为 index。

{#each colors as color, i (color.id)}
  <button
    style="background: {color}"
    aria-label={color}
    aria-current={selected === color}
    onclick={() => selected = color}
  >{i + 1}</button>
{/each}

最好给每个生成的元素赋予唯一 id(即在 each template 最后使用括号包含你想作为 id 的变量)。注意不要使用自增变量 i 作为 id,因为这和没有使用没有任何区别。

(最好是对于会产生变化的列表都赋予 id,能够避免很多非预期行为)

await
#

可以在等待 Promise 时做一些操作

{#await promise}
	<p>...rolling</p>
{:then number}
	<p>you rolled a {number}!</p>
{:catch error}
	<p style="color: red">{error.message}</p>
{/await}

await 后面的 promise 存获取到的 Promise,then 里面的 number 存放 Promise fulfilled 的结果。如果你不想要等待元素,可以直接精简:

{#await promise then number}
	<p>you rolled a {number}!</p>
{/await}

DOM event
#

可以这样简写直接绑定:on<name>

<div onpointermove={onpointermove}>
	The pointer is at {Math.round(m.x)} x {Math.round(m.y)}
</div>

或者更暴力可以直接 inline:

<div
	onpointermove={(event) => {
		m.x = event.clientX;
		m.y = event.clientY;
	}}
>
	The pointer is at {m.x} x {m.y}
</div>

DOM event capture && bubbling
#

在 HTML 的层级中,DOM 的事件是先在 Capture 阶段从外到内传递事件,到达触发的节点后,再在 Bubbling 阶段把事件从内往外传递。Svelte 默认行为是监听 Bubbling 阶段,如果需要指定监听 Capture 阶段,需要加关键字 on<name>capture

<div onkeydowncapture={(e) => alert(`<div> ${e.key}`)} role="presentation">
	<input onkeydowncapture={(e) => alert(`<input> ${e.key}`)} />
</div>

如果混合了 Capture 阶段和 Bubbling 阶段,根据之前阐述的 DOM 事件传递顺序,Capture 组的事件永远会提前发生。

DOM event passing && spreading
#

可以将 Function 当作参数传给子组件,如果你传了个叫 onclick 的函数,在子组件中甚至可以直接从 props 中解压出来就可以用:

Stepper.svelte

<script>
	let { increment, decrement } = $props();
</script>

<button onclick={decrement}>-1</button>
<button onclick={increment}>+1</button>

App.svelte

<script>
	import Stepper from './Stepper.svelte';

	let value = $state(0);
</script>

<p>The current value is {value}</p>

<Stepper
	increment={() => value += 1}
	decrement={() => value -= 1}
/>

Binding
#

Svelte 中数据流向默认是从父组件通过 props / 直接设置子组件属性来向子组件传。如果在某些情况下子组件需要传数据给父组件(尤其是涉及到表单等 HTML 组件),则需要使用Binding:

<script>
	let name = $state('world');
</script>

<input bind:value={name} />

<h1>Hello {name}!</h1>

后记:如果你想要一个 $state 在父组件和子组件间相互绑定(因为默认情况下数据是单项传递的),你需要在两边都声明这个状态双向绑定:

// 在父组件中
<Child bind:index={currIndex}/>
// 在子组件中
let { index = $bindable() } = $props()

后记 #2:如果使用 $state outside components 的方法,就相当于一个简易的 Global state

input
#

这里 <input> 的 bind 是必须的,即使你使用了 $state outside components 这种方式。将 $state 和表单组件绑定的话,相当一个语法糖会自动监控更改,并且必要时回写给父组件的状态。

同时如果 <input> 的类型为 range/number 时,使用 bind:value 会自动帮你转换值为预期格式(数字)。

对于 checkbox 格式的 <input>,需要使用 bind:checked好坏啊),会自动帮你转换值为布尔值。

select
#

对于 <select>,又需要用回 bind:value,同时需要注意

  • 在示例中的 selected 变量(没有赋予初始值),虽然进行了 bind:value,但是 binding 只会在 <select> 组件加载后才会赋值。所以在 selected 没有赋予初始值、加载时还是 undefined 的情况下,不能直接使用,必须要判断一下这个变量是不是未初始化(或者在已知初始值的情况下直接提前赋予初始值);
  • 如果使用了 <select multiple>(即可以使用 ctrl 键选中多个),返回的 selected 会是一个数组;
  • 语法糖 #2:如果 <option> 的显示的值和 value 值一致,可以只传入显示值,Svelte 会自动帮你赋 value(示例中没有体现)。
<script>
	let questions = [
		{
			id: 1,
			text: `Where did you go to school?`
		},
		{
			id: 2,
			text: `What is your mother's name?`
		},
		{
			id: 3,
			text: `What is another personal fact that an attacker could easily find with Google?`
		}
	];

	let selected = $state();

	let answer = $state('');

	function handleSubmit(e) {
		e.preventDefault();

		alert(
			`answered question ${selected.id} (${selected.text}) with "${answer}"`
		);
	}
</script>

<h2>Insecurity questions</h2>

<form onsubmit={handleSubmit}>
	<select
		bind:value={selected}
		onchange={() => (answer = '')}
	>
		{#each questions as question}
			<option value={question}>
				{question.text}
			</option>
		{/each}
	</select>

	<input bind:value={answer} />

	<button disabled={!answer} type="submit">
		Submit
	</button>
</form>

<p>
	selected question {selected
		? selected.id
		: '[waiting...]'}
</p>

Group
#

如果你有用 each 生成多个单选项/多选项,可以使用 bind:group 把这些同组的分类在一起,并且返回到同一个传入的 $state 中。(对成组的多选很友好,多选的值能够以数组的形式进入 $state)。

{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
	<label>
		<input
			type="checkbox"
			name="flavours"
			value={flavour}
			bind:group={flavours}
		/>

		{flavour}
	</label>
{/each}

Classes && Styles
#

Class 能够使用可变变量来控制,也可以使用数组(利用自带的 clsx)来构建。

style: 也可以使用可变变量控制,也可以存在多个 style: 来美观。

<button
	class="card"
	style:transform={flipped ? 'rotateY(0)' : ''}
	style:--bg-1="palegoldenrod"
	style:--bg-2="black"
	style:--bg-3="goldenrod"
	onclick={() => flipped = !flipped}
>

推荐修改子组件的样式通过 CSS 变量:直接传递给子组件 --color="red",并在子组件中的样式中使用这个 CSS 变量:background-color: var(--color, #ddd);。Svelte 会自动生成一个 wrapper div 防止出现奇怪问题。


Attachments
#

有点看不懂,主要用在 SSR 中,可能作用是针对 SSR 生成的 DOM 进行更新。

之后用到了再回来。


Transitions
#

Use built-in
#

Svelte 内置了一些预设方便的过渡动画,比如这个简单的 fade 例子,从 svelte/transition 导入了 fade 后,直接在 <p> 里面声明 transition:fade,让它在加入和移除 DOM 的时候添加动画:

<script>
	import {fade} from 'svelte/transition'
	let visible = $state(true);
</script>

<label>
	<input type="checkbox" bind:checked={visible} />
	visible
</label>

{#if visible}
	<p transition:fade>
		Fades in and out
	</p>
{/if}

过渡动画可以设置表现:

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

使用 transition 时过渡动画是有打断动画的,这个好评。如果指定 inout,即进入和退出为不同的动画,那么就没有打断动画了:

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

Use my-own
#

你也可以自己写过渡动画。但是看了一下很复杂,就贴官方示例吧:

// css transision
function spin(node, { duration }) {
  return {
    duration,
    css: (t, u) => {
      const eased = elasticOut(t);

      return `
        transform: scale(${eased}) rotate(${eased * 1080}deg);
        color: hsl(
          ${Math.trunc(t * 360)},
          ${Math.min(100, 1000 * u)}%,
          ${Math.min(50, 500 * u)}%
        );`
    }
  };
}

// js transision
function typewriter(node, { speed = 1 }) {
	const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;

	if (!valid) {
		throw new Error(`This transition only works on elements with a single text node child`);
	}

	const text = node.textContent;
	const duration = text.length / (speed * 0.01);

	return {
		duration,
		tick: (t) => {
			const i = Math.trunc(text.length * t);
			node.textContent = text.slice(0, i);
		}
	};
}

Transition events
#

Svelte 还给你留了进入/退出动画的开始/结束 event handler,快说谢谢 Svelte:

<p
	transition:fly={{ y: 200, duration: 2000 }}
	onintrostart={() => status = 'intro started'}
	onoutrostart={() => status = 'outro started'}
	onintroend={() => status = 'intro ended'}
	onoutroend={() => status = 'outro ended'}
>
	Flies in and out
</p>

Global transitions
#

默认情况下,组件的过渡动画只在自身销毁/加载时会有,如果希望同等级组件在任意组件销毁/加载时一起动,那么就需要使用|global修饰符:transition:slide|global

Key blocks
#

Svelte 的动画只对组件的销毁/加载有作用。如果在某些时候组件只是内容有更改,但是我也想让它有动画,可以使用 {#key} 包裹起来,这样 Svelte 会监控比如例子中 i 的更改,并且自动帮你“销毁和重建”组件,并执行动画:

<script>
  // 这里的 i 也是状态
  // 没有贴完整的 typewriter 代码,只作最简单示例
  let i = $state(-1)
</script>

{#key i}
	<p in:typewriter={{ speed: 10 }}>
		{messages[i] || ''}
	</p>
{/key}