文件系统路由
路由文件约定
SvelteKit 使用 src/routes/ 目录下的文件结构来定义路由。以 + 开头的文件名是路由相关的特殊文件,普通文件(组件、工具函数等)不会成为路由:
src/routes/
├── +page.svelte → /
├── +layout.svelte → 全局布局(包裹所有页面)
├── +error.svelte → 全局错误页
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog(博客列表)
│ ├── +layout.svelte → /blog/* 共享布局
│ └── [slug]/
│ └── +page.svelte → /blog/:slug(博客详情)
├── (auth)/ → 路由组(不影响 URL)
│ ├── login/
│ │ └── +page.svelte → /login
│ └── register/
│ └── +page.svelte → /register
└── api/
└── users/
└── +server.ts → /api/users(API 端点)
+page.svelte — 页面组件
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
// data 来自 +page.server.ts 的 load 函数
let { data } = $props<{ data: PageData }>();
</script>
<svelte:head>
<title>首页 - 我的网站</title>
<meta name="description" content="网站首页" />
</svelte:head>
<h1>欢迎来到 {data.siteName}</h1>
+layout.svelte — 共享布局
<!-- src/routes/+layout.svelte(全局布局)-->
<script lang="ts">
import type { Snippet } from 'svelte';
import Nav from '$lib/Nav.svelte';
import Footer from '$lib/Footer.svelte';
let { children } = $props<{ children: Snippet }>();
</script>
<Nav />
<main>
{@render children()} <!-- 渲染当前页面 -->
</main>
<Footer />
+error.svelte — 错误页面
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message}</h1>
{#if $page.status === 404}
<p>页面不存在,<a href="/">返回首页</a></p>
{:else}
<p>发生了一个错误,请稍后重试。</p>
{/if}
动态路由
[slug] 动态参数
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data } = $props<{ data: PageData }>();
</script>
<article>
<h1>{data.post.title}</h1>
{@html data.post.content}
</article>
<!-- src/routes/blog/[slug]/+page.server.ts -->
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params }) => {
// params.slug 是 URL 中 [slug] 的实际值
const post = await getPost(params.slug);
if (!post) {
error(404, '文章不存在');
}
return { post };
};
可选参数与 Rest 参数
# 可选参数:[[lang]] 使参数变为可选
src/routes/[[lang]]/about/+page.svelte
# 匹配 /about 和 /en/about 和 /zh/about
# Rest 参数:[...path] 匹配多层路径
src/routes/docs/[...slug]/+page.svelte
# 匹配 /docs/intro 和 /docs/api/rest/get
编程式导航
<script lang="ts">
import { goto, invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
async function handleLogin() {
await loginUser();
// 导航到指定路径
await goto('/dashboard');
// 替换历史记录而不是推入(类似 replace state)
await goto('/dashboard', { replaceState: true });
// 保留当前滚动位置
await goto('/dashboard', { noScroll: true });
}
// 读取当前路由信息
$effect(() => {
console.log('当前路径:', $page.url.pathname);
console.log('路由参数:', $page.params);
console.log('路由数据:', $page.data);
});
</script>
<!-- 声明式导航 -->
<a href="/about">关于</a>
<a href="/blog/{post.slug}">{post.title}</a>
<!-- 程序控制 -->
<button onclick={handleLogin}>登录</button>
路由组 (group) 的用途
用圆括号包裹的目录名(如 (auth)、(marketing))会创建路由组:目录名不出现在 URL 中,但可以为该组内的路由应用不同的 +layout.svelte,非常适合区分需要认证和不需要认证的页面布局。
hooks.server.ts:服务端中间件
SvelteKit 的 src/hooks.server.ts 文件提供服务端请求生命周期钩子,用于实现认证、日志、性能监控等横切关注点。
handle 钩子
拦截所有服务端请求,可以在 resolve 前后添加逻辑。通过向
event.locals 写入数据,在 load 函数和 Action 中读取认证信息。类似于 Express 的 middleware 或 Next.js 的 middleware.ts。event.locals
本次请求上下文中的自定义数据存储。由 handle 钩子填充(如解析 JWT 获得的用户对象),在该请求的所有 load 函数和 Action 中通过
locals 参数读取。sequence()
将多个 handle 函数串联,类似于多个 Express 中间件按顺序执行。
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';
import jwt from 'jsonwebtoken';
// handle1:认证中间件——解析 JWT 并设置 locals.user
const auth: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session_token');
if (token) {
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any;
// 将用户信息存入 locals,load 函数通过 locals.user 访问
event.locals.user = {
id: payload.userId,
email: payload.email,
role: payload.role
};
} catch {
// token 无效或过期,清除 cookie
event.cookies.delete('session_token', { path: '/' });
}
}
return resolve(event);
};
// handle2:性能日志中间件
const logger: Handle = async ({ event, resolve }) => {
const start = Date.now();
const response = await resolve(event);
const duration = Date.now() - start;
if (duration > 500) {
console.warn(`[SLOW] ${event.request.method} ${event.url.pathname} - ${duration}ms`);
}
response.headers.set('X-Response-Time', `${duration}ms`);
return response;
};
// handle3:内容安全策略(CSP)
const security: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'"
);
return response;
};
// 串联多个中间件(按顺序执行:auth → logger → security)
export const handle = sequence(auth, logger, security);
// src/app.d.ts:扩展 locals 类型定义
declare global {
namespace App {
// 定义 event.locals 的类型,使 TypeScript 能推断其结构
interface Locals {
user: {
id: string;
email: string;
role: 'admin' | 'user';
} | null;
}
}
}
export {};
// +page.server.ts:使用 locals 中的认证信息
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
// locals.user 由 handle 钩子填充,类型安全
if (!locals.user) {
throw redirect(303, '/login');
}
if (locals.user.role !== 'admin') {
error(403, '需要管理员权限');
}
return { user: locals.user };
};
API 路由 (+server.ts)
// src/routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const users = await getUsers({ page });
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
const user = await createUser(body);
return json(user, { status: 201 });
};
SvelteKit 路由的工作原理
文件系统路由如何映射到 HTTP 请求
理解 SvelteKit 内部如何将文件路径转化为路由处理器,有助于正确设计项目结构和排查路由问题:
路由识别顺序(Route Resolution Order)
当一个 URL 请求到来时,SvelteKit 按以下优先级匹配路由:精确静态路径(
/blog/about)> 带参数路径(/blog/[slug])> 通配符路径(/blog/[...rest])。这意味着静态路由永远比动态路由优先,你不需要担心 /blog/about 被 /blog/[slug] 错误匹配。+layout.svelte 的嵌套机制
所有 +layout.svelte 文件形成嵌套结构。每个 +page.svelte 会被其所有上级目录的 layout 包裹,最外层是根
src/routes/+layout.svelte。这不只是"视觉嵌套",数据也会从父 layout 向下流动(父 layout 的 load 函数返回的 data 子页面可以访问)。+page.server.ts vs +page.ts 的执行位置
+page.server.ts 只在服务器端执行(包含 SSR 时的服务端和 API 路由),可以安全访问数据库、读取环境变量、使用 Node.js API。+page.ts 在服务端和客户端都执行(SSR 时在服务端,CSR 导航时在客户端),不能访问数据库,适合获取公开 API 数据。路由组(Route Groups)的用途
(auth)/login 和 (public)/about:圆括号内的名字不出现在 URL 中(/login 和 /about),但允许为不同组的路由使用不同的 +layout.svelte。这是实现"需要认证的页面共享一个带导航栏的布局,无需认证的页面使用干净布局"的标准做法。动态路由参数的类型安全
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
// $types 是 SvelteKit 自动生成的类型文件,保证 params.slug 的类型安全
export const load: PageServerLoad = async ({ params }) => {
// params.slug 的类型是 string(由路由名决定)
// 如果路由是 [id=integer](带参数验证器),类型是 string 但已通过验证
const post = await db.post.findUnique({
where: { slug: params.slug }
});
// 如果文章不存在,抛出 404 错误
if (!post) {
throw error(404, { message: '文章不存在' });
}
return { post }; // 返回的类型会传递给 +page.svelte 的 data prop
};
+server.ts(API 路由)vs +page.server.ts(页面数据)的选择
使用 +server.ts 的场景(REST API 端点):
✓ 需要被外部客户端(移动 App、第三方)调用
✓ 返回 JSON 以外的格式(二进制、流、CSV)
✓ 需要细粒度控制 HTTP 状态码和头部
✓ WebSocket 升级处理
使用 +page.server.ts 的场景(页面数据加载):
✓ 为当前页面提供数据(不需要被外部调用)
✓ 表单 Actions(POST 处理)
✓ 处理认证/授权(可以访问 cookies、session)
选择原则:
如果数据只为这个页面服务,用 +page.server.ts;
如果数据需要被多个地方复用或外部调用,用 +server.ts
常见错误:在 +page.ts 中访问数据库
+page.ts 在客户端导航时也会执行,而浏览器中没有数据库连接。如果你需要访问数据库,始终使用 +page.server.ts(后缀 .server.ts 表示只在服务端执行)。SvelteKit 会在构建时检测并报错,防止意外将数据库凭证暴露到客户端。
第7章小结
- 文件系统路由:
src/routes/目录结构直接对应 URL;[param]是动态参数,[...rest]是通配符,(group)是不影响 URL 的路由组 - 路由匹配优先级:静态路径 > 动态参数路径 > 通配符;不需要手动声明顺序
- +layout.svelte 嵌套:所有上级 layout 都会包裹子页面;父 layout 的 data 子页面可以访问
- .server.ts 只在服务端执行:可以安全访问数据库和 secrets;
+page.ts两端都执行,不能访问数据库 - 路由组 (group) 是实现"同 URL 结构,不同布局"的标准方案(如认证页面 vs 公开页面)
- +server.ts API 路由适合对外暴露的 REST 端点;内部页面数据用 +page.server.ts 更简洁