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.jsvs+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 序列化(JSON + BigInt / Date / Map / Set / RegExp / 循环引用)。可访问 cookies / locals / request / clientAddress / platform。
// +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) };
}
// +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 的返回值才到达页面:
// +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 是旧式等价物):
<!-- 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)。
// 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。
// +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 吃掉)。
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:
import { goto } from '$app/navigation';
goto('/login');
Critical: Streaming with promises
Server load 返回未 await 的 Promise 会被流式传输到浏览器,允许快数据先渲染、慢数据后到。
// +page.server.js —— 关键数据先 await,慢数据后流
export async function load({ params }) {
return {
post: await loadPost(params.slug),
comments: loadComments(params.slug) // 不 await → 单独流
};
}
<!-- +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 重新执行的条件:
- 访问
params某属性且值变了 - 访问
url.pathname/url.search等且值变了 url.searchParams.get/getAll/has对应参数变了await parent()且父 load 重跑了- 通过
fetch(url)或depends(url)声明依赖,且invalidate(url)被调用 invalidateAll()强制重跑所有 active load
// +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() };
}
<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
server load 不会自动依赖 fetch 的 URL(避免泄漏凭据)——必须 depends(url) 显式声明。用 untrack(fn) 排除依赖:
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:
// src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async (event) => { /* ... */ }
};
<form method="POST">
<input name="email">
<input name="password" type="password">
<button>Log in</button>
</form>
Named actions:用 ?/name 区分:
export const actions = {
login: async (event) => { /* ... */ },
register: async (event) => { /* ... */ }
};
<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:
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 };
}
};
Anatomy:action 接收 RequestEvent,可读 request.formData(),可写 cookies,可 return / fail / redirect / error。返回值进 form prop,仅本次响应有效(reload 即消失)。回填安全——只 echo 用户允许的字段,绝不回显密码。
Redirects:和 load 一样用 redirect(status, location)。
Action 之后的 load:action 完成后(除非 redirect / unexpected error)页面会重渲染——load 函数会重跑。handle hook 不会重跑——若你在 handle 里从 cookie 读 locals.user,action 修改 cookie 后必须手动更新 event.locals。
Critical: use:enhance (progressive enhancement)
use:enhance 是 <form> 的 action,给表单加上"无 JS 也工作、有 JS 时更平滑"的能力。只用于 method="POST" + +page.server.js action;用于 +server.js 或 GET 都会报错。
最小用法:
<script>
import { enhance } from '$app/forms';
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" value={form?.email ?? ''}>
</form>
默认行为:模拟浏览器原生但避免整页刷新——更新 form / page.form / page.status(仅同页 action,否则不会更新)、reset <form>、success 时 invalidateAll、redirect 时 goto、error 时渲染最近 +error.svelte、重置焦点。
自定义 SubmitFunction(返回 callback 即覆盖默认 post-submit 行为,要恢复可用 update() 或 applyAction(result)):
<script>
import { enhance, applyAction } from '$app/forms';
import { goto } from '$app/navigation';
let submitting = $state(false);
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
submitting = true;
return async ({ result, update }) => {
submitting = false;
if (result.type === 'redirect') {
goto(result.location, { invalidateAll: true });
} else {
await applyAction(result); // 等价于默认 success/failure/redirect/error 行为
}
};
}}
>
<button disabled={submitting}>Save</button>
</form>
完全手写(无 use:enhance):用 submit 事件 + fetch + deserialize(不能用 JSON.parse,因为 result 含 Date / BigInt):
<script>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
/** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement }} e */
async function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget, e.submitter);
const response = await fetch(e.currentTarget.action, {
method: 'POST', body: data,
headers: { 'x-sveltekit-action': 'true' } // 同名 +server.js 时强制走 action
});
const result = deserialize(await response.text());
if (result.type === 'success') await invalidateAll();
applyAction(result);
}
</script>
<form method="POST"
同路由有 +server.js 时,fetch 默认走 +server.js。要强制 POST 到 action 必须加 header x-sveltekit-action: true。
Critical: Page options (prerender/entries/ssr/csr/trailingSlash)
Page options 控制整页(或子树)的渲染方式。从 +page.js / +page.server.js / +layout.{js,server.js} 导出。子覆盖父——可在根 layout 开 prerender、个别页关闭。
// +page.js / +layout.js / +page.server.js
export const prerender = true; // 构建时生成 HTML
export const prerender = false; // 显式禁用(用于根 layout 开启全部 prerender 的场景)
export const prerender = 'auto'; // 可 prerender 也可 SSR(不写入 manifest 排除)
export const ssr = false; // 仅 CSR——空 shell
export const csr = false; // 不发任何 JS
export const trailingSlash = 'always' | 'never' | 'ignore';
// entries —— 动态路由告诉 prerender 哪些值
// src/routes/blog/[slug]/+page.server.js
/** @type {import('./$types').EntryGenerator} */
export function entries() {
return [{ slug: 'hello-world' }, { slug: 'another-post' }]; // 可 async
}
// config —— adapter-specific
/** @type {import('some-adapter').Config} */
export const config = { runtime: 'edge' };
prerender:内容对所有用户相同(marketing/docs/blog);不适用 cookies/url.searchParams/用户状态/form action(POST 需 server)。动态路由用entries()或kit.prerender.entries。报错 "marked as prerenderable, but were not prerendered" → 加entries/ link / 改'auto'ssr = false:根 layout 设整个 app 变 SPA。ssr = false+csr = false= 什么都不渲染,禁止csr = false:<script>被剥掉,<form>不可用use:enhance,链接变浏览器原生跳转,HMR 失效。开发期csr = dev;保留 HMRtrailingSlash:'never'(默认)/about/→ 301 →/about;'always'prerender 输出about/index.html;'ignore'不推荐(破坏 SEO)config对象顶层 merge(不深 merge)——子 layout / page 只覆盖需要改的 key
Quick Fixes
- 私密 API 暴露凭据 → 改
+page.server.js或 universal 中转不敏感字段 datapropundefined→ 漏let { data } = $props();或类型声明- 流式数据 hydration 丢失 → universal load 的 Promise 不会流传输,改
+page.server.js - streaming 中
setHeaders报错 → 响应开始流后 header 不可改 fail()后 form 字段空 → 第二参必须含回显字段({ email })+value={form?.email ?? ''}- action 改 cookie 后页面还是旧用户 →
handle只跑一次,必须在 action 里手动event.locals.user = ... use:enhance不生效 → 必须是method="POST"+ POST 到+page.server.jsaction- "marked as prerenderable, but not prerendered" → 加
entries()或kit.prerender.entries entries()位置错 → 必须在带动态参数的叶子(+page.js/+page.server.js/+server.js),不是 layoutparent()瀑布 → 不依赖parent()的getData(params)先 await 再await parent()url.hashSSR 阶段 undefined → 服务端无 fragment;改onMount读
Gotchas
- Server load 返回值必须可序列化:
Map/Set/Date/BigInt/RegExp/循环引用 OK(devalue),但不能返回 Svelte 组件 class / class instance(除非 transport hook 自定义) - load 函数应纯净——不要在
+page.server.js顶层let user跨请求共享(多租户长生命周期,状态会泄漏) - 依赖追踪只对顶层 body 生效:await 后的 promise 内访问
params.x不会触发重跑(dev 警告) - searchParams 追踪粒度:
get/getAll/has独立;url.searchParams整体访问等同url.search整串追踪 formprop 仅响应存在——刷新即清空(仅本次提交回执,非持久数据)use:enhance默认不更新跨页form:从/aPOST/b?action时/a的 form 不更新——需applyAction(result)+server.js与+page.server.js同名冲突:fetch('/x')默认走+server.js;POST 到 action 必须加x-sveltekit-action: trueheader+page.js桥接 server load:缺省 layout.js 视为({ data }) => data,自动传 server load 数据- streaming + 重定向冲突:响应开始流后 header 不可改;不能在流出的 promise 内
redirect - Lambda/Firebase 缓冲整页——不持流式响应
trailingSlash: 'ignore'破坏 SEO:/x和/x/是不同 URLcsr = false与 HMR 不兼容:开发期csr = dev;临时开启redirect在try {...}中被 catch——直接redirect()不要包 try- 回显安全:form 数据不要 echo 密码/token,仅回显允许的字段
FAQ
Q: +page.js vs +page.server.js 选哪个? A: 私密凭据/DB/cookies → server;公共 API/不可序列化对象 → universal。两者可同时存在,server 先跑。
Q: page.data vs data prop? A: data prop = 当前组件 + 全部祖先的合并;page.data = 当前页面返回的数据,从任意祖先可读。
Q: 怎么让 load 强制重跑? A: invalidate(url) 精确失效(按 URL 或 depends 标签),或 invalidateAll() 全量。server load 中 fetch(url) 不会自动依赖 URL,必须 depends(url)。
Q: form 提交后 form prop 没了? A: 正常。form prop 只在响应存在时存在;刷新即清空。持久数据应入 DB 后由 load 读。
Q: use:enhance + redirect? A: 默认就调 goto(result.location)。自定义可在 callback 里判 result.type === 'redirect' 后 goto(result.location, { invalidateAll: true })。
Q: prerender /blog/[slug] 动态路由? A: 加 entries() 函数返回 slug 列表,或在 svelte.config.js 的 kit.prerender.entries 配置。
Q: 整个 app 变 SPA? A: 根 +layout.js 设 export const ssr = false;(不推荐 SSG)。
Q: 禁用 JS? A: export const csr = false;——无 hydration、<form> 仍工作(POST 整页刷新)。
Q: 同表单提交到不同 action? A: <button formaction="?/other"> 覆盖 <form action>。
Q: action 数据类型安全? A: /** @satisfies {import('./$types').Actions} */ 注解;form prop 用 ActionData 类型。
Q: streaming 在 serverless 能用吗? A: Lambda/Firebase 缓冲整页;NGINX 需配置不缓冲。
Q: parent() 同步还是异步? A: 必须 await parent()。
Examples & References tables
关键 API 一览
| 概念 | 关键文件 / API |
|---|---|
| Universal load | +page.js / +layout.js PageLoad / LayoutLoad |
| Server load | +page.server.js / +layout.server.js PageServerLoad / LayoutServerLoad |
| 跨组件读数据 | $app/state 的 page / App.PageData |
| URL 数据 | params / route.id / url(hash SSR 不可用) |
| Cookies / Headers | cookies / setHeaders(仅 server) |
| 错误/重定向 | error() / redirect() / goto |
| Streaming | server load 返回未 await 的 Promise |
| 依赖追踪 | depends / untrack / invalidate / invalidateAll |
| Form actions | actions / fail / redirect / form prop |
| Progressive enhancement | enhance / applyAction / deserialize |
| Page options | prerender / entries / ssr / csr / trailingSlash / config |
文件索引
| 文件 | 主题 | 示例数 |
|---|---|---|
examples/load-functions.md |
load 函数全谱 | 17 |
examples/form-actions.md |
form action + use:enhance | 17 |
examples/page-options.md |
page options 全谱 | 12 |
| 文件 | 主题 |
|---|---|
references/load-functions-reference.md |
load 完整 API / 参数 / 返回值 / $types |
references/form-actions-reference.md |
action API / use:enhance 回调 / hook 集成 |
references/page-options-reference.md |
全部 page options / 约束 / 行为 |
references/rerunning-loads-reference.md |
何时重跑 / 手动 invalidate / untrack |