mcpField
S
sveltekit-data

sveltekit-data

github.com/full-stack-skillsDevelopment472 installsUpdated 5w ago
Repository

About sveltekit-data

A SvelteKit 2 skill for developers working with route data, form actions, and page options. It explains server and universal loading, streaming, invalidation, and rendering configuration.

Key features

  1. 01Universal and server load functions
  2. 02Data sharing and URL dependencies
  3. 03Streaming and load invalidation
  4. 04Form actions and validation
  5. 05Progressive enhancement with use:enhance
  6. 06Prerendering and rendering options

Use cases

  1. 01Choose server or universal loading
  2. 02Stream slow data alongside page content
  3. 03Build and validate POST forms
  4. 04Configure prerendering, SSR, and CSR

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/full-stack-skills/svelte-skills --skill sveltekit-data

After installing, type/sveltekit-datain Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

agents/
openai.yaml
examples/
form-actions.md
load-functions.md
page-options.md
README.md
references/
form-actions-reference.md
load-functions-reference.md
page-options-reference.md
README.md
rerunning-loads-reference.md

Details

From the project README

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 序列化(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 重新执行的条件:

  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
// +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}。

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" onsubmit={handleSubmit}>...</form>

同路由有 +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; 保留 HMR
  • trailingSlash:'never'(默认)/about/ → 301 → /about;'always' prerender 输出 about/index.html;'ignore' 不推荐(破坏 SEO)
  • config 对象顶层 merge(不深 merge)——子 layout / page 只覆盖需要改的 key

Quick Fixes

  • 私密 API 暴露凭据 → 改 +page.server.js 或 universal 中转不敏感字段
  • data prop undefined → 漏 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.js action
  • "marked as prerenderable, but not prerendered" → 加 entries() 或 kit.prerender.entries
  • entries() 位置错 → 必须在带动态参数的叶子(+page.js/+page.server.js/+server.js),不是 layout
  • parent() 瀑布 → 不依赖 parent() 的 getData(params) 先 await 再 await parent()
  • url.hash SSR 阶段 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 整串追踪
  • 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 数据
  • streaming + 重定向冲突:响应开始流后 header 不可改;不能在流出的 promise 内 redirect
  • Lambda/Firebase 缓冲整页——不持流式响应
  • trailingSlash: 'ignore' 破坏 SEO:/x 和 /x/ 是不同 URL
  • csr = 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 / Headerscookies / setHeaders(仅 server)
错误/重定向error() / redirect() / goto
Streamingserver load 返回未 await 的 Promise
依赖追踪depends / untrack / invalidate / invalidateAll
Form actionsactions / fail / redirect / form prop
Progressive enhancementenhance / applyAction / deserialize
Page optionsprerender / entries / ssr / csr / trailingSlash / config
文件索引
文件主题示例数
examples/load-functions.mdload 函数全谱17
examples/form-actions.mdform action + use:enhance17
examples/page-options.mdpage options 全谱12
文件主题
references/load-functions-reference.mdload 完整 API / 参数 / 返回值 / $types
references/form-actions-reference.mdaction API / use:enhance 回调 / hook 集成
references/page-options-reference.md全部 page options / 约束 / 行为
references/rerunning-loads-reference.md何时重跑 / 手动 invalidate / untrack

Comments

Markdown supported