Back

Svelte 开发最佳实践

Svelte 开发最佳实践

如果你已经掌握了 Svelte 的基础知识,并开始构建实际应用,你可能已经注意到官方文档主要介绍了各种功能”是什么”,但并不总是说明”何时”或”为什么”使用它们。本文聚焦于 Svelte 5 的最佳实践,旨在提升生产代码的可维护性、性能与清晰度,前提是你已经理解组件和响应式的基本工作原理。

核心要点

  • 仅在值需要驱动 UI 更新时使用 $state;如果是替换值而非修改值,请使用 $state.raw
  • 对于计算值,优先使用 $derived 而非 $effect;将 $effect 保留用于与外部系统同步。
  • 在 SSR 环境中避免使用模块级状态。使用 Svelte 的 context API 配合基于类的 $state,以实现类型安全、按请求隔离的共享状态。
  • 在 SvelteKit 中,使用 +page.server.js 处理服务端页面数据,使用 +server.js 处理独立的 API 端点。
  • 在新代码中采用现代 Svelte 5 语法(onclick{#snippet}$props()),而非旧式写法。

Svelte 5 Runes:精确使用

Svelte 5 的 runes 是其核心响应式模型,正确使用它们比无处不用更重要。

只在变量需要驱动 UI 更新时使用 $state 对于其他场景,普通变量更经济也更清晰。

当状态是一个会被替换而非修改的大型对象或数组时,请改用 $state.raw

// ❌ 对仅会被重新赋值的 API 数据来说,代理开销没有必要
let users = $state(await fetchUsers());

// ✅ 当只是替换而非修改时,无需代理开销
let users = $state.raw(await fetchUsers());

当你需要直接修改嵌套属性(如 cart.items[0].quantity++)时使用 $state;当你是替换整个值时使用 $state.raw

计算值优先使用 $derived 而非 $effect

这是现代 Svelte 开发中最常见的错误之一:

let num = $state(0);

// ❌ 避免 —— 制造了不必要的副作用
let square = $state(0);
$effect(() => { square = num * num; });

// ✅ 正确 —— 声明式且自动追踪依赖
let square = $derived(num * num);

$effect 是一种”逃生舱”。请将其保留用于与外部系统(如 D3)同步,对于自然契合的 DOM 层集成,可以考虑使用 {@attach}

将 Props 视为动态值

从 props 派生的值应使用 $derived,而非普通赋值:

let { type } = $props();

// ✅ 当 `type` 变化时自动保持同步
let color = $derived(type === 'danger' ? 'red' : 'green');

类型安全的 Context 优于共享模块

对于在组件子树中共享的状态,请优先使用 Svelte 的 context API,而非模块级状态。在 SSR 环境中,模块状态会跨请求持久化,可能导致数据在用户之间泄露。

现代的做法是使用带有 $state 字段的类:

// lib/theme.svelte.ts
import { getContext, setContext } from 'svelte';

class ThemeContext {
  current = $state('light');

  toggle() {
    this.current = this.current === 'light' ? 'dark' : 'light';
  }
}

const KEY = Symbol('theme');

export const setTheme = () => setContext(KEY, new ThemeContext());

export const getTheme = () => getContext<ThemeContext>(KEY);

这一种模式同时提供了类型安全、响应式状态以及合理的 SSR 作用域隔离。

SvelteKit 数据加载:选择合适的模式

SvelteKit 中一个常见的困惑点是:何时使用 +page.server.js,何时使用 +server.js

场景使用
为带有 SSR 或仅服务端访问的页面获取数据+page.server.js 配合 load()
构建供外部使用的 API 端点+server.js
客户端水合后才需要的数据onMount + fetch

对于需要服务端访问、密钥或 SSR 的页面数据,+page.server.js 通常是合适的默认选择。它在服务端运行,避免密钥暴露到客户端,并能与 SvelteKit 的 form actions 无缝集成,实现渐进增强。

一些实用的小技巧

带 key 的 {#each}可以防止微妙的 DOM 复用 bug。请始终使用稳定且唯一的 ID 作为 key,切勿使用索引。

$inspect.trace 在调试响应式问题时被低估了。在任何 $effect$derived.by 的顶部加上它,就能精确看到是哪个依赖触发了重新执行。

优先使用 Snippets 而非 Slots 来复用标记片段。Snippets 组合性更好,并且可以作为 props 传递,使组件 API 更清晰。

在新代码中避免旧式语法。onclick 替换 on:click,用 {#snippet} 替换 <slot>,用 $props() 替换 export let。这些模式与现代 Svelte 5 的约定和当前编译器行为保持一致。

结语

Svelte 5 奖励克制。响应式作用域越精确——仅在需要时使用 $state、用 $derived 替代 $effect、用 context 替代模块全局变量——你的应用就越可预测且性能越好。先从能解决问题的最简单响应式原语开始,只有当简单工具确实不够用时,才转向更强大的工具。

常见问题

当你打算整体替换某个值而非局部修改时使用 $state.raw,例如存储获取到的 API 响应或会被整体重新赋值的大数组。它跳过了响应式代理的开销,对大数据集而言能提升性能。当你需要对嵌套修改进行细粒度响应(如更新数组中的某项)时,请使用普通的 $state。

$derived 是声明式的,会自动追踪依赖,并产生一个保持同步的只读值。$effect 以副作用形式命令式地执行,更难推理,可能引入时序 bug 和不必要的重复执行。请将 $effect 保留用于与 Svelte 响应式之外的系统同步,例如第三方库、canvas API 或手动 DOM 操作。

在 SvelteKit 等 SSR 环境中并不安全。模块级状态会被服务器处理的所有请求共享,可能导致一个用户的数据泄露到另一个用户的会话中。请使用 Svelte 的 context API,配合 setContext 和 getContext,最好以包含 $state 字段的类作为载体。这样能在保持响应式和类型安全的同时,将状态作用域限定在每个请求和组件树中。

当数据属于特定页面,并且需要 SSR、SEO、服务端访问或表单操作时,使用 +page.server.js。当你需要一个独立的 HTTP 端点时使用 +server.js,例如供外部客户端使用的 JSON API、webhook 或非页面的 fetch 调用。如果数据仅由某个页面渲染且需要服务端能力,+page.server.js 通常是更好的默认选择。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay