版本: Svelte:5.x [[toc]] Svelte 基础 介绍 使用{}嵌入JS表达式 <script> let src = '/tutorial/image.gif'; let name = 'Rick Astley'; </script> <p>Name: {name}</p> <img src={src}/> <!-- 语法糖 --> <img {src} /> 使用<style>加入样式 <p>This is a paragraph.</p> <style> p { color: goldenrod; font-size: 2em; } </style> 导入和使用组件 <script lang="ts"> import Nested from './Nested.svelte'; </script> <Nested /> 将字符串变成HTML代码 <p>{@html string}</p> 响应式 「状态」的创建和修改 $... 被称作Runes(符文) <script> let count = $state(0); function increment() { count += 1; } </script> 「深状态」 <script> let numbers = $state([1, 2, 3, 4]); function addNumber() { numbers.push(numbers.length + 1); } </script> 「派生状态」 <script> let numbers = $state([1, 2, 3, 4]); let total = $derived(numbers.reduce((t, n) => t + n, 0)); </script> 状态「快照」 <script> let numbers = $state([1, 2, 3, 4]); console.log($state.snapshot(numbers)); // 使用 $inspect 在状态每次变化时自动记录快照 $inspect(numbers).with(console.trace); </script> 「副作用」 <script> let elapsed = $state(0); let interval = $state(1000); $effect(() => { const id = setInterval(() => { elapsed += 1; }, interval); return () => clearInterval(id); }); </script> 在Svelte文件外使用「状态」 export const counter = $state({ count: 0 }); 组件的「属性」 声明「属性」 <script lang="ts"> let { answer } = $props(); </script> 属性的默认值 <script> let { answer = 'a mystery' } = $props(); </script> 传递属性 <PackageInfo name={pkg.name} version={pkg.version} description={pkg.description} website={pkg.website} /> <!-- 语法糖 --> <PackageInfo {...pkg} /> HTML中的「逻辑」 分支(#if, :else if, :else, /if) {#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 as) <div> {#each colors as color, i} <!-- i为可选 --> <button style="background: {color}" aria-label={color} >{i + 1}</button> {/each} </div> 带「键」的遍历 {#each things as thing (thing.id)} <Thing name={thing.name}/> {/each} 异步 {#await promise} <p>...rolling</p> {:then number} <p>you rolled a {number}!</p> {:catch error} <p style="color: red">{error.message}</p> {/await} <!-- 若promise不会被拒绝,catch可省略 --> <!-- 若不需要在promise完成前显示内容,可以简写 --> {#await promise then number} <p>you rolled a {number}!</p> {/await} 事件 监听事件 <!-- 语法:on<name> --> <div onpointermove={onpointermove} /> <!-- 语法糖 --> <div {onpointermove} /> <!-- 内联 --> <div onpointermove={(event) => { m.x = event.clientX; m.y = event.clientY; }} /> 使用「捕获」而非「冒泡」进行事件处理 <div onkeydowncapture={(e) => alert(`<div> ${e.key}`)} > <input onkeydowncapture={(e) => alert(`<input> ${e.key}`)} /> </div> 组件向外传递Event Handler <script> let props = $props(); </script> <button {...props}> Push </button> (双向)绑定 语法 <script> let value = $state('world'); let a = $state(0); let b = $state(0); </script> <input bind:value={value} /> <!-- 语法糖 --> <input bind:value /> <!-- 语法糖:a和b会被自动转换为number --> <input type="number" bind:value={a} /> <input type="range" bind:value={b} min="0" max="10" /> bind:group:单选/多选框 <script> let scoops = $state(1); let flavours = $state([]); </script> <!-- scoops为被选中的value --> {#each [1, 2, 3] as number} <label> <input type="radio" name="scoops" value={number} bind:group={scoops} /> {number} </label> {/each} <!-- flavours为被选中的value的数组 --> {#each ['a', 'b', 'c'] as flavour} <label> <input type="checkbox" name="flavours" value={flavour} bind:group={flavours} /> {flavour} </label> {/each} <select multiple> <select multiple bind:value={flavours}> {#each ['a', 'b', 'c'] as flavour} <option>{flavour}</option> {/each} </select> 类与样式 clsx支持 <button class={["card", { flipped }]} onclick={() => flipped = !flipped} > style: <button class="card" style:transform={flipped ? 'rotateY(0)' : ''} style:--bg-1="palegoldenrod" style:--bg-2="black" style:--bg-3="goldenrod" onclick={() => flipped = !flipped} > 在父组件中指定子组件样式 <!-- 子组件 Box --> <style> .box { background-color: var(--color, #ddd); } </style> <!-- 父组件 --> <div class="boxes"> <Box --color="red" /> <Box --color="green" /> <Box --color="blue" /> </div> Actions export function f(node) { // ... } export function g(node, param) { // ... } <!-- 元素挂载后,调用该action --> <div use:f use:g={/* 表达式 */}> 过渡动画 语法 <script> import { fade, fly } 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> <p transition:fly={{ y: 200, duration: 2000 }}> Flies in and out </p> <p in:fly={{ y: 200, duration: 2000 }} out:fade> Flies in, fades out </p> {/if} 自定义CSS过渡动画 <script> import { fade } from 'svelte/transition'; import { elasticOut } from 'svelte/easing'; let visible = $state(true); 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)}% );` } }; } </script> 自定义JS过渡动画 自定义JS过渡动画 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); } }; } 过渡动画的事件 <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> 全局过渡 默认情况下,只有元素内部的内容的增删会触发过渡 <div transition:slide|global> {item} </div> Key block 通过彻底销毁并重建内容来强制触发过渡动画 {#key i} <p in:typewriter={{ speed: 10 }}> {messages[i] || ''} </p> {/key} Svelte 进阶 响应式进阶 原始状态 特点:属性和内容的变化不会触发更新 let data = $state.raw(poll()); 响应式的类 class Box { width = $state(0); height = $state(0); area = $derived(this.width * this.height); constructor(width, height) { this.width = width; this.height = height; } embiggen(amount) { this.width += amount; this.height += amount; } } class Box { #width = $state(0); #height = $state(0); area = $derived(this.#width * this.#height); constructor(width, height) { this.#width = width; this.#height = height; } get width() { return this.#width; } get height() { return this.#height; } set width(value) { this.#width = Math.max(0, Math.min(MAX_SIZE, value)); } set height(value) { this.#height = Math.max(0, Math.min(MAX_SIZE, value)); } embiggen(amount) { this.width += amount; this.height += amount; } } 自带的响应式的类 支持Map, Set, Date, URL, URLSearchParams import { SvelteDate } from 'svelte/reactivity'; let date = new SvelteDate(); store 内容复用 #snippet snippet也可以作为属性传递给子组件 <table> <tbody> {#snippet monkey(emoji, description)} <tr> <td>{emoji}</td> <td>{description}</td> <td>\u{emoji.charCodeAt(0).toString(16)}\u{emoji.charCodeAt(1).toString(16)}</td> <td>&#{emoji.codePointAt(0)}</td> </tr> {/snippet} {@render monkey('🙈', 'see no evil')} {@render monkey('🙉', 'hear no evil')} {@render monkey('🙊', 'speak no evil')} </tbody> </table> 将snippet作为组件的属性 <FilteredList data={colors} field="name" {header} {row} ></FilteredList> {#snippet header()} <!-- ... --> {/snippet} {#snippet row()} <!-- ... --> {/snippet} <!-- 语法糖:在组件内部声明的snippet会自动成为这些组件的属性 --> <FilteredList data={colors} field="name" > {#snippet header()} <!-- ... --> {/snippet} {#snippet row()} <!-- ... --> {/snippet} </FilteredList> 动效 Tween <script> import { Tween } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; let progress = new Tween(0, { duration: 400, easing: cubicOut }); </script> <progress value={progress.current}></progress> <button onclick={() => (progress.target = 0)}> 0% </button> <button onclick={() => (progress.target = 1)}> 100% </button> Spring <script> import { Spring } from 'svelte/motion'; let coords = new Spring({ x: 50, y: 50 }, { stiffness: 0.1, damping: 0.25 }); let size = new Spring(10); </script> <svg onmousemove={(e) => { coords.target = { x: e.clientX, y: e.clientY }; }} onmousedown={() => (size.target = 30)} onmouseup={() => (size.target = 10)} role="presentation" > <circle cx={coords.current.x} cy={coords.current.y} r={size.current} /> </svg> (双向)绑定进阶 contenteditable 支持绑定textContent和innerHTML <div bind:innerHTML={html} contenteditable></div> each块 {#each todos as todo} <li class={{ done: todo.done }}> <input type="checkbox" bind:checked={todo.done} /> <input type="text" placeholder="What needs to be done?" bind:value={todo.text} /> </li> {/each} Media元素 <audio {src} bind:currentTime={time} bind:duration bind:paused ></audio> Dimensions 支持clientWidth, clientHeight, offsetWidth, offsetHeight 只读绑定 <div bind:clientWidth={w} bind:clientHeight={h}> </div> DOM元素 只读绑定 <script> let canvas; $effect(() => { const context = canvas.getContext('2d'); // ... }); </script> <canvas bind:this={canvas} width={32} height={32}></canvas> 让组件属性可绑定 let { value = $bindable(''), onsubmit } = $props(); 组件元素 <!-- 子组件 --> <script> export function f() {} </script> <!-- 父组件 --> <script> let child; </script> <Child bind:this={child} /> <button onclick={child.f}>Button</button> 过渡动画进阶 延时过渡 import { crossfade } from 'svelte/transition'; import { quintOut } from 'svelte/easing'; export const [send, receive] = crossfade({ duration: (d) => Math.sqrt(d * 200), fallback(node, params) { const style = getComputedStyle(node); const transform = style.transform === 'none' ? '' : style.transform; return { duration: 600, easing: quintOut, css: (t) => ` transform: ${transform} scale(${t}); opacity: ${t} ` }; } }); <li in:receive={{ key: todo.id }} out:send={{ key: todo.id }} /> 动画(animate:) 为不进行过渡的元素提供动画效果 <li class={{ done: todo.done }} in:receive={{ key: todo.id }} out:send={{ key: todo.id }} animate:flip > Context <!-- 设置 --> <script> import { setContext } from 'svelte'; setContext('key', value); </script> <!-- 获取 --> <script> import { getContext } from 'svelte'; const value = getContext('key'); </script> 特殊元素 <svelte:window> 可添加事件监听器 可绑定innerWidth, innerHeight, outerWidth, outerHeight, scrollX, scrollY, online(window.navigator.onLine)。除了scrollX和scrollY均为只读绑定 <svelte:document> 可添加事件监听器 <svelte:body> 可添加事件监听器 <svelte:head> 可以往HTML的<head>中加入内容 SSR模式下会与其他HTML内容分开返回 <svelte:element> 可通过this属性指定该元素的类型 <script> const options = ['h1', 'h2', 'h3', 'p', 'marquee']; let selected = $state(options[0]); </script> <svelte:element this={selected}> I'm a <code><{selected}></code> element </svelte:element> <svelte:boundary> 可用于处理组件加载错误的情况 <svelte:boundary onerror={(e) => console.error(e)}> <FlakyComponent /> {#snippet failed(error, reset)} <p>Oops! {error.message}</p> <button onclick={reset}>Reset</button> {/snippet} </svelte:boundary> <script module> 让代码从组件实例中分离出来 代码只会在模块首次被Evaluate的时候运行 可以使用export导出(但不能使用默认导出,因为默认导出是组件自身)