Svelte 极简设计哲学与实践
Svelte 是一种全新的构建用户界面的方法。与 React、Vue 等运行时框架不同,Svelte 将大部分工作转移到编译阶段,生成高效的原生 JavaScript 代码。本文将深入探讨 Svelte 的设计哲学与核心特性。
Svelte 设计哲学
核心理念对比
| 理念 | 传统框架 | Svelte |
|---|---|---|
| 响应式实现 | 运行时追踪依赖 | 编译时代码生成 |
| DOM 更新 | 虚拟 DOM Diff | 直接 DOM 操作 |
| 框架代码 | 打包进应用 | 编译后消失 |
| 学习曲线 | 框架特定 API | 接近原生 HTML/CSS/JS |
| 包体积 | 框架 + 应用代码 | 仅应用代码 |
编译时优化
Svelte 的核心优势在于编译时优化:
<!-- 源代码:简洁直观 -->
<script>
let count = 0
function increment() {
count += 1
}
</script>
<button on:click={increment}>
点击了 {count} 次
</button>
编译后生成高效的更新代码:
// 编译输出(简化版)
function instance($$self, $$props, $$invalidate) {
let count = 0
function increment() {
$$invalidate(0, count += 1) // 精确标记更新
}
return [count, increment]
}
// 更新函数
function update(ctx, [dirty]) {
if (dirty & 1) { // 位掩码检查
set_data(t1, ctx[0]) // 直接更新 DOM
}
}
响应式原语
响应式声明
Svelte 使用 $: 符号创建响应式声明:
<script>
let width = 10
let height = 5
// 响应式计算属性
$: area = width * height
// 响应式语句
$: if (area > 100) {
console.log('面积超过100')
}
// 响应式代码块
$: {
console.log(`宽: ${width}, 高: ${height}`)
console.log(`面积: ${area}`)
}
</script>
<input type="number" bind:value={width}>
<input type="number" bind:value={height}>
<p>面积: {area}</p>
Store 响应式状态管理
// stores.ts
import { writable, derived, readable } from 'svelte/store'
// 可写 Store
export const count = writable(0)
// 使用方法
count.set(10) // 设置值
count.update(n => n + 1) // 更新值
// 派生 Store
export const doubled = derived(count, $count => $count * 2)
// 只读 Store
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date())
}, 1000)
return function stop() {
clearInterval(interval)
}
})
// 自定义 Store
function createTodos() {
const { subscribe, set, update } = writable<Todo[]>([])
return {
subscribe,
add: (text: string) => update(todos => [...todos, { id: Date.now(), text, done: false }]),
toggle: (id: number) => update(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
),
remove: (id: number) => update(todos => todos.filter(t => t.id !== id)),
reset: () => set([])
}
}
export const todos = createTodos()
在组件中使用 Store:
<script>
import { count, doubled, todos } from './stores'
// 使用 $ 前缀自动订阅
</script>
<p>计数: {$count}</p>
<p>双倍: {$doubled}</p>
<button on:click={() => count.update(n => n + 1)}>增加</button>
<ul>
{#each $todos as todo (todo.id)}
<li>
<input type="checkbox" checked={todo.done} on:change={() => todos.toggle(todo.id)}>
{todo.text}
<button on:click={() => todos.remove(todo.id)}>删除</button>
</li>
{/each}
</ul>
组件设计
组件基础结构
<!-- Card.svelte -->
<script lang="ts">
// Props 定义
export let title: string
export let subtitle: string = '' // 可选,带默认值
export let variant: 'default' | 'outlined' | 'elevated' = 'default'
// 事件派发
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher<{
click: { id: string }
close: void
}>()
function handleClick() {
dispatch('click', { id: title })
}
</script>
<div class="card card-{variant}" on:click={handleClick}>
<header class="card-header">
<h3>{title}</h3>
{#if subtitle}
<p>{subtitle}</p>
{/if}
<button class="close-btn" on:click|stopPropagation={() => dispatch('close')}>
×
</button>
</header>
<div class="card-body">
<slot />
</div>
{#if $$slots.footer}
<footer class="card-footer">
<slot name="footer" />
</footer>
{/if}
</div>
<style>
.card {
border-radius: 8px;
padding: 16px;
background: white;
}
.card-outlined {
border: 1px solid #e0e0e0;
}
.card-elevated {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
}
.close-btn {
margin-left: auto;
background: none;
border: none;
cursor: pointer;
font-size: 20px;
}
</style>
组件组合模式
<!-- Tabs.svelte -->
<script context="module" lang="ts">
// 模块级上下文:所有实例共享
export interface TabContext {
registerTab: (id: string, label: string) => void
unregisterTab: (id: string) => void
selectTab: (id: string) => void
selectedId: Writable<string>
}
</script>
<script lang="ts">
import { setContext, onMount } from 'svelte'
import { writable } from 'svelte/store'
const tabs: { id: string; label: string }[] = []
const selectedId = writable<string>('')
setContext<TabContext>('tabs', {
registerTab(id, label) {
tabs.push({ id, label })
if (tabs.length === 1) {
selectedId.set(id)
}
},
unregisterTab(id) {
const index = tabs.findIndex(t => t.id === id)
if (index > -1) tabs.splice(index, 1)
},
selectTab(id) {
selectedId.set(id)
},
selectedId
})
</script>
<div class="tabs">
<div class="tab-list" role="tablist">
{#each tabs as tab (tab.id)}
<button
role="tab"
class:active={$selectedId === tab.id}
on:click={() => selectedId.set(tab.id)}
>
{tab.label}
</button>
{/each}
</div>
<div class="tab-panels">
<slot />
</div>
</div>
<!-- TabPanel.svelte -->
<script lang="ts">
import { getContext, onMount, onDestroy } from 'svelte'
import type { TabContext } from './Tabs.svelte'
export let id: string
export let label: string
const { registerTab, unregisterTab, selectedId } = getContext<TabContext>('tabs')
onMount(() => registerTab(id, label))
onDestroy(() => unregisterTab(id))
</script>
{#if $selectedId === id}
<div role="tabpanel">
<slot />
</div>
{/if}
使用组合组件:
<Tabs>
<TabPanel id="overview" label="概览">
<p>这是概览内容</p>
</TabPanel>
<TabPanel id="details" label="详情">
<p>这是详情内容</p>
</TabPanel>
<TabPanel id="settings" label="设置">
<p>这是设置内容</p>
</TabPanel>
</Tabs>
过渡与动画
Svelte 内置强大的过渡和动画系统:
<script>
import { fade, fly, slide, scale, draw } from 'svelte/transition'
import { quintOut, elasticOut } from 'svelte/easing'
import { flip } from 'svelte/animate'
let items = ['苹果', '香蕉', '橙子']
let visible = true
function addItem() {
items = [...items, `水果${items.length + 1}`]
}
function removeItem(index: number) {
items = items.filter((_, i) => i !== index)
}
</script>
<!-- 基础过渡 -->
{#if visible}
<div transition:fade>淡入淡出</div>
{/if}
<!-- 带参数的过渡 -->
{#if visible}
<div transition:fly={{ y: 200, duration: 500, easing: quintOut }}>
飞入效果
</div>
{/if}
<!-- 进入和离开分开定义 -->
{#if visible}
<div in:fly={{ y: -50 }} out:fade>
进入飞入,离开淡出
</div>
{/if}
<!-- 列表动画 -->
<ul>
{#each items as item, index (item)}
<li
in:fly={{ x: -100, duration: 300, delay: index * 100 }}
out:fade
animate:flip={{ duration: 300 }}
>
{item}
<button on:click={() => removeItem(index)}>删除</button>
</li>
{/each}
</ul>
<!-- 自定义过渡 -->
<script>
function typewriter(node: HTMLElement, { speed = 1 }) {
const text = node.textContent
const duration = text.length / (speed * 0.01)
return {
duration,
tick: (t: number) => {
const i = Math.trunc(text.length * t)
node.textContent = text.slice(0, i)
}
}
}
</script>
{#if visible}
<p transition:typewriter={{ speed: 2 }}>
这段文字会像打字机一样逐字显示
</p>
{/if}
Actions 指令
Actions 是 Svelte 中复用 DOM 操作逻辑的方式:
<script lang="ts">
import type { Action } from 'svelte/action'
// 点击外部关闭
const clickOutside: Action<HTMLElement, () => void> = (node, callback) => {
function handleClick(event: MouseEvent) {
if (!node.contains(event.target as Node)) {
callback?.()
}
}
document.addEventListener('click', handleClick, true)
return {
destroy() {
document.removeEventListener('click', handleClick, true)
}
}
}
// 自动聚焦
const autofocus: Action = (node) => {
node.focus()
}
// 工具提示
const tooltip: Action<HTMLElement, string> = (node, text) => {
let tooltipEl: HTMLElement
function show() {
tooltipEl = document.createElement('div')
tooltipEl.className = 'tooltip'
tooltipEl.textContent = text
const rect = node.getBoundingClientRect()
tooltipEl.style.left = `${rect.left + rect.width / 2}px`
tooltipEl.style.top = `${rect.top - 8}px`
document.body.appendChild(tooltipEl)
}
function hide() {
tooltipEl?.remove()
}
node.addEventListener('mouseenter', show)
node.addEventListener('mouseleave', hide)
return {
update(newText) {
text = newText
},
destroy() {
node.removeEventListener('mouseenter', show)
node.removeEventListener('mouseleave', hide)
hide()
}
}
}
// 长按事件
const longpress: Action<HTMLElement, number> = (node, duration = 500) => {
let timer: ReturnType<typeof setTimeout>
function handleMouseDown() {
timer = setTimeout(() => {
node.dispatchEvent(new CustomEvent('longpress'))
}, duration)
}
function handleMouseUp() {
clearTimeout(timer)
}
node.addEventListener('mousedown', handleMouseDown)
node.addEventListener('mouseup', handleMouseUp)
return {
destroy() {
clearTimeout(timer)
node.removeEventListener('mousedown', handleMouseDown)
node.removeEventListener('mouseup', handleMouseUp)
}
}
}
let isOpen = false
let message = ''
</script>
<!-- 使用 Actions -->
<div class="dropdown" use:clickOutside={() => isOpen = false}>
<button on:click={() => isOpen = !isOpen}>菜单</button>
{#if isOpen}
<ul class="dropdown-menu">
<li>选项 1</li>
<li>选项 2</li>
</ul>
{/if}
</div>
<input use:autofocus placeholder="自动获得焦点">
<button use:tooltip="这是提示文字">悬停查看提示</button>
<button
use:longpress={1000}
on:longpress={() => message = '长按触发!'}
>
长按我
</button>
SvelteKit 全栈开发
SvelteKit 是 Svelte 的官方应用框架:
路由与页面
src/routes/
├── +page.svelte # 首页 /
├── +layout.svelte # 根布局
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ ├── +page.server.ts # 服务端数据加载
│ └── [slug]/
│ ├── +page.svelte # /blog/:slug
│ └── +page.ts # 客户端数据加载
└── api/
└── posts/
└── +server.ts # API 端点 /api/posts
数据加载
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ fetch, params, url }) => {
const page = Number(url.searchParams.get('page')) || 1
const response = await fetch(`/api/posts?page=${page}`)
const posts = await response.json()
return {
posts,
page
}
}
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData
</script>
<h1>博客文章</h1>
<ul>
{#each data.posts as post}
<li>
<a href="/blog/{post.slug}">{post.title}</a>
</li>
{/each}
</ul>
API 端点
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1
const limit = 10
const posts = await db.posts.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
})
return json(posts)
}
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
const post = await db.posts.create({
data: {
title: data.title,
content: data.content
}
})
return json(post, { status: 201 })
}
Svelte 5 新特性
Svelte 5 引入了 Runes,进一步简化响应式系统:
<script>
// Svelte 5 Runes
// $state: 响应式状态
let count = $state(0)
// $derived: 派生状态
let doubled = $derived(count * 2)
// $effect: 副作用
$effect(() => {
console.log(`count 变为: ${count}`)
// 返回清理函数
return () => {
console.log('清理')
}
})
// $props: 组件属性
let { title, description = '' } = $props()
// $bindable: 可绑定属性
let { value = $bindable() } = $props()
</script>
<button onclick={() => count++}>
{count} × 2 = {doubled}
</button>
性能优势总结
Svelte 的性能优势来源于编译时优化:
| 优化项 | 实现方式 | 性能收益 |
|---|---|---|
| 无虚拟 DOM | 直接 DOM 操作 | 减少运行时开销 |
| 编译时分析 | 静态代码生成 | 最小化更新范围 |
| 无框架运行时 | 代码即应用 | 更小的包体积 |
| 响应式编译 | 依赖追踪内联 | 无需运行时追踪 |
| CSS 作用域 | 编译时处理 | 无运行时开销 |
总结
Svelte 的设计哲学核心:
- 编译时优化:将运行时工作转移到编译阶段
- 极简语法:接近原生 HTML/CSS/JS,学习成本低
- 响应式原语:简洁直观的响应式声明
- 内置动画:强大的过渡和动画系统
- 全栈能力:SvelteKit 提供完整的应用框架
Svelte 代表了前端框架的另一种可能:通过智能编译,让开发者写出简洁的代码,同时获得极致的运行时性能。


