- name
- sveltekit-data
- license
- Apache-2.0
- description
- SvelteKit 数据加载、表单、页面选项技能。当用户在 SvelteKit 中使用 +page.js/+page.server.js 加载数据、使用 +server.js API 路由、处理表单 actions(默认/具名/GET)、使用 use:enhance 进行渐进增强、配置 prerender/ssr/csr/trailingSlash 页面选项时使用。
# SvelteKit Data: Loading, Form Actions, Page Options (SvelteKit 2)
本技能覆盖 SvelteKit 数据层的三大支柱:(1) Loading data(`+page.js` / `+page.server.js` / `+layout.js` / `+layout.server.js` 的 `load` 函数、`page.data`、URL 数据、cookies/headers、错误与重定向、流式响应、依赖追踪与手动失效),(2) Form actions(默认/具名 actions、`fail` 验证、`use:enhance` 渐进增强、`GET` vs `POST`),(3) Page options(`prerender` / `entries` / `ssr` / `csr` / `trailingSlash` / `config`)。
## When to use this skill
- 决定 `+page.js` vs `+page.server.js` 的取舍(隐私凭据、序列化、组合使用)
- 在 `load` 函数中使用 `params` / `route` / `url` / `fetch` / `parent` / `depends` / `untrack`
- 用 `page.data` 在父布局访问子页面数据
- 用 `error(status, msg)` / `redirect(status, location)` 终止 load/action
- 用返回 Promise 实现 streaming 骨架屏
- 解决"load 函数何时重新执行"以及如何用 `invalidate` / `invalidateAll` / `untrack` 精确控制
- 在 server `load` / action 中读 `cookies` / 设响应头
- 设计 `+page.server.js` 的 actions:默认 action、具名 action、`?/name` 查询参数、`fail()` 验证
- 用 `use:enhance` 渐进增强表单、回填 `form` 字段、阻止默认重置、自定义 `applyAction`
- 决定哪些路由 `prerender = true` / `entries()` / `ssr = false` / `csr = false` / `trailingSlash`
## Critical: Loading data (universal vs server load functions)
SvelteKit 有两种 `load` 函数,运行位置和约束不同。
**Universal load**(`+page.js` / `+layout.js`):默认 SSR 时在服务端跑一次、客户端 hydration 时再跑一次;之后所有导航都在浏览器内运行。可以返回**任意 JS 值**(包括 Svelte 组件构造函数等不可序列化的对象)。`fetch` 在 SSR 阶段内联到 HTML,hydration 时复用,不会泄漏私密凭据到客户端。
**Server load**(`+page.server.js` / `+layout.server.js`):**永远在服务端跑**。返回值必须用 [devalue](https://github.com/rich-harris/devalue) 序列化(JSON + `BigInt` / `Date` / `Map` / `Set` / `RegExp` / 循环引用)。可访问 `cookies` / `locals` / `request` / `clientAddress` / `platform`。
```js
// +page.server.js
import * as db from '$lib/server/database';
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return { post: await db.getPost(params.slug) };
}
```
```js
// +page.js —— 公共 API fetch,浏览器可直接拉
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, params }) {
const res = await fetch(`/api/items/${params.id}`);
return { item: await res.json() };
}
```
**两者可以同时存在**。当同时存在时,server load 先跑,其返回值成为 universal load 的 `data` 参数;universal load 的返回值才到达页面:
```js
// +page.server.js —— 仅在服务端,返回 sessionId
export async function load({ locals }) {
return { sessionId: locals.sessionId };
}
// +page.js —— 浏览器也能跑,但能拿到服务端上下文
export async function load({ data, fetch }) {
const res = await fetch(`/api/me`, { headers: { 'x-session': data.sessionId } });
return { me: await res.json() };
}
```
**何时用哪个**:
- 需要私密环境变量、数据库、文件系统 → server load
- 数据是公开 API、且希望减少服务器往返 → universal load
- 需要返回不可序列化对象(Svelte 组件 class) → universal load
- 需要 streaming(慢数据 + 骨架屏) → **必须 server load**(universal load 的 Promise 不会被流式传输)
- 需要在客户端也能重新跑 → universal load
## Critical: page.data (sharing data across components)
页面和所有祖先 layout 各有**自己的** `data` prop,包含它自己 + 全部祖先的合并数据。当**父**组件需要**子**组件返回的数据时,用 `$app/state` 的 `page.data`(`$app/stores` 的 `$page` 是旧式等价物):
```svelte
<!-- src/routes/+layout.svelte -->
<script>
import { page } from '$app/state';
/** @type {import('./$types').LayoutProps} */
let { data, children } = $props();
</script>
<svelte:head>
<title>{page.data.title}</title>
</svelte:head>
{@render children()}
```
合并规则:同级出现相同 key 时**后者覆盖**。`+layout.js` 返回 `{ a:1, b:2 }` + `+page.js` 返回 `{ b:3, c:4 }` → `data = { a:1, b:3, c:4 }`。
## Critical: URL data (params/route/url)
`load` 函数通过 `url` / `route` / `params` 访问 URL。`url.hash` 在 SSR 阶段不可用(服务端无 fragment)。
```js
// src/routes/a/[b]/[...c]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ url, route, params }) {
// route.id = '/a/[b]/[...c]'
// url = URL 实例(origin/pathname/searchParams/...)
// params.b = 'x' params.c = 'y/z'
return { q: url.searchParams.get('q') };
}
```
`url.searchParams.get/getAll/has` 在依赖追踪中是**独立**的 key——`?x=1&y=1` → `?x=1&y=2` **不会**重跑只依赖 `y` 的 load。
## Critical: Cookies and Headers
**只有 server load** 可以读 / 写 `cookies`。`setHeaders` 在 universal load 中调用 SSR 时生效,浏览器内调用是 no-op。
```js
// +layout.server.js
export async function load({ cookies }) {
const sessionid = cookies.get('sessionid');
return { user: await db.getUser(sessionid) };
}
// +page.js —— 转发上游 cache 头
export async function load({ fetch, setHeaders }) {
const response = await fetch('https://cms.example.com/products.json');
setHeaders({ 'cache-control': response.headers.get('cache-control') });
return response.json();
}
```
约束:`setHeaders` 同名 header 只能设一次;不能用 `setHeaders` 设 `set-cookie`(用 `cookies.set`);`fetch` 只在**同源或子域**目标带 cookies,其他域用 `handleFetch` hook。
## Critical: Errors and Redirects
`error(status, message)` 和 `redirect(status, location)` 都**直接抛异常**——不要自己 `throw`(SvelteKit 1.x 行为已废弃)。`redirect` **不要放在 try 块里**(会被 catch 吃掉)。
```js
import { error, redirect } from '@sveltejs/kit';
export function load({ locals }) {
if (!locals.user) error(401, 'not logged in');
if (!locals.user.isAdmin) error(403, 'not an admin');
}
export function load({ locals, url }) {
if (!locals.user) {
const next = url.pathname + url.search;
redirect(303, `/login?redirectTo=${encodeURIComponent(next)}`);
}
}
```
`expected error`(用 `error()` 抛出)显示最近 `+error.svelte` 并带正确 status;`unexpected error` 触发 `handleError` hook,按 500 处理。
浏览器端导航用 `$app/navigation` 的 `goto`:
```js
import { goto } from '$app/navigation';
goto('/login');
```
## Critical: Streaming with promises
Server `load` 返回**未 await 的 Promise** 会被流式传输到浏览器,允许快数据先渲染、慢数据后到。
```js
// +page.server.js —— 关键数据先 await,慢数据后流
export async function load({ params }) {
return {
post: await loadPost(params.slug),
comments: loadComments(params.slug) // 不 await → 单独流
};
}
```
```svelte
<!-- +page.svelte -->
<h1>{data.post.title}</h1>
{#await data.comments}
<p>Loading comments...</p>
{:then comments}
{#each comments as c}<p>{c.content}</p>{/each}
{:catch error}
<p>error: {error.message}</p>
{/await}
```
**强约束**:
- streaming 仅在 JS 启用且非 Lambda/Firebase 等缓冲平台时生效
- 响应开始流式输出后**不能**再 `setHeaders` / `redirect`
- 手写 Promise 加 `.catch(() => {})` 防止 unhandled rejection;`fetch` 由 SvelteKit 自动处理
- 嵌套 promise 内的 `params.x` 不会被依赖追踪——必须在顶层 body 访问
## Critical: When load functions rerun
SvelteKit 跟踪每个 load 的依赖以避免重跑。`load` 重新执行的条件:
1. 访问 `params` 某属性且值变了
2. 访问 `url.pathname` / `url.search` 等且值变了
3. `url.searchParams.get/getAll/has` 对应参数变了
4. `await parent()` 且父 load 重跑了
5. 通过 `fetch(url)` 或 `depends(url)` 声明依赖,且 `invalidate(url)` 被调用
6. `invalidateAll()` 强制重跑所有 active load
```js
// +page.js —— 自定义依赖标签(约定 [a-z]: 前缀)
export async function load({ fetch, depends }) {
depends('app:random');
const r = await fetch('https://api.example.com/random-number');
return { number: await r.json() };
}
```
```svelte
<script>
import { invalidate, invalidateAll } from '$app/navigation';
function rerun() {
invalidate('app:random');
invalidate('https://api.example.com/random-number');
invalidate(url => url.href.includes('random-number'));
invalidateAll();
}
</script>
<button onclick={rerun}>refresh</button>
```
**server load 不会**自动依赖 fetch 的 URL(避免泄漏凭据)——必须 `depends(url)` 显式声明。用 `untrack(fn)` 排除依赖:
```js
export async function load({ untrack, url }) {
if (untrack(() => url.pathname === '/')) return { message: 'Welcome!' };
}
```
**重跑 ≠ 重建组件**。`+layout.svelte` / `+page.svelte` 实例保留,只有 `data` prop 更新,组件内部 state 保留。需要强制重建 → 用 `{#key page.url.pathname}`。
## Critical: Form actions (default/named, validation, redirects)
`+page.server.js` 导出 `actions` 对象提供 `<form>` 端点。**action 总是 POST**(GET 不应有副作用)。
**Default action**:
```js
// src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async (event) => { /* ... */ }
};
```
```svelte
<form method="POST">
<input name="email">
<input name="password" type="password">
<button>Log in</button>
</form>
```
**Named actions**:用 `?/name` 区分:
```js
export const actions = {
login: async (event) => { /* ... */ },
register: async (event) => { /* ... */ }
};
```
```svelte
<form method="POST" action="?/login">...</form>
<form method="POST" action="/login?/register">...</form> <!-- 跨页调用 -->
<!-- 同表单不同按钮 -->
<form method="POST" action="?/login">
<button>Login</button>
<button formaction="?/register">Register</button>
</form>
```
**重要**:default + named 不能共存——若 POST 具名 action 不 redirect,`?/name` 留在 URL 里,下次 default POST 也会命中它。
**Validation errors**:`fail(status, data)` 返回 status + 数据,data 进 `form` prop / `page.form` / `page.status`:
```js
import { fail } from '@sveltejs/kit';
export const actions = {
login: async ({ cookies, request }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email) return fail(400, { email, missing: true });
const user = await db.getUser(email);
if (!user || user.password !== db.hash(password)) {
return fail(400, { email, incorrect: true });
}
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true };
}
};
عرض على GitHub