sveltekit-data
SvelteKit 数据加载、表单、页面选项技能。当用户在 SvelteKit 中使用 +page.js/+page.server.js 加载数据、使用 +server.js API 路由、处理表单 actions(默认/具名/GET)、使用 use:enhance 进行渐进增强、配置 prerender/ssr/csr/trailingSlash 页面选项时使用。
SvelteKit 数据加载、表单、页面选项技能。当用户在 SvelteKit 中使用 +page.js/+page.server.js 加载数据、使用 +server.js API 路由、处理表单 actions(默认/具名/GET)、使用 use:enhance 进行渐进增强、配置 prerender/ssr/csr/trailingSlash 页面选项时使用。
本技能覆盖 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)。
+page.js vs +page.server.js 的取舍(隐私凭据、序列化、组合使用)load 函数中使用 params / route / url / fetch / parent / depends / untrackpage.data 在父布局访问子页面数据error(status, msg) / redirect(status, location) 终止 load/actioninvalidate / invalidateAll / untrack 精确控制load / action 中读 cookies / 设响应头+page.server.js 的 actions:默认 action、具名 action、?/name 查询参数、fail() 验证use:enhance 渐进增强表单、回填 form 字段、阻止默认重置、自定义 applyActionprerender = true / entries() / ssr = false / csr = false / trailingSlashSvelteKit 有两种 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() };
}
何时用哪个:
页面和所有祖先 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 }。
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。
只有 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。
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');
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}
强约束:
setHeaders / redirect.catch(() => {}) 防止 unhandled rejection;fetch 由 SvelteKit 自动处理params.x 不会被依赖追踪——必须在顶层 body 访问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 onclick={rerun}>refresh</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}。
+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。
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" onsubmit={handleSubmit}>...</form>
同路由有 +server.js 时,fetch 默认走 +server.js。要强制 POST 到 action 必须加 header x-sveltekit-action: true。
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+page.server.js 或 universal 中转不敏感字段data prop undefined → 漏 let { data } = $props(); 或类型声明+page.server.jssetHeaders 报错 → 响应开始流后 header 不可改fail() 后 form 字段空 → 第二参必须含回显字段({ email })+ value={form?.email ?? ''}handle 只跑一次,必须在 action 里手动 event.locals.user = ...use:enhance 不生效 → 必须是 method="POST" + POST 到 +page.server.js actionentries() 或 kit.prerender.entriesentries() 位置错 → 必须在带动态参数的叶子(+page.js/+page.server.js/+server.js),不是 layoutparent() 瀑布 → 不依赖 parent() 的 getData(params) 先 await 再 await parent()url.hash SSR 阶段 undefined → 服务端无 fragment;改 onMount 读Map/Set/Date/BigInt/RegExp/循环引用 OK(devalue),但不能返回 Svelte 组件 class / class instance(除非 transport hook 自定义)+page.server.js 顶层 let user 跨请求共享(多租户长生命周期,状态会泄漏)params.x 不会触发重跑(dev 警告)get/getAll/has 独立;url.searchParams 整体访问等同 url.search 整串追踪form prop 仅响应存在——刷新即清空(仅本次提交回执,非持久数据)use:enhance 默认不更新跨页 form:从 /a POST /b?action 时 /a 的 form 不更新——需 applyAction(result)+server.js 与 +page.server.js 同名冲突:fetch('/x') 默认走 +server.js;POST 到 action 必须加 x-sveltekit-action: true header+page.js 桥接 server load:缺省 layout.js 视为 ({ data }) => data,自动传 server load 数据redirecttrailingSlash: 'ignore' 破坏 SEO:/x 和 /x/ 是不同 URLcsr = false 与 HMR 不兼容:开发期 csr = dev; 临时开启redirect 在 try {...} 中被 catch——直接 redirect() 不要包 tryQ: +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()。
| 概念 | 关键文件 / 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 |