写在前面#
跟着官方的互动教程在学,然后记录一下关键的知识点防止学完就忘记了。示例直接复制的官方代码。
过完教程就要来重写我的个人主页。
本文章只涉及基础的 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 时过渡动画是有打断动画的,这个好评。如果指定 in 和 out,即进入和退出为不同的动画,那么就没有打断动画了:
<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}
