SvelteKit 高级路由:匹配、编码与布局继承

剩余参数

路由段数量未知时,可以使用剩余参数语法。例如,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 文档贡献者。本文为原文的中文译文;代码保留原文内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容