Files
BOJ/.claude/skills/code-review-skill/reference/svelte.md
T

25 KiB
Raw Blame History

Svelte / SvelteKit Code Review Guide

Svelte 5 / SvelteKit 审查重点:Runes 响应式系统、Server/Client 边界、Form Actions、Store 迁移、以及安全性。

目录


Runes: $state / $derived / $effect

$state 基础用法

<!-- ❌ $state 用于永远不会变化的值 -->
<script lang="ts">
  let config = $state({ timeout: 5000 });  // 不需要响应式
  const API_URL = $state('/api');           // 常量不需要 $state
</script>

<!-- ✅ 常量直接声明 -->
<script lang="ts">
  const config = { timeout: 5000 };
  const API_URL = '/api';

  // $state 只用于会变化的值
  let count = $state(0);
  let user = $state<User | null>(null);
</script>

$state.raw 与大型对象

<!-- ❌ 大型不可变数据使用深度响应式 -->
<script lang="ts">
  // largeData 会被深度代理,性能开销大
  let data = $state(hugeApiResponse);
</script>

<!-- ✅ $state.raw 避免深度代理 -->
<script lang="ts">
  let data = $state.raw(hugeApiResponse);

  // 整体替换时才触发更新
  async function refresh() {
    data = await fetchLatestData();  // ✅ triggers reactivity
  }

  // ❌ 修改嵌套属性不会触发更新
  // data.items[0].name = 'new';  // will NOT re-render
</script>

$state.snapshot 用于外部库

<!-- ❌ 直接将 $state 对象传给外部库 -->
<script lang="ts">
  let state = $state({ x: 0, y: 0 });

  onMount(() => {
    // 外部库可能无法正确处理 Proxy 对象
    chartLibrary.update(state);  // state is a Proxy!
  });
</script>

<!-- ✅ $state.snapshot 获取普通对象副本 -->
<script lang="ts">
  let state = $state({ x: 0, y: 0 });

  onMount(() => {
    // $state.snapshot produces a plain object (Svelte 5)
    chartLibrary.update($state.snapshot(state));
  });
</script>

解构 $state 丢失响应性

<!-- ❌ 解构 $state 对象丢失响应性 -->
<script lang="ts">
  let state = $state({ count: 0, name: 'Svelte' });
  let { count, name } = state;  // count and name are plain values!
</script>
<p>{count}</p>  <!-- ❌ will NOT update when state.count changes -->

<!-- ✅ 直接访问 $state 属性 -->
<script lang="ts">
  let state = $state({ count: 0, name: 'Svelte' });
</script>
<p>{state.count}</p>  <!-- ✅ stays reactive -->

<!-- ✅ 或者单独声明每个 $state -->
<script lang="ts">
  let count = $state(0);
  let name = $state('Svelte');
</script>

$derived 正确用法

<!-- ❌ #1 反模式:用 $effect 做状态同步 -->
<script lang="ts">
  let firstName = $state('John');
  let lastName = $state('Doe');
  let fullName = $state('');

  // 不要用 $effect 来同步派生状态!
  $effect(() => {
    fullName = `${firstName} ${lastName}`;  // unnecessary effect
  });
</script>

<!-- ✅ 使用 $derived 计算派生值 -->
<script lang="ts">
  let firstName = $state('John');
  let lastName = $state('Doe');
  let fullName = $derived(`${firstName} ${lastName}`);
</script>

$derived 中不应有副作用

<!-- ❌ $derived 中产生副作用 -->
<script lang="ts">
  let items = $state<Item[]>([]);
  let count = $derived(() => {
    console.log('recalculating');  // side effect!
    analytics.track('count', items.length);  // side effect!
    return items.length;
  });
</script>

<!-- ✅ $derived 只用于纯计算 -->
<script lang="ts">
  let items = $state<Item[]>([]);
  let count = $derived(items.length);

  // side effects go in $effect
  $effect(() => {
    analytics.track('count', count);
  });
</script>

$effect 正确用法

$effect vs $derived

<!-- ❌ $effect 用于同步状态(第一大反模式) -->
<script lang="ts">
  let searchQuery = $state('');
  let results = $state([]);

  $effect(() => {
    results = searchQuery ? items.filter(i => i.name.includes(searchQuery)) : items;
  });
</script>

<!-- ✅ 使用 $derived -->
<script lang="ts">
  let searchQuery = $state('');
  let results = $derived(
    searchQuery ? items.filter(i => i.name.includes(searchQuery)) : items
  );
</script>

无限循环

<!-- ❌ $effect 中更新自身依赖 → 无限循环 -->
<script lang="ts">
  let count = $state(0);

  $effect(() => {
    console.log(count);
    count++;  // modifying dependency inside effect → infinite loop!
  });
</script>

<!-- ✅ 避免在 $effect 中修改被追踪的状态 -->
<script lang="ts">
  let count = $state(0);
  let log = $state<string[]>([]);

  $effect(() => {
    // read count, write to a different state
    log = [...log, `count is ${count}`];
  });
</script>

清理函数

<!-- ❌ 缺少清理函数 → 内存泄漏 -->
<script lang="ts">
  let roomId = $state('');

  $effect(() => {
    const ws = new WebSocket(`ws://example.com/${roomId}`);
    ws.onmessage = (e) => {
      messages = [...messages, JSON.parse(e.data)];
    };
    // no cleanup! WebSocket leaks when roomId changes
  });
</script>

<!-- ✅ 返回清理函数 -->
<script lang="ts">
  let roomId = $state('');

  $effect(() => {
    const ws = new WebSocket(`ws://example.com/${roomId}`);
    ws.onmessage = (e) => {
      messages = [...messages, JSON.parse(e.data)];
    };
    return () => ws.close();  // cleanup on re-run
  });
</script>

<!-- ✅ 定时器清理 -->
<script lang="ts">
  $effect(() => {
    const id = setInterval(() => {
      console.log('tick');
    }, 1000);
    return () => clearInterval(id);
  });
</script>

async $effect 的追踪陷阱

<!-- ❌ await 后读取的状态不会被追踪 -->
<script lang="ts">
  let userId = $state('1');
  let preference = $state('dark');

  $effect(async () => {
    const user = await fetchUser(userId);   // userId IS tracked
    const theme = preference;               // NOT tracked (read after await)!
    applyTheme(user, theme);
  });
</script>

<!-- ✅ 在 await 前读取所有依赖 -->
<script lang="ts">
  let userId = $state('1');
  let preference = $state('dark');

  $effect(async () => {
    const currentPref = preference;  // read before await
    const user = await fetchUser(userId);
    applyTheme(user, currentPref);
  });
</script>

untrack 排除依赖

<!-- ❌ 不小心追踪了不必要的依赖 -->
<script lang="ts">
  let data = $state<Data | null>(null);
  let debugMode = $state(false);

  $effect(() => {
    if (debugMode) {  // debugMode becomes a dependency!
      console.log('data changed', data);
    }
  });
</script>

<!-- ✅ untrack 排除不相关的依赖 -->
<script lang="ts">
  import { untrack } from 'svelte';

  let data = $state<Data | null>(null);
  let debugMode = $state(false);

  $effect(() => {
    if (untrack(() => debugMode)) {  // debugMode is NOT tracked
      console.log('data changed', data);
    }
  });
</script>

Load 函数(Server vs Client

+page.server.js vs +page.js

// ❌ 在 +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 };
}
// ✅ +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() 瀑布流

// ❌ 顺序 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 };
}

不可序列化的返回值

// ❌ 从 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 处理副作用

<!-- ❌ 用 GET/load 函数处理副作用 -->
<script lang="ts">
  import { goto } from '$app/navigation';

  async function deleteUser(id: string) {
    await fetch(`/api/users/${id}`, { method: 'DELETE' });
    goto('/users');  // side effect via client navigation
  }
</script>
<button onclick={() => deleteUser(user.id)}>Delete</button>

<!-- ✅ 使用 form actions -->
// 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');
  }
};
<!-- form with progressive enhancement -->
<script lang="ts">
  import { enhance } from '$app/forms';
</script>

<form method="POST" action="?/delete" use:enhance>
  <input type="hidden" name="id" value={user.id} />
  <button type="submit">Delete</button>
</form>

fail() 中不暴露敏感信息

// ❌ 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 渐进增强

<!-- ❌ 表单不使用 use:enhance → 没有 JS 时才用原生行为 -->
<form method="POST" action="?/create">
  <input name="title" />
  <button type="submit">Create</button>
</form>

<!-- ✅ use:enhance 提供 SPA 体验 + progressive enhancement -->
<script lang="ts">
  import { enhance } from '$app/forms';
</script>

<form method="POST" action="?/create" use:enhance={() => {
  return ({ update }) => {
    update({ reset: false });  // customize behavior
  };
}}>
  <input name="title" />
  <button type="submit">Create</button>
</form>

<!-- ✅ 带加载状态 -->
<form
  method="POST"
  action="?/create"
  use:enhance={() => {
    submitting = true;
    return ({ update }) => {
      update();
      submitting = false;
    };
  }}
>
  <button type="submit" disabled={submitting}>
    {submitting ? 'Creating...' : 'Create'}
  </button>
</form>

Store 迁移(→ $state

writable/readable → $state

// ❌ 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<User | null>(null);

export function getUser() {
  return currentUser;
}

export function setUser(user: User | null) {
  currentUser = user;
}

export function isLoggedIn() {
  return currentUser !== null;
}

$ 前缀 store 语法是遗留语法

<!-- ❌ $ 前缀 store 自动订阅是遗留模式 -->
<script lang="ts">
  import { count } from '$lib/stores/count';
  // $count is legacy syntax in Svelte 5
</script>
<p>{$count}</p>

<!-- ✅ Svelte 5 runes 模式 -->
<script lang="ts">
  import { getCount } from '$lib/stores/count.svelte';

  let count = $derived(getCount());
</script>
<p>{count}</p>

<!-- ✅ 或者直接用 export 的 $state 响应式 getter -->
<script lang="ts">
  // count.svelte.js exports a reactive reference
  import { counter } from '$lib/stores/count.svelte';
</script>
<p>{counter.value}</p>

.svelte.js / .svelte.ts 扩展名

// ❌ 在普通 .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 模式

// ❌ 在根 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 中

<!-- ❌ 在模块顶层访问浏览器 API -->
<script lang="ts">
  const height = window.innerHeight;        // ReferenceError during SSR!
  const prefersDark = matchMedia('(prefers-color-scheme: dark)');  // crash!
</script>

<!-- ✅ 在 onMount 或 browser guard 中访问 -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { browser } from '$app/environment';

  let height = $state(0);

  onMount(() => {
    height = window.innerHeight;
  });

  // or conditional check
  const prefersDark = browser
    ? matchMedia('(prefers-color-scheme: dark)').matches
    : false;
</script>

prerender 与 actions 冲突

// ❌ 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 4 响应式语句 -->
<script lang="ts">
  let count = 0;
  let doubled = 0;

  $: doubled = count * 2;              // reactive assignment
  $: if (count > 10) console.log('big');
</script>

<!-- ✅ Svelte 5 runes -->
<script lang="ts">
  let count = $state(0);
  let doubled = $derived(count * 2);   // derived value

  $effect(() => {
    if (count > 10) console.log('big');
  });
</script>

export let → $props()

<!-- ❌ Svelte 4 props -->
<script lang="ts">
  export let title: string;
  export let count = 0;
</script>

<!-- ✅ Svelte 5 $props() -->
<script lang="ts">
  let { title, count = 0 }: { title: string; count?: number } = $props();
</script>

on:click → onclick

<!-- ❌ Svelte 4 指令式事件 -->
<button on:click={handleClick}>Click</button>
<button on:click={() => count++}>Increment</button>

<!-- ✅ Svelte 5 HTML 属性式事件 -->
<button onclick={handleClick}>Click</button>
<button onclick={() => count++}>Increment</button>

createEventDispatcher → 回调 props

<!-- ❌ Svelte 4 事件 dispatch -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  function handleDelete() {
    dispatch('delete', { id: 42 });
  }
</script>

<!-- ✅ Svelte 5 回调 props -->
<script lang="ts">
  let { ondelete }: { ondelete?: (e: { id: number }) => void } = $props();

  function handleDelete() {
    ondelete?.({ id: 42 });
  }
</script>

<!-- parent usage -->
<Child ondelete={(e) => removeItem(e.id)} />

slot → @render children()

<!-- ❌ Svelte 4 slot -->
<!-- Card.svelte -->
<div class="card">
  <slot />
</div>

<!-- ✅ Svelte 5 snippets -->
<!-- Card.svelte -->
<script lang="ts">
  let { children } = $props();
</script>
<div class="card">
  {@render children()}
</div>

<!-- with named slots → named snippets -->
<!-- Layout.svelte -->
<script lang="ts">
  let { header, children, footer } = $props();
</script>
<div>
  <header>{@render header?.()}</header>
  <main>{@render children()}</main>
  <footer>{@render footer?.()}</footer>
</div>

<!-- parent usage -->
<Layout>
  {#snippet header()}<h1>Title</h1>{/snippet}
  <p>Body content</p>
  {#snippet footer()}<p>Footer</p>{/snippet}
</Layout>

beforeUpdate / afterUpdate → $effect.pre

<!-- ❌ Svelte 4 lifecycle hooks -->
<script lang="ts">
  import { beforeUpdate, afterUpdate } from 'svelte';

  let count = 0;

  beforeUpdate(() => {
    console.log('about to update', count);
  });

  afterUpdate(() => {
    console.log('updated', count);
    document.title = `Count: ${count}`;
  });
</script>

<!-- ✅ Svelte 5 $effect and $effect.pre -->
<script lang="ts">
  let count = $state(0);

  // $effect.pre runs before DOM updates (like beforeUpdate)
  $effect.pre(() => {
    console.log('about to update', count);
  });

  // $effect runs after DOM updates (like afterUpdate)
  $effect(() => {
    console.log('updated', count);
    document.title = `Count: ${count}`;
  });
</script>

性能优化

$state.raw 用于大型不可变数据

<!-- ❌ 深度代理大型不可变数据 -->
<script lang="ts">
  let searchResults = $state(largeResultArray);  // deep proxy on every item
</script>

<!-- ✅ $state.raw 避免深度代理 -->
<script lang="ts">
  let searchResults = $state.raw<SearchResult[]>([]);

  async function search(query: string) {
    searchResults = await fetchResults(query);  // whole-array replacement
  }
</script>

Keyed

<!-- ❌ 无 key 的 each → 低效 DOM diff -->
{#each items as item}
  <div>{item.name}</div>
{/each}

<!-- ✅ 带唯一 key 的 each -->
{#each items as item (item.id)}
  <div>{item.name}</div>
{/each}

<!-- ✅ 复合 key -->
{#each items as item (`${item.category}-${item.id}`)}
  <div>{item.name}</div>
{/each}

Streaming 与 load 中的 Promise

// ❌ 串行等待所有数据 → 页面阻塞
// src/routes/+page.server.js
export async function load({ params }) {
  const posts = await getPosts();       // slow
  const comments = await getComments(); // slow
  const tags = await getTags();         // slow
  return { posts, comments, tags };
}

// ✅ 并行加载独立数据
export async function load({ params }) {
  return {
    posts: getPosts(),       // return promises directly for streaming
    comments: getComments(),
    tags: getTags(),
  };
}
<!-- streaming in template with {#await} -->
{#await data.posts}
  <p>Loading posts...</p>
{:then posts}
  <ul>
    {#each posts as post (post.id)}
      <li>{post.title}</li>
    {/each}
  </ul>
{:catch error}
  <p>Failed to load posts: {error.message}</p>
{/await}

安全审查

不暴露私有环境变量

// ❌ 在 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/ 服务端隔离

// ❌ 服务端代码放在可被客户端导入的位置
// 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 内建防护

// ✅ 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 设置
// 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
  • 浏览器 APIwindow、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 不泄露密钥和内部数据