SvelteKit 路由与数据加载:load 函数详解

要点

这是 Svelte 系列的第二篇。开山篇《Svelte 入门》讲清了编译时框架与 runes 响应式,本篇往下走一层:SvelteKit 2 如何用文件树表达路由,以及全栈框架最容易踩坑的部分——load 函数的数据加载。看完这篇你应该能回答:路由优先级怎么定、universal load 与 server load 怎么分工、什么会触发 load 重跑、为什么必须用 event.fetch 而不是全局 fetch。版本锚点:截至本文(2025 年 10 月底),稳定线为 Svelte 5.43.x 与 SvelteKit 2.48.x,全文均以此为准。

路由是文件树的投影

SvelteKit 没有路由配置文件,src/routes 目录本身就是路由表。目录是路径段,特殊的 + 前缀文件承担不同职责:

src/routes/
├── +layout.svelte        # 根布局:每个页面都会套上它
├── +page.svelte          # / 首页
├── about/
│   └── +page.svelte      # /about
├── blog/
│   ├── +page.svelte      # /blog 列表页
│   └── [slug]/
│       └── +page.svelte  # /blog/hello 动态详情页

只有带 + 前缀的文件参与路由,其余文件(工具函数、组件)放在同级目录里就是普通私有模块——惯用做法是放进 src/lib(有内置别名 $lib)。各文件分工如下表:

文件 职责 运行位置
+page.svelte 页面 UI 服务端首渲 + 客户端
+page.js universal load,取数后交给页面 服务端与客户端都跑
+page.server.js server load 与表单 action,可碰密钥 仅服务端
+layout.svelte 嵌套布局,包裹子路由 服务端首渲 + 客户端
+layout.server.js 整棵子树共享的 server 数据 仅服务端
+server.js 纯 API 端点(GET/POST 等处理器) 仅服务端
+error.svelte 错误兜底页,可放在任意层级 服务端首渲 + 客户端

动态段与匹配优先级

方括号目录是动态段,五种写法覆盖绝大多数场景:

写法 匹配 示例
[slug] 一段任意内容 /blog/hello
[[optional]] 可有可无的一段 /shop 与 /shop/electronics 同一组件
[…rest] 剩余全部路径(含多段) 兜底 404 页、深层 catch-all
[id=integer] 带匹配器的段,只匹配数字 /post/42 命中、/post/abc 不命中
(marketing) 分组目录,不进入 URL 同一 URL 换一套布局

匹配器是 src/params/ 下的一个模块,适合在路由层就把非法参数挡掉,而不是进页面再校验:

// src/params/integer.js —— 拒绝非数字,路由直接 404
/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param) {
  return /^\d+$/.test(param);
}

同一位置多个路由都命中时,优先级从高到低是:静态段、带匹配器的动态段、普通动态段、rest 段。这个排序直觉上就是”越具体越优先”,边界情况以官方文档 routing 章节为准。分组布局 (marketing) 解决的是经典问题:/about 要营销页头,/dashboard 要侧边栏,两者互斥——把它们放进不同组,各自写 +layout.svelte,URL 不受影响。

布局的嵌套与”不重置”

布局沿目录层层嵌套,Svelte 5 用 snippet 接收子内容——子页面渲染在 children 里:

<!-- src/routes/+layout.svelte -->
<script>
  let { children } = $props();   // Svelte 5 写法;旧 slot 语法已让位
</script>

<nav><a href="/">首页</a></nav>
{@render children()}

一个值得记住的性质:导航只在同层级换页时替换 page 部分,layout 组件实例是保留的——里面的 $state 不会因为跳转子路由而被重置。侧边栏的展开状态、滚动位置因此天然存活;反过来,想在每次导航都重算的数据不要放布局里,要放 load。出错时的兜底也按层级生效:/blog/[slug]/+error.svelte 只接管该子树的异常,根部的 +error.svelte 接管其余。

两个 load:universal 与 server

数据加载的核心是 load 函数,它有两种载体,分工是整个 SvelteKit 数据流的第一决策点:

关注点 +page.js(universal load) +page.server.js(server load)
运行环境 服务端首渲时跑一次,之后客户端导航时在浏览器跑 永远只在服务端跑
能碰什么 公开数据、CMS API 数据库、 cookies、私有环境变量
返回值 可序列化(经 devalue 编码传给客户端) 必须可序列化,不能含函数或类实例
典型用途 调公共第三方 API、想省一次服务端往返 拉需要鉴权的数据、渲染前取私密内容

只有 +page.server.js 时数据在服务端取好随 HTML 到达;只有 +page.js 时服务端也执行它(保证首屏有数据),后续导航在浏览器执行。两者并存时 SvelteKit 会等 server load 完成后,把其结果通过 parent() 供给 universal load。load 收到的 event 参数常用成员如下:

成员 用途 备注
event.params 路径动态段,如 { slug: ‘hello’ } 匹配器已保证格式
event.url 当前 URL 对象 读 searchParams 会影响重跑规则,见下节
event.fetch 带凭证与去重的 fetch,见下节 不要换成全局 fetch
event.cookies 读写 Cookie(server load 专属) 写即向响应附加 Set-Cookie
event.locals hooks 里挂载的请求级数据(如登录用户) server load 专属
event.parent() 取上级 load 的数据 注意别造成请求瀑布
event.depends() 声明自定义依赖,配合 invalidate 重跑 轮询、实时数据用

server load 的典型样子:

// src/routes/blog/[slug]/+page.server.js
import { error } from '@sveltejs/kit';

export async function load({ params, locals }) {
  const post = await locals.db.getPost(params.slug); // 数据库只在服务端碰
  if (!post) error(404, '文章不存在');   // SvelteKit 2 中 error 是抛出式函数
  return { post };   // 经序列化传给 +page.svelte,组件用 let { data } = $props() 接
}

什么会触发 load 重跑

SvelteKit 会追踪每个 load 的依赖,导航时只重跑”依赖变了”的 load。规则里最容易踩的是查询串(query string):

  • universal load:只有当它实际读过 url.searchParams(get/getAll/has),对应参数变化才会触发重跑——读没读,SvelteKit 是靠追踪知道的。
  • server load:客户端导航时不会因为查询串变化而重跑,重跑条件是 params、depends() 声明的依赖或父级数据变化。

反模式点名:在 server load 里读查询串做筛选,然后期待用户切换筛选条件时数据自动刷新。客户端侧导航不会重跑它,页面看起来”没反应”。两条正路:筛选条件放进 universal load 读(读 url.searchParams 即建立依赖),或在交互后调用 invalidate(url) / invalidateAll() 手动触发。同理,路径参数变化一定会重跑 load,所以 /blog/a 跳 /blog/b 不需要任何手动刷新代码——有人在组件里用 $effect 监听参数再发请求,把 SSR 白做掉,还多一次客户端往返,这是第二个反模式。

event.fetch 而不是 fetch

load 里的 HTTP 请求必须用 event.fetch,不用全局 fetch,换来的三件事都是实打实的:相对 URL 可用(全局 fetch 在服务端没有 origin,相对路径直接抛错);Cookie 与凭证自动透传(用户身份跟着请求走);对站内 +server.js 端点的请求在 SSR 时直接调用处理函数,不经过真实网络往返。第三点经常被低估——你的页面 load 与数据端点各自独立开发,SSR 时却能合并成一次进程内调用。

流式:慢数据不必阻塞首屏

server load 的返回值里可以放未完成的 Promise,SvelteKit 会先把页面骨架随流式 HTML 发出,慢数据就绪后再补上:

export async function load({ locals }) {
  return {
    user: await locals.getUser(),        // 快,阻塞渲染
    stats: locals.db.slowAggregate()     // 慢,作为 Promise 直接种进 data
  };
}
<!-- +page.svelte:流式数据用 await 语法等它 -->
{#await data.stats}
  <p>统计生成中…</p>
{:then s}
  <p>共 {s.total} 条</p>
{/await}

边界要清楚:流开始后无法再改响应头和状态码,流中才发生的错误只能在 +error.svelte 边界里呈现,SEO 上流式内容对爬虫可见性也不如静态内容——所以只把”可迟到的数据”做成流式。

下一篇与延伸

路由与 load 之外,SvelteKit 还有第三块拼图:表单 action 与渐进增强(无 JavaScript 也能提交),那部分在本系列下一篇实战里落地——《用 SvelteKit 做一个 Markdown 片段收藏夹》(2025-11-06 发布)会把本篇的 load 知识用起来。另外,SvelteKit 2.27 起提供了实验性的 remote functions(.remote.js 文件里的函数永远在服务端执行),目标是替代手写端点的样板代码,截至本文仍是实验特性,生产项目以官方文档为准。常用 API 的速查版,见《Svelte / SvelteKit 基础知识参考字典》。

总结:本篇给了你 routes 文件树、动态段与优先级、两种 load 的分工、重跑规则与 event.fetch 五块认知。如果项目是纯静态内容站,server load 与 cookies 用不上,考虑直接 prerender;如果异步基础还不牢,先补Node.js 异步与 Promise——load 的心智全建立在 async/await 上。

版本核实(以官方文档为准):截至本文(2025-10-30)Svelte 稳定线为 5.43.x、SvelteKit 稳定线为 2.48.x;$app/state 自 SvelteKit 2.12(2024-12)取代 $app/stores;remote functions 自 SvelteKit 2.27(2025-07)起为实验特性;error()/redirect()/fail() 为 SvelteKit 2 抛出式 API。安装命令不钉版本号。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇