剩余参数
路由段数量未知时,可以使用剩余参数语法。例如,GitHub 风格的文件查看器可以这样实现:
/[org]/[repo]/tree/[branch]/[...file]
请求 /sveltejs/kit/tree/main/documentation/docs/04-advanced-routing.md 时,页面会收到以下参数:
{
org: 'sveltejs',
repo: 'kit',
branch: 'main',
file: 'documentation/docs/04-advanced-routing.md'
}
src/routes/a/[...rest]/z/+page.svelte 既匹配 /a/z,即完全没有剩余参数,也匹配 /a/b/z、/a/b/c/z 等。务必验证剩余参数值,例如通过匹配器。
404 页面
剩余参数也能用来渲染自定义 404。假设存在以下路由:
src/routes/
├ marx-brothers/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte
访问 /marx-brothers/karl 时,由于没有路由匹配,marx-brothers/+error.svelte 不会渲染。要使用这个嵌套错误页,需要创建能匹配全部 /marx-brothers/* 请求的路由,并在其中返回 404:
src/routes/
├ marx-brothers/
| ├ [...path]/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte
文件:src/routes/marx-brothers/[...path]/+page,JavaScript 版本:
import { error } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export function load(event) {
error(404, 'Not Found');
}
TypeScript 版本:
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = (event) => {
error(404, 'Not Found');
};
如果没有自行处理 404,它们会作为框架错误进入 handleError,其 kind 为 'framework';自行处理后,kind 为 'app'。
可选参数
[lang]/home 中的 lang 是必需参数。有时希望它可选,让 home 和 en/home 指向同一页面,只需再包一层方括号:[[lang]]/home。
可选参数不能跟在剩余参数后面,例如 [...rest]/[[optional]]。参数采用贪婪匹配,可选参数将永远没有机会被使用。
匹配
src/routes/fruits/[page] 既会匹配 /fruits/apple,也会匹配并不期望的 /fruits/rocketship。为了确保参数格式正确,可以在 src/params.js 或 src/params.ts 添加匹配器:
import { defineParams } from '@sveltejs/kit/params';
export const params = defineParams({
fruit: (param) => {
if (param !== 'apple' && param !== 'orange') return;
return param;
}
});
然后为路由指定匹配器:
src/routes/fruits/[page=fruit]
如果路径不匹配,SvelteKit 会按照下文排序规则尝试其他路由,最终找不到才返回 404。若匹配,匹配器的返回值会作为参数值传入。
也可以使用 Standard Schema,例如 Valibot:
import { defineParams } from '@sveltejs/kit/params';
import * as v from 'valibot';
export const params = defineParams({
number: v.pipe(v.string(), v.toNumber())
});
验证失败时,路由不匹配。验证成功时,参数类型使用 schema 的输出类型,必须是 string、boolean、number 或 bigint 的子类型。本例为 number。
文件:src/routes/items/[id=number]/+page,JavaScript 版本:
/** @type {import('./$types').PageLoad} */
export function load({ params }) {
console.log(typeof params.id); // 'number'
}
TypeScript 版本:
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => {
console.log(typeof params.id); // 'number'
};
转换应当是对称的。例如,转换成数字后,对数字调用 toString() 应得到原始字符串,这样 resolve 才能正确构造路径:
import { resolve } from '$app/paths';
resolve('/blog/[id=number]', { id: 1 });
匹配器同时在服务器和浏览器中运行。
在 SvelteKit 3 之前,每个参数匹配器必须分别写在 params 目录中的独立文件里,例如 src/params/foo.js 导出 export const match = (param) => param === 'foo';。是否匹配取决于返回值是否为真值,因此不会转换参数值。
排序
多个路由可能匹配同一路径。例如,下面的路由都能匹配 /foo-abc:
src/routes/[...catchall]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/foo-abc/+page.svelte
SvelteKit 需要确定实际应使用哪个路由,因此采用以下排序规则:
- 更具体的路由优先。例如,无参数路由比含一个动态参数的路由更具体。
- 带匹配器的参数
[name=type]优先于不带匹配器的[name]。 - 除非位于路由末尾,否则忽略
[[optional]]和[...rest];位于末尾时,它们的优先级最低。因此,排序时x/[[y]]/z与x/z等价。 - 仍然相同的,按字母顺序排序。
结果如下。/foo-abc 会使用 src/routes/foo-abc/+page.svelte,而 /foo-def 会使用 src/routes/foo-[c]/+page.svelte,不会进入更泛化的路由:
src/routes/foo-abc/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/[...catchall]/+page.svelte
编码
一些字符不能用于文件系统名称:Linux 和 Mac 上是 /,Windows 上是 \ / : * ? " < > |。# 和 % 在 URL 中有特殊意义,[ ] ( ) 在 SvelteKit 中有特殊意义,因此也不能直接作为路由名称的一部分。
可以用十六进制转义序列表示它们,格式为 [x+nn],其中 nn 是十六进制字符码:
| 字符 | 转义 |
|---|---|
| \ | [x+5c] |
| / | [x+2f] |
| : | [x+3a] |
| * | [x+2a] |
| ? | [x+3f] |
| “ | [x+22] |
| < | [x+3c] |
| > | [x+3e] |
| | | [x+7c] |
| # | [x+23] |
| % | [x+25] |
| [ | [x+5b] |
| ] | [x+5d] |
| ( | [x+28] |
| ) | [x+29] |
例如,要创建 /smileys/:-),文件路径应为 src/routes/smileys/[x+3a]-[x+29]/+page.svelte。
可以通过 JavaScript 获取字符的十六进制编码:
':'.charCodeAt(0).toString(16); // '3a', hence '[x+3a]'
也可以使用 Unicode 转义。通常直接使用未编码字符就可以,但如果因为某种原因,文件名不能包含表情符号,就可以转义。以下两种路径等价:
src/routes/[u+d83e][u+dd2a]/+page.svelte
src/routes/🤪/+page.svelte
Unicode 转义格式为 [u+nnnn],nnnn 是 0000 到 10ffff 之间的有效值。与 JavaScript 字符串转义不同,超过 ffff 的码点无需用代理对表示。进一步说明见 Programming with Unicode。
TypeScript 对以点号开头的目录支持存在问题,因此创建 .well-known 等路由时,可以编码该字符,例如 src/routes/[x+2e]well-known/...。
高级布局
默认情况下,布局层级与路由层级一致,但有时这不是你需要的结构。
路由组:(group)
比如,/dashboard 和 /item 属于应用页面,需要一种布局;/about 和 /testimonials 属于营销页面,需要另一种布局。可以使用名称包在圆括号内的目录分组。与普通目录不同,(app) 和 (marketing) 不影响内部路由的 URL 路径:
src/routes/
│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte
也可以直接在 (group) 中放置 +page,例如希望根路径 / 属于应用或营销页面时。
跳出布局
根布局适用于应用所有页面;省略时默认等同于 {@render children()}。如果某些页面需要不同的布局层级,可以把应用其他部分放在一个或多个分组中,让不应继承公共布局的路由留在分组外。
前面的例子中,/admin 不继承 (app) 或 (marketing) 布局。
+page@
每个页面都可以单独跳出当前布局层级。假设前例的 (app) 分组中存在 /item/[id]/embed:
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte
通常,它会依次继承根布局、(app) 布局、item 布局和 [id] 布局。文件名添加 @ 和目标路由段名称,可以重置到某一层;根布局使用空字符串。
+page@[id].svelte:继承src/routes/(app)/item/[id]/+layout.svelte。+page@item.svelte:继承src/routes/(app)/item/+layout.svelte。+page@(app).svelte:继承src/routes/(app)/+layout.svelte。+page@.svelte:继承src/routes/+layout.svelte。
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page@(app).svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte
+layout@
布局本身也可以用相同方式跳出父布局层级。例如,+layout@.svelte 会为全部子路由重置布局层级:
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte // uses (app)/item/[id]/+layout.svelte
│ │ │ ├ +layout.svelte // inherits from (app)/item/+layout@.svelte
│ │ │ └ +page.svelte // uses (app)/item/+layout@.svelte
│ │ └ +layout@.svelte // inherits from root layout, skipping (app)/+layout.svelte
│ └ +layout.svelte
└ +layout.svelte
何时使用布局分组
不是所有情况都适合布局分组,也无需勉强使用。你的场景可能导致复杂的分组嵌套,或者你不想为一个例外页面单独引入分组。通过组合可复用的 load 函数、Svelte 组件或 if 语句实现需求,完全没有问题。
下面的布局回到根布局,同时复用其他布局也能使用的组件与函数。
文件:src/routes/nested/route/+layout@,JavaScript 版本:
<script>
import ReusableLayout from '#lib/ReusableLayout.svelte';
let { data, children } = $props();
</script>
<ReusableLayout {data}>
{@render children()}
</ReusableLayout>
TypeScript 版本:
<script lang="ts">
import ReusableLayout from '#lib/ReusableLayout.svelte';
let { data, children } = $props();
</script>
<ReusableLayout {data}>
{@render children()}
</ReusableLayout>
文件:src/routes/nested/route/+layout,JavaScript 版本:
import { reusableLoad } from '#lib/reusable-load-function.js';
/** @type {import('./$types').PageLoad} */
export function load(event) {
// Add additional logic here, if needed
return reusableLoad(event);
}
TypeScript 版本:
import { reusableLoad } from '#lib/reusable-load-function.js';
import type { PageLoad } from './$types';
export const load: PageLoad = (event) => {
// Add additional logic here, if needed
return reusableLoad(event);
};
延伸阅读
原文:Advanced routing。作者/维护者:SvelteKit 文档贡献者。本文为原文的中文译文;代码保留原文内容。











暂无评论内容