# Svelte / SvelteKit Code Review Guide Svelte 5 / SvelteKit 审查重点:Runes 响应式系统、Server/Client 边界、Form Actions、Store 迁移、以及安全性。 ## 目录 - [Runes: $state / $derived / $effect](#runes-state--derived--effect) - [Load 函数(Server vs Client)](#load-函数server-vs-client) - [Form Actions](#form-actions) - [Store 迁移(→ $state)](#store-迁移) - [SSR vs CSR 边界](#ssr-vs-csr-边界) - [响应式语句迁移($: → Runes)](#响应式语句迁移) - [性能优化](#性能优化) - [安全审查](#安全审查) - [Review Checklist](#review-checklist) --- ## Runes: $state / $derived / $effect ### $state 基础用法 ```svelte ``` ### $state.raw 与大型对象 ```svelte ``` ### $state.snapshot 用于外部库 ```svelte ``` ### 解构 $state 丢失响应性 ```svelte
{count}
{state.count}
``` --- ### $derived 正确用法 ```svelte ``` ### $derived 中不应有副作用 ```svelte ``` --- ### $effect 正确用法 #### $effect vs $derived ```svelte ``` #### 无限循环 ```svelte ``` #### 清理函数 ```svelte ``` #### async $effect 的追踪陷阱 ```svelte ``` #### untrack 排除依赖 ```svelte ``` --- ## Load 函数(Server vs Client) ### +page.server.js vs +page.js ```typescript // ❌ 在 +page.js 中访问数据库或 secrets // src/routes/admin/+page.js export async function load({ fetch }) { // universal load runs on both server and client const data = await db.query('SELECT * FROM users'); // db not available in browser! return { users: data }; } // ✅ 服务端逻辑放在 +page.server.js // src/routes/admin/+page.server.js import { db } from '$lib/server/db'; export async function load() { const users = await db.query('SELECT * FROM users'); return { users }; } ``` ```typescript // ✅ +page.js 用于客户端也可用的数据(如 fetch 聚合) // src/routes/dashboard/+page.js export async function load({ fetch, parent }) { const [analytics, notifications] = await Promise.all([ fetch('/api/analytics').then(r => r.json()), fetch('/api/notifications').then(r => r.json()) ]); return { analytics, notifications }; } ``` ### await parent() 瀑布流 ```typescript // ❌ 顺序 await parent → 瀑布流 // src/routes/blog/[slug]/+page.js export async function load({ parent, fetch }) { const parentData = await parent(); // wait for parent const post = await fetch(`/api/posts/${parentData.blogId}`); return { post }; } // ✅ 尽可能并行,避免不必要的 parent await // src/routes/blog/[slug]/+page.js export async function load({ parent, fetch }) { // only await parent if you truly need its data const post = await fetch('/api/posts/slug'); return { post }; } // ✅ 如果确实需要 parent 数据,无法避免瀑布流,但要明确注释 // src/routes/blog/[slug]/+page.js export async function load({ parent, fetch }) { const { blogId } = await parent(); // required: need blogId for post URL const post = await fetch(`/api/posts/${blogId}`); return { post }; } ``` ### 不可序列化的返回值 ```typescript // ❌ 从 server load 返回不可序列化的值 // src/routes/api/+page.server.js export async function load() { return { stream: fs.createReadStream('data.csv'), // not serializable! callback: () => console.log('hi'), // functions not serializable! date: new Date(), // OK — devalue serializes Date/Map/Set fine }; } // ✅ 只返回可序列化的数据 // src/routes/api/+page.server.js export async function load() { return { data: await readFile('data.csv', 'utf-8'), timestamp: Date.now(), }; } ``` --- ## Form Actions ### 使用 POST 处理副作用 ```svelte ``` ```typescript // src/routes/users/+page.server.js import { fail, redirect } from '@sveltejs/kit'; export const actions = { delete: async ({ request, locals }) => { const formData = await request.formData(); const id = formData.get('id'); if (!id) return fail(400, { message: 'Missing id' }); await locals.db.users.delete(id); throw redirect(303, '/users'); } }; ``` ```svelte ``` ### fail() 中不暴露敏感信息 ```typescript // ❌ fail() 中返回敏感信息 // src/routes/login/+page.server.js export const actions = { default: async ({ request, locals }) => { const formData = await request.formData(); const user = await locals.db.users.findByEmail(formData.get('email')); return fail(401, { password: formData.get('password'), // ❌ exposes password in page data! hint: user.passwordHint, // ❌ leaks internal data! }); } }; // ✅ 只返回安全的错误信息 export const actions = { default: async ({ request }) => { const formData = await request.formData(); const email = formData.get('email'); return fail(401, { email, // ✅ safe to echo back incorrect: true, // ✅ generic error flag }); } }; ``` ### use:enhance 渐进增强 ```svelte ``` --- ## Store 迁移(→ $state) ### writable/readable → $state ```typescript // ❌ Legacy store pattern (Svelte 4) // src/lib/stores/user.js import { writable, derived } from 'svelte/store'; export const user = writable(null); export const isLoggedIn = derived(user, $user => !!$user); // usage with $ prefix // $user = { name: 'John' }; // ✅ Svelte 5: shared state in .svelte.js files // src/lib/stores/user.svelte.js let currentUser = $state{$count}
{count}
{counter.value}
``` ### .svelte.js / .svelte.ts 扩展名 ```typescript // ❌ 在普通 .js 文件中使用 runes → 编译错误 // src/lib/utils.js let state = $state(0); // runes only work in .svelte.js files! // ✅ 使用 .svelte.js 扩展名 // src/lib/utils.svelte.js let state = $state(0); export function getState() { return state; } export function setState(val: number) { state = val; } ``` --- ## SSR vs CSR 边界 ### ssr=false SPA 模式 ```typescript // ❌ 在根 layout 中禁用 SSR → 全部变成 CSR // src/routes/+layout.js export const ssr = false; // entire app becomes SPA // ✅ 只在需要的页面禁用 SSR // src/routes/admin/dashboard/+page.js export const ssr = false; // only this page skips SSR // ✅ 更好的做法:按路由配置 // src/routes/editor/+page.js export const ssr = false; // editor needs browser APIs, skip SSR ``` ### 浏览器全局变量在 SSR 中 ```svelte ``` ### prerender 与 actions 冲突 ```typescript // ❌ prerender 页面中定义 actions → 编译错误 // src/routes/contact/+page.server.js export const prerender = true; export const actions = { // Error: prerendered pages cannot have server-side form actions default: async ({ request }) => { /* ... */ } }; // ✅ prerender 页面不使用 server actions // src/routes/about/+page.server.js export const prerender = true; // no actions — static page // ✅ 需要 actions 的页面不 prerender // src/routes/contact/+page.server.js export const actions = { default: async ({ request }) => { // handle form submission } }; ``` --- ## 响应式语句迁移 ### $: → $derived / $effect ```svelte ``` ### export let → $props() ```svelte ``` ### on:click → onclick ```svelte ``` ### createEventDispatcher → 回调 props ```svelteBody content
{#snippet footer()}Footer
{/snippet}Loading posts...
{:then posts}Failed to load posts: {error.message}
{/await} ``` --- ## 安全审查 ### 不暴露私有环境变量 ```typescript // ❌ 在 universal load 中暴露服务端 secrets // src/routes/admin/+page.js (universal — runs on client too!) export async function load() { return { apiKey: process.env.SECRET_API_KEY, // exposed to client bundle! dbUrl: import.meta.env.DATABASE_URL, // leaks to browser! }; } // ✅ 私有环境变量只在 server load 中使用 // src/routes/admin/+page.server.js (server-only) export async function load({ locals }) { // secrets stay on server const data = await fetch(process.env.SECRET_API_KEY + '/admin'); return { data }; // only derived data is sent to client } // ✅ 公开变量使用 PUBLIC_ 前缀 // .env // PUBLIC_API_URL=https://api.example.com // SECRET_API_KEY=xxx (no PUBLIC_ prefix = server-only) ``` ### $lib/server/ 服务端隔离 ```typescript // ❌ 服务端代码放在可被客户端导入的位置 // src/lib/db.js import { SECRET_DB_URL } from '$env/static/private'; // any client component importing this gets the secret! // ✅ 放在 $lib/server/ 目录 → 客户端导入会编译报错 // src/lib/server/db.js import { SECRET_DB_URL } from '$env/static/private'; export async function query(sql: string) { // safe: client cannot import from $lib/server/ } // usage in server files only // src/routes/api/users/+server.js import { query } from '$lib/server/db'; ``` ### CSRF 内建防护 ```typescript // ✅ SvelteKit 内建 CSRF 防护 // Origin header is checked automatically for POST/PUT/DELETE/PATCH // No additional CSRF tokens needed for form actions // ❌ 不要禁用 CSRF 检查(除非有充分理由) // src/hooks.server.js export const handle = sequence( // do NOT do this without understanding the implications // ({ event, resolve }) => resolve(event, { filterSerializedResponseHeaders: () => true }) ); ``` ### Cookie 安全设置 ```typescript // ❌ 不安全的 Cookie 设置 // src/hooks.server.js export async function handle({ event, resolve }) { const token = event.cookies.get('session'); // cookie without httpOnly, secure, sameSite flags event.cookies.set('session', token, { path: '/', // missing: httpOnly, secure, sameSite }); } // ✅ 安全的 Cookie 配置 import { dev } from '$app/environment'; event.cookies.set('session', token, { path: '/', httpOnly: true, // not accessible via JS secure: !dev, // HTTPS only in production sameSite: 'lax', // CSRF protection maxAge: 60 * 60 * 24 * 7 // 1 week, explicit expiry }); ``` --- ## Review Checklist ### Runes: $state / $derived / $effect - [ ] $state 只用于会变化的值,常量直接声明 - [ ] 大型不可变数据使用 $state.raw - [ ] 没有解构 $state 对象(会丢失响应性) - [ ] 外部库使用 $state.snapshot 传入普通对象 - [ ] $derived 中没有副作用 - [ ] 没有用 $effect 替代 $derived 做状态同步 - [ ] $effect 中不修改被追踪的状态(避免无限循环) - [ ] $effect 有清理函数(订阅、定时器、WebSocket) - [ ] async $effect 在 await 前读取所有需要追踪的状态 - [ ] 使用 untrack 排除不相关的依赖 ### Load 函数 - [ ] 服务端逻辑放在 +page.server.js(不是 +page.js) - [ ] 避免不必要的 await parent() 瀑布流 - [ ] 独立数据并行加载(Promise.all 或直接返回 Promise) - [ ] server load 只返回可序列化的数据 ### Form Actions - [ ] 副作用操作(增删改)使用 form actions + POST - [ ] fail() 不返回敏感信息(密码、内部数据) - [ ] 使用 use:enhance 实现渐进增强 ### Store 迁移 - [ ] writable/readable → $state 在 .svelte.js 文件中 - [ ] 不在普通 .js 文件中使用 runes - [ ] 不使用遗留的 $ 前缀 store 语法 ### SSR vs CSR 边界 - [ ] 不在根 layout 中全局禁用 SSR - [ ] 浏览器 API(window、document)在 onMount 或 browser guard 中使用 - [ ] prerender 页面不包含 server actions ### Svelte 4 → 5 迁移 - [ ] $: → $derived / $effect - [ ] export let → $props() - [ ] on:click → onclick - [ ] createEventDispatcher → 回调 props - [ ] slot → @render children() - [ ] beforeUpdate/afterUpdate → $effect.pre / $effect ### 性能优化 - [ ] 大型不可变数据使用 $state.raw - [ ] {#each} 使用唯一 key - [ ] load 函数返回 Promise 实现流式传输 - [ ] 独立数据并行加载 ### 安全审查 - [ ] 私有环境变量只在 server load 中使用 - [ ] 服务端代码放在 $lib/server/ 目录 - [ ] 不禁用内建 CSRF 防护 - [ ] Cookie 设置 httpOnly、secure、sameSite - [ ] server load 不泄露密钥和内部数据