Svelte
Svelte 的口号是"编译器,而非运行时框架":组件在构建期被编译成直接操作 DOM 的命令式 JS,浏览器里没有虚拟 DOM、没有庞大运行时——代价是"框架逻辑"编译进产物。Svelte 5 引入 runes(rune = 符文),把响应式从"魔法语法"升级为显式的通用原语。本文基于 Svelte 5(2024-10 发布,当前主线)+ SvelteKit 2;示例为标准写法,可复制进
npm create svelte@latest模板直接运行。
一、编译时框架:没有虚拟 DOM 的 UI
1.1 一次编译,胜过一次运行时
对比主流框架的更新策略:
| 框架 | 更新机制 | 运行时成本 |
|---|---|---|
| React | 组件函数重跑 → 虚拟 DOM diff → 应用差异 | 每次更新全量 diff(可并发中断) |
| Vue | 模板编译期静态分析 + 运行时细粒度响应式 | 组件级更新 + 依赖级缓存 |
| Svelte | 编译期生成"精确更新指令",值变化直接改对应 DOM 节点 | 无 diff、无虚拟 DOM(旧 Svelte 4 语法时代) |
| Angular | Zone.js/信号 触发变更检测 | 组件树检查(可 OnPush 收敛) |
<!-- Counter.svelte(Svelte 5 runes 写法) -->
<script>
let count = $state(0) // rune:声明响应式状态
const inc = () => count += 1
</script>
<button onclick={inc}>点了 {count} 次</button>编译后这不再是"模板解释器",而是一段带精准 DOM 引用的 JS:count 变化 → 只更新按钮文本节点。
1.2 runes 是什么
Svelte 4 的响应式靠编译器魔法:顶层 let 自动响应、$: 语句标记派生。魔法好写但难迁移、难在普通 .js/.ts 文件里复用。Svelte 5 把能力显式化为一组以 $ 开头的编译期原语——runes:
| rune | 作用 | 对应旧语法(legacy) |
|---|---|---|
$state(...) | 声明响应式状态(深响应) | 顶层 let(隐式) |
$derived(...) | 由状态派生、惰性缓存的值 | $: double = count * 2 |
$effect(...) | 依赖变化后自动运行的副作用 | $: console.log(count) |
$props() | 读取组件 props | export let |
$bindable() | 声明可被父级 bind: 的属性 | export let + 约定 |
$inspect(...) | 调试打印 | $: console.log |
$host() | 自定义元素模式下取宿主节点 | — |
双模式迁移策略:为平滑升级,Svelte 5 中不含任何 rune 的组件默认按 legacy 模式(约等于 Svelte 4 语义)运行;只要组件里出现任一 rune 即整体进入 runes 模式(也可在文件顶层显式开启)。新项目请直接全程 runes,别两边混着写。
1.3 通用响应式:rune 走出组件
runes 可以在任意 .svelte.ts / .svelte.js 模块里用——状态不再绑定组件生命周期,可脱离 UI 独立测试:
// store.svelte.ts:模块级共享状态(编译时把 rune 编译进这个模块)
export const session = $state({ user: null, token: '' })
export const isLoggedIn = $derived(!!session.user)二、runes 响应式细节
2.1 $state:深响应,基于 Proxy
let profile = $state({ name: 'Ada', tags: [] })
profile.name = 'Grace' // 触发更新(深层属性也响应)
profile.tags.push('editor') // 数组方法 OK(Proxy 拦截)- 对象与数组默认深响应(Proxy 包裹);
$state.raw(...)可声明浅层、不代理的大对象(性能优化);- 想按类实例管理的用
$state(class 实例)(class 会被代理,见官方文档注意事项)。
坑 1:对
$state对象做解构会丢失响应性——解构出来的是普通值。需要派生若干字段时用$derived:
<script>
let user = $state({ name: 'Ada', age: 36 })
// ❌ const { name } = user // 之后的 user.name 变化不再响应
const displayName = $derived(`${user.name} (${user.age})`) // ✅ 派生表达式内同步读取
</script>2.2 $derived:惰性、只读
let count = $state(0)
const double = $derived(count * 2) // 惰性:没人读就不计算- 依赖必须同步读取:写在回调/异步里不会被追踪;
$derived.by(fn)形式用于多语句派生。
2.3 $effect:副作用要克制
$effect 在依赖变化后(DOM 更新完)执行,可返回清理函数:
$effect(() => {
const v = count * 2 // 同步读取即注册依赖
document.title = `count=${v}` // 副作用放这里
return () => clearTimer() // 依赖变化/卸载时清理
})心智锚点与 React 的 useEffect 一致:别用它改别的 $state 去"同步数据"——那会触发新一轮 effect,是循环与闪烁之源。派生一律 $derived。SSR 期间 $effect 不执行(只在浏览器跑)。
2.4 事件与 props:一切皆属性
Svelte 5 移除了 createEventDispatcher——组件通信 = props 传函数,事件就是带 on 前缀的普通属性:
<!-- Child.svelte -->
<script>
let { value, onIncrement } = $props()
</script>
<button onclick={onIncrement}>父级的值:{value}</button><!-- Parent.svelte -->
<script>
import Child from './Child.svelte'
let n = $state(0)
</script>
<Child value={n} onIncrement={() => n += 1} />- 双向:子组件用
$bindable()声明,父级bind:value={n}; - 原生 DOM 事件在 runes 下写
onclick(移除on:前缀,也移除.preventDefault等修饰符语法,需要手动处理或onclick={(e) => {...}}); - 不想透传的属性可用
{...restProps}展开模式。
三、模板与块语法
3.1 逻辑块
{#if loading}
<p>加载中…</p>
{:else if error}
<p class="err">出错了</p>
{:else}
<ul>
{#each items as item (item.id)} <!-- 括号内为 key -->
<li>{item.title}</li>
{/each}
</ul>
{/if}{#each}的 key 必须给:否则增删会复用 DOM、输入框状态错位(和 React 的 key 教训同源);{#each obj, i as v, i}取索引;{#await promise}处理异步值(见 §3.2)。
3.2 异步块与 key 块
{#await loadList() then list}
<ul>{#each list as it}<li>{it.name}</li>{/each}</ul>
{:catch err}
<p>失败:{err.message}</p>
{/await}
{#key userId} <!-- 值变化时销毁重建内部,重跑初始化/动画 -->
<Profile id={userId} />
{/key}3.3 snippets:可复用模板片断
<script>
let users = $state([{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }])
</script>
{#snippet row(u)}
<li>{u.name}(#{u.id})</li>
{/snippet}
<ul>
{#each users as u}{@render row(u)}{/each}
</ul>- 类似函数式模板,可在组件间通过 props 传递(回调式的 slot 替代品);
- 默认 children 片段:
{@render children()}。
四、SvelteKit:全栈框架(Svelte 5 + SvelteKit 2)
4.1 文件路由与加载
src/routes/
├── +layout.svelte # 布局壳(含每个页面的共同 UI)
├── +layout.server.ts # 布局级服务端加载(登录态等)
├── +page.svelte # 页面组件
├── +page.server.ts # 服务端 load:访问 db/密钥,只跑在服务端
├── +page.ts # 通用 load:前后端都能跑(后端 SSR / 前端导航)
└── blog/[slug]/
├── +page.svelte
└── +page.server.ts # ({ params }) => ({ post })// +page.server.ts:load 返回值作为页面组件的 props
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params }) => {
const post = await db.post.findUnique({ where: { slug: params.slug } })
return { post } // 页面里直接 {#if post}{@render ...}{/if}
}- Universal vs Server load:需要密钥/直连 DB →
+page.server.ts;纯公共数据/想减少服务端往返 →+page.ts; - 关键点:load 返回的对象若含 Promise,SvelteKit 自动 streaming(外壳先出、数据分块到);
- 组件内取路由态用
$app/state(SvelteKit 2.12+ 推荐,取代旧$app/stores的 store 方式)。
4.2 表单 Actions(无需 JS 也能提交)
// +page.server.ts
export const actions = {
default: async ({ request }) => {
const fd = await request.formData()
const name = String(fd.get('name') ?? '')
await db.user.create({ data: { name } })
return { ok: true, name } // 返回给页面显示
},
}<form method="POST" action="?/">
<input name="name" required />
<button>创建</button>
</form>无 JS 可用表单即交;需要渐进增强时用 use:enhance 变成 SPA 式提交。
4.3 部署与渲染模式
| 渲染方式 | 手段 | 适用 |
|---|---|---|
| SSR(默认) | 首屏服务端渲染后水合 | 大多数动态站 |
| SSG/预渲染 | export const prerender = true 或项目级配置 | 内容站、文档(配合 adapter-static) |
| CSR(SPA 模式) | export const ssr = false | 纯客户端应用 |
| 边缘/Node 服务 | adapter-node / adapter-vercel / adapter-cloudflare 等 | 动态接口场景 |
// svelte.config.js:SSG 需要 adapter-static + prerender 配置
import adapter from '@sveltejs/adapter-static'
export default {
kit: {
adapter: adapter({ pages: 'build', assets: 'build', fallback: undefined }),
prerender: { entries: ['/', '/about'] },
},
}五、常见坑速查
- legacy 与 runes 混写:同一组件只要出现 rune 就整体进入 runes 模式,旧语法(
export let、$:、on:click)同时混用会行为混乱——迁移时按文件整份处理; - 解构
$state对象丢响应(§2.1 坑 1):派生用$derived,别指望解构出的变量联动; $effect里写$state:改完再触发 effect → 循环或闪烁——"数据联动"交给$derived,effect 只做外部副作用;$derived在回调/异步里读依赖:不会注册依赖,值永远不更新——必须同步读取;{#each}不给 key:DOM 复用错位、输入框状态串行(§3.1);- 深响应对象整体替换:
obj = { ...obj, a: 1 }会把旧的 proxy 整个换掉,依赖它的$derived会重算但若有外部引用会指向旧对象——需要时用$state.snapshot(obj)取值; - 在 load 里 throw 后页面无提示:SvelteKit 里
throw error(404, '...')才能正确走+error.svelte,直接 throw 新 Error 只对顶层兜底可见; - 开发环境"hot 报错"与
$inspect残留:$inspect记得移除,模板里误留{}表达式会直接编译报错(编译器信息通常很具体,跟着改即可)。
状态与参考
- 状态:已收录(2026-09-02,由前端领域规划清单「Svelte|编译时框架、细粒度响应、SvelteKit 全栈」转正式)。
- 版本:基于 Svelte 5(2024-10 发布,当前主线,runes 为默认推荐写法;legacy 模式用于平滑迁移)+ SvelteKit 2(
$app/state自 2.12+ 提供)。文中示例为标准 runes 写法,可复制进npm create svelte@latest模板直接运行;版本细节以 官方 Releases 为准。 - 参考:Svelte 官方文档、Svelte 5 迁移指南、SvelteKit 文档、runes 详解。
- 阅读联动:与 React/Vue 的渲染机制差异对照前端领域概览与 React/Vue 3;细粒度更新与信号理论可对照 Angular 信号章节;编译期思路见构建工具。
下一步
- [ ] 镜像 Svelte 官方文档分篇(类似 Vue 3 栏目),把 runes、snippets、SvelteKit load/actions 展开为独立子页
- [ ] 跑通一个 SvelteKit「表单 actions + 流式 load + adapter-static 预渲染」三模式混合示例
- [ ] 对照 React Compiler 与 Vue 响应式,写"编译器优化 vs 运行时信号"机制对比
写作规范请参阅领域概览。