12k
All articles

Svelte 开发最佳实践

Svelte 5 实用规范:$state、$derived、context 和 SvelteKit 数据加载,以及 keyed each 和现代语法的要点。

OpenReplay Team
OpenReplay Team
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 而非 $state?

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

为什么计算值优先使用 $derived 而非 $effect?

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

将共享状态存储在模块级变量中安全吗?

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

什么时候应该选择 +server.js 而非 +page.server.js?

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

DevTools for the frontend

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.