我的学习笔记

土猛的员外

SvelteKit中文4-高级概念

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念

高级路由

Rest参数

如果路由段的数量未知,您可以使用rest语法——例如,您可以像这样实现GitHub的文件查看器…

1
/[org]/[repo]/tree/[branch]/[...file]

…在这种情况下,对/sveltejs/kit/tree/master/documentation/docs/04-advanced-routing.md的请求将导致以下参数可用于页面:

1
2
3
4
5
6
{
org: 'sveltejs',
repo: 'kit',
branch: 'master',
file: 'documentation/docs/04-advanced-routing.md'
}

src/routes/a/[...rest]/z/+page.svelte 将匹配/a/z(即根本没有参数)以及/a/b/z/a/b/c/z 等等。请确保检查 rest 参数的值是否有效,例如使用一个匹配器。

404 pages

剩余参数还允许您呈现自定义的404页面。鉴于这些路由…

1
2
3
4
5
6
7
src/routes/
├ marx-brothers/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte

…如果您访问/marx-brothers/karl,则不会呈现marx-brothers/+error.svelte文件,因为没有匹配的路由。 如果要呈现嵌套的错误页面,则应创建与任何/marx-brothers/*请求匹配的路由,并从中返回404:

1
2
3
4
5
6
7
8
src/routes/
├ marx-brothers/
| ├ [...path]/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte

src/routes/marx-brothers/[…path]/+page.js

1
2
3
4
5
6
7
import { error } from '@sveltejs/kit';


/** @type {import('./$types').PageLoad} */
export function load(event) {
throw error(404, 'Not Found');
}

If you don’t handle 404 cases, they will appear in handleError

可选参数

[lang]/home 这样的路由包含一个名为lang的参数,该参数是必需的。有时将这些参数设置为可选项会更加有益,因此在此示例中,home en/home 都指向同一页。您可以通过将参数放入另一对方括号中来实现:[[lang]]/home

请注意,可选路由参数不能跟在 rest 参数后面([...rest]/[[optional]]),因为参数匹配是“贪婪”的,并且可选参数始终不会被使用。

Matching

src/routes/archive/[page] 这样的路由会匹配 /archive/3,但它也会匹配 /archive/potato。我们不希望这种情况发生。您可以通过向 params 目录添加一个匹配器(该匹配器接受参数字符串("3" "potato")并在其有效时返回 true)来确保路由参数格式正确…

src/params/integer.js

1
2
3
4
/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param) {
return /^\d+$/.test(param);
}

…并扩充您的路线:

1
2
src/routes/archive/[page]
src/routes/archive/[page=integer]

如果路径名不匹配,SvelteKit 将尝试匹配其他路由(使用下面指定的排序顺序),最终返回 404。

params 目录中的每个模块都对应一个匹配器,除了 *.test.js*.spec.js 文件可以用于单元测试您的匹配器。

Matchers run both on the server and in the browser.

排序

一个路径可能匹配多个路由。例如,以下每个路由都将匹配 /foo-abc:

1
2
3
4
5
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,而不是更少具体的路由:

1
2
3
4
5
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 确定字符的十六进制代码:

1
':'.charCodeAt(0).toString(16); // '3a', hence '[x+3a]'

您也可以使用Unicode转义序列。通常情况下,您不需要这样做,因为您可以直接使用未编码的字符,但是如果由于某种原因无法在文件名中包含表情符号等,则可以使用转义字符。换句话说,以下两者是等效的:

1
2
src/routes/[u+d83e][u+dd2a]/+page.svelte
src/routes/../+page.svelte

Unicode转义序列的格式是[u+nnnn],其中nnnn是介于000010ffff之间的有效值。(与JavaScript字符串转义不同,不需要使用代理对来表示ffff以上的代码点。)要了解有关Unicode编码的更多信息,请参阅Programming with Unicode。

由于TypeScript无法处理以.字符开头的目录,因此在创建例如.well-known路由时编码这些字符可能会很有用:src/routes/[x+2e]well-known/...

高级布局

默认情况下,“布局层次结构”是“路由层次结构”的镜像。在某些情况下,这可能不是你想要的。

(group)

也许你有一些路由是“应用程序”路由,应该有一个布局(例如/dashboard/item),而其他的则是“营销”路由,应该有不同的布局(/blog/testimonials)。我们可以使用括号将这些路由分组到一个目录中 - 与普通目录不同,(app)(marketing)不会影响它们内部路径名的URL:

1
2
3
4
5
6
7
8
9
10
11
src/routes/
│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte

您也可以直接将 +page 放置在 (group)中,例如如果/应该是一个(app)(marketing) 页面。

突破布局

根布局适用于应用程序的每个页面 - 如果省略,则默认为<slot />。如果您希望某些页面具有与其余部分不同的布局层次结构,则可以将整个应用程序放在一个或多个组中,除了不应继承公共布局的路由之外。

在上面的示例中,/admin路由既不继承(app)也不继承(marketing)布局。

+page@

页面可以按路由基础打破当前的布局层次结构。假设我们在前面示例中的(app)组内有一个/item/[id]/embed路由:

1
2
3
4
5
6
7
8
9
10
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

通常情况下,这将继承根布局、(app)布局、layout布局和[id]布局。我们可以通过在段名称后面添加@来重置到其中一个布局,或者对于根布局,使用空字符串。在此示例中,我们可以从以下选项中选择:

  • +page@[id].svelte - inherits from src/routes/(app)/item/[id]/+layout.svelte
  • +page@item.svelte - inherits from src/routes/(app)/item/+layout.svelte
  • +page@(app).svelte - inherits from src/routes/(app)/+layout.svelte
  • +page@.svelte - inherits from src/routes/+layout.svelte
1
2
3
4
5
6
7
8
9
10
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page@(app).svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

+layout@

像页面一样,布局本身也可以使用相同的技术打破其父布局层次结构。例如,+layout@.svelte 组件将为所有子路由重置层次结构。

1
2
3
4
5
6
7
8
9
10
11
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

何时使用布局组

并非所有的用例都适合布局分组,也不应该感到必须使用它们。可能是因为您的用例会导致复杂的(group)嵌套,或者您不想为单个异常值引入(group)。完全可以使用其他手段,如组合(可重用load函数或Svelte组件)或if语句来实现所需功能。以下示例显示了一个布局,它将倒回到根布局并重用其他布局也可以使用的组件和函数:

src/routes/nested/route/+layout@.svelte

1
2
3
4
5
6
7
8
<script>
import ReusableLayout from '$lib/ReusableLayout.svelte';
export let data;
</script>

<ReusableLayout {data}>
<slot />
</ReusableLayout>

src/routes/nested/route/+layout.js

1
2
3
4
5
6
7
8
import { reusableLoad } from '$lib/reusable-load-function';


/** @type {import('./$types').PageLoad} */
export function load(event) {
// Add additional logic here, if needed
return reusableLoad(event);
}

Hooks

‘Hooks’ 是您声明的应用程序范围函数,SvelteKit 将在特定事件发生时调用它们,从而使您对框架的行为具有细粒度控制。

有两个 hooks 文件,都是可选的:

  • src/hooks.server.js - 应用程序服务器端钩子
  • src/hooks.client.js - 应用程序客户端钩子

这些模块中的代码将在应用程序启动时运行,因此它们非常适合初始化数据库客户端等操作。

您可以使用 config.kit.files.hooks 配置这些文件的位置。

Server hooks

以下钩子可以添加到 src/hooks.server.js 文件中:

handle

每当 SvelteKit 服务器接收到请求时,此函数都会运行 - 无论是在应用程序运行期间还是在预渲染期间 - 并确定响应。它接收一个表示请求的event对象和一个名为 resolve 的函数,该函数呈现路由并生成Response。这使您可以修改响应标头或正文,或完全绕过 SvelteKit(例如,用于编程实现路由)。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
if (event.url.pathname.startsWith('/custom')) {
return new Response('custom response');
}


const response = await resolve(event);
return response;
}

对于静态资源的请求,包括已经预渲染的页面,SvelteKit 不会进行处理。

如果未实现,则默认为 ({ event, resolve }) => resolve(event)。要向请求添加自定义数据,该数据将传递给 +server.js 和服务器加载函数中的处理程序,请填充以下所示的event.locals对象。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUserInformation(event.cookies.get('sessionid'));


const response = await resolve(event);
response.headers.set('x-custom-header', 'potato');


return response;
}

你可以定义多个处理函数,并使用序列助手函数执行它们。

resolve 还支持第二个可选参数,该参数可以更好地控制响应的呈现方式。该参数是一个对象,可以具有以下字段:

  • transformPageChunk(opts:{html:string,done:boolean}):MaybePromise <string | undefined> ——将自定义转换应用于HTML。如果done为true,则它是最终块。不能保证块是格式良好的HTML(例如,它们可能包括元素的开放标记但不包括其关闭标记),但它们总是在合理的边界处分割,例如%sveltekit.head%或layout/page组件。
  • filterSerializedResponseHeaders(name:string,value:string):boolean ——确定在使用fetch加载资源时哪些标题应包含在序列化响应中。默认情况下,不会包含任何内容。
  • preload(input: {type:'js'|'css'|'font'|'asset',path:string}): boolean - 确定要添加到<head>标签以预加载的文件。构建代码块时找到每个文件时都会调用该方法-因此如果您例如在+page.svelteimport “./styles.css”,则访问该页面时将使用解析后的路径调用preload 该CSS文件。请注意,在dev模式下不会调用预加载程序,因为它取决于构建时发生的分析。预加载可以通过更早地下载资产来提高性能,但如果下载过多而不必要,则也可能会损害性能。默认情况下,jscss文件将被预加载程序预加载所有当前未预先加载资产文件 ,但我们可能稍后根据反馈添加此功能。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('old', 'new'),
filterSerializedResponseHeaders: (name) => name.startsWith('x-'),
preload: ({ type, path }) => type === 'js' || path.includes('/important/')
});


return response;
}

请注意,resolve(...) 永远不会抛出错误,它将始终返回一个带有适当状态代码的 Promise<Response>。如果在 handle 过程中发生其他地方抛出错误,则被视为致命错误,并且 SvelteKit 将响应一个 JSON 表示形式的错误或回退错误页面(可以通过 src/error.html 自定义),具体取决于 Accept 标头。您可以在此处阅读更多关于错误处理的信息。

handleFetch

该功能允许您修改(或替换)在服务器上运行的loadaction函数内发生的fetch请求(或预渲染期间)。

例如,当用户执行客户端导航到相应页面时,您的load函数可能会向公共URLhttps://api.yourapp.com发出请求,但在SSR期间直接访问API可能更有意义(绕过任何代理和负载均衡器)。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ request, fetch }) {
if (request.url.startsWith('https://api.yourapp.com/')) {
// clone the original request, but change the URL
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}


return fetch(request);
}

资质证明

对于同源请求,SvelteKit的fetch实现将转发cookieauthorization 头,除非将credentials 选项设置为“omit”

对于跨域请求,如果请求URL属于应用程序的子域名,则会包括cookie。例如,如果您的应用程序位于my-domain.com上,并且您的API位于api.my-domain.com上,则cookie将包含在请求中。

如果您的应用程序和API位于兄弟子域名上 - 例如www.my-domain.comapi.my-domain.com - 那么属于共同父域(如my-domain.com)的cookie不会被包括在内,因为SvelteKit无法知道该cookie所属的域。在这些情况下,您需要使用handleFetch手动包含cookie:

src/hooks.server.js

1
2
3
4
5
6
7
8
9
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ event, request, fetch }) {
if (request.url.startsWith('https://api.my-domain.com/')) {
request.headers.set('cookie', event.request.headers.get('cookie'));
}


return fetch(request);
}

Shared hooks

以下内容可以添加到 src/hooks.server.jssrc/hooks.client.js

handleError

如果在加载或渲染过程中出现意外错误,将调用此函数并传递 errorevent。这样可以实现两个目的:

  • 你可以记录错误
  • 也可以生成一个定制的、安全的错误表示形式,以供用户查看,省略敏感信息如消息和堆栈跟踪。返回值成为 $page.error 的值。如果是 404 错误(你可以通过 event.route.id 为空来检测),则默认为 { message: 'Not Found' };对于其他所有情况,默认为 { message: 'Internal Error' }。要使其类型安全,请声明一个App.Error接口来自定义期望的形状(必须包括 message:string,以保证合理的回退行为)。

以下代码展示了将错误形状定义为 { message: string; errorId: string },并从 handleError 函数中相应地返回它的示例:

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
11
declare global {
namespace App {
interface Error {
message: string;
errorId: string;
}
}
}


export {};

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as Sentry from '@sentry/node';
import crypto from 'crypto';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, event }) {
const errorId = crypto.randomUUID();
// example integration with https://sentry.io/
Sentry.captureException(error, { event, errorId });


return {
message: 'Whoops!',
errorId
};
}

src/hooks.client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import * as Sentry from '@sentry/svelte';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleClientError} */
export async function handleError({ error, event }) {
const errorId = crypto.randomUUID();
// example integration with https://sentry.io/
Sentry.captureException(error, { event, errorId });


return {
message: 'Whoops!',
errorId
};
}

src/hooks.client.js 中,handleError 的类型是 HandleClientError 而不是 HandleServerError,并且eventNavigationEvent 而不是 RequestEvent

此函数不适用于预期错误(那些使用从 @sveltejs/kit 导入的 error 函数抛出的错误)。

在开发过程中,如果由于 Svelte 代码中的语法错误而导致错误,则传入的错误会附加一个frame属性,以突出显示错误位置。

确保 handleError 永远不会抛出错误。

Errors

错误是软件开发中不可避免的事实。SvelteKit 根据错误发生的位置、错误类型以及传入请求的性质,采用不同的处理方式。

Error objects

SvelteKit区分预期和非预期错误,两者默认都表示为简单的{ message: string }对象。

您可以添加其他属性,例如code或跟踪id,如下面的示例所示。(在使用TypeScript时,这需要您重新定义Error类型,如类型安全性中所述)。

Expected errors

预期错误是使用从 @sveltejs/kit 导入的错误助手创建的错误:

src/routes/blog/[slug]/+page.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { error } from '@sveltejs/kit';
import * as db from '$lib/server/database';


/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const post = await db.getPost(params.slug);


if (!post) {
throw error(404, {
message: 'Not found'
});
}


return { post };
}

这告诉 SvelteKit 将响应状态码设置为 404 并渲染一个 +error.svelte 组件,其中$page.error是作为 error(...) 的第二个参数提供的对象。

src/routes/+error.svelte

1
2
3
4
5
<script>
import { page } from '$app/stores';
</script>

<h1>{$page.error.message}</h1>

如果需要,您可以向错误对象添加额外的属性…

1
2
3
4
throw error(404, {
message: 'Not found',
code: 'NOT_FOUND'
});

否则,为了方便起见,您可以将字符串作为第二个参数传递:

1
2
throw error(404, { message: 'Not found' });
throw error(404, 'Not found');

Unexpected errors

意外错误”是指在处理请求时发生的任何其他异常。由于这些可能包含敏感信息,因此不会向用户公开意外错误消息和堆栈跟踪。

默认情况下,意外错误将被打印到控制台上(或者在生产环境中,记录到服务器日志中),而暴露给用户的错误具有通用形式:

1
{ "message": "Internal Error" }

意外错误将通过handleError钩子进行处理,您可以在其中添加自己的错误处理 - 例如,将错误发送到报告服务或返回自定义错误对象。

src/hooks.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import * as Sentry from '@sentry/node';


Sentry.init({/*...*/})


/** @type {import('@sveltejs/kit').HandleServerError} */
export function handleError({ error, event }) {
// example integration with https://sentry.io/
Sentry.captureException(error, { event });


return {
message: 'Whoops!',
code: error?.code ?? 'UNKNOWN'
};
}

Make sure that handleError never throws an error

Responses

如果在 handle 函数或 +server.js 请求处理程序内部发生错误,SvelteKit 将根据请求的 Accept 标头响应回退错误页面或错误对象的 JSON 表示。

您可以通过添加src/error.html文件来自定义回退错误页面:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>%sveltekit.error.message%</title>
</head>
<body>
<h1>My custom error page</h1>
<p>Status: %sveltekit.status%</p>
<p>Message: %sveltekit.error.message%</p>
</body>
</html>

SvelteKit 将使用它们对应的值替换 %sveltekit.status% %sveltekit.error.message%

如果错误发生在渲染页面时的load函数内,SvelteKit 将在最接近错误位置的+error.svelte组件中呈现。如果错误发生在 +layout(.server).js中的加载函数中,则树中最接近该布局上方(而不是旁边)的 +error.svelte 文件是最接近的错误边界。

例外情况是当错误发生在根 +layout.js 或者 +layout.server.js 内部时,因为根布局通常包含了+error.svelte组件。这种情况下,SvelteKit 使用备用错误页面。

Type safety

如果您正在使用TypeScript并且需要自定义错误的形状,可以通过在应用程序中声明一个App.Error接口来实现(按照惯例,在src/app.d.ts中,尽管它可以存在于TypeScript“看到”的任何位置):

src/app.d.ts

1
2
3
4
5
6
7
8
9
10
declare global {
namespace App {
interface Error {
code: string;
id: string;
}
}
}

export {};

这个接口总是包含一个message: string属性。

Link options

在 SvelteKit 中,使用<a>元素(而不是特定于框架的 <Link> 组件)来在应用程序的路由之间导航。如果用户单击链接,其 href 属性“属于”应用程序(而不是指向外部站点的链接),则 SvelteKit 将通过导入其代码并调用任何需要获取数据的load函数来导航到新页面。

您可以使用 data-sveltekit-* 属性自定义链接行为。这些属性可以应用于<a>本身或父元素。

这些选项也适用于method="GET" <form> 元素。

data-sveltekit-preload-data

在浏览器注册用户点击链接之前,我们可以检测到他们在桌面上悬停鼠标或触发了touchstartmousedown事件。在这两种情况下,我们可以猜测即将出现一个click事件。

SvelteKit 可以利用这些信息提前导入代码和获取页面数据,这可以为我们节省几百毫秒的时间——这是让用户界面感觉卡顿和流畅之间的差异。

我们可以使用 data-sveltekit-preload-data 属性来控制此行为,该属性有两个值:

  • "hover" 的意思是,如果鼠标停留在链接上,预加载将开始。在移动设备上,touchstart 事件触发时即开始预加载。
  • “tap”的意思是,只要注册了touchstartmousedown事件,就会立即开始预加载。

默认的项目模板在src/app.html文件中的 <body> 元素上应用了 data-sveltekit-preload-data="hover" 属性,这意味着默认情况下每个链接都会在鼠标悬停时预加载:

1
2
3
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>

有时,在用户悬停在链接上时调用load可能是不可取的,因为它很可能会导致误报(点击并不一定跟随悬停),或者因为数据正在快速更新,延迟可能意味着过期。

在这些情况下,您可以指定“tap”值,这将导致SvelteKit仅在用户轻触或单击链接时调用load

1
2
3
<a data-sveltekit-preload-data="tap" href="/stonks">
Get current stonk values
</a>

你也可以通过$app/navigation编程调用 preloadData 方法。

如果用户选择了减少数据使用,即navigator.connection.saveDatatrue,则数据将永远不会预加载。

data-sveltekit-preload-code

即使在您不想为链接预加载数据的情况下,预加载代码也可能是有益的。 data-sveltekit-preload-code 属性与 data-sveltekit-preload-data 类似,但它可以取四个值之一,按“渴望程度”递减:

  • "eager" 表示链接将立即预加载
  • “viewport” 表示链接将在进入视口时预加载
  • “hover” - 与上述相同,只是仅预加载代码
  • “tap” - 与上述相同,只是仅预加载代码

请注意,viewporteager 只适用于紧随导航后立即出现在 DOM 中的链接 - 如果稍后添加链接(例如在{ #if } 块中),则只有在hovertap时才会预加载。这是为了避免由于过度观察 DOM 的更改而导致的性能问题。

由于预加载代码是预加载数据的先决条件,因此只有在它指定比任何已存在的 data-sveltekit-preload-data 属性更急切的值时,该属性才会生效。

data-sveltekit-preload-data 一样,如果用户选择了减少数据使用量,则会忽略此属性。

data-sveltekit-reload

有时候,我们需要告诉 SvelteKit 不要处理一个链接,而是让浏览器来处理它。在链接中添加 data-sveltekit-reload 属性即可

1
<a data-sveltekit-reload href="/path">Path</a>

当链接被点击时,会导致整个页面的跳转。

带有 rel="external" 属性的链接将接受相同的处理。此外,在预渲染期间它们将被忽略。

data-sveltekit-replacestate

有时候,您不希望导航在浏览器的会话历史中创建一个新条目。将 data-sveltekit-replacestate 属性添加到链接中…

1
<a data-sveltekit-replacestate href="/path">Path</a>

当链接被点击时,将替换当前的history记录条目而不是使用pushState创建一个新的条目。

data-sveltekit-keepfocus

有时候你不希望在导航后重置焦点。例如,可能你有一个搜索表单,在用户输入时提交,而你想保持文本输入框的焦点。可以给它添加一个data-sveltekit-keepfocus属性

1
2
3
<form data-sveltekit-keepfocus>
<input type="text" name="query">
</form>

将导致当前焦点元素在导航后保持焦点。一般来说,避免在链接上使用此属性,因为聚焦的元素将是<a> 标签(而不是先前聚焦的元素),屏幕阅读器和其他辅助技术用户通常期望在导航后移动焦点。您还应仅在导航后仍存在的元素上使用此属性。如果该元素不存在,则用户的重点将丢失,这会给辅助技术用户带来困惑。

data-sveltekit-noscroll

当导航到内部链接时,SvelteKit会镜像浏览器的默认导航行为:它将更改滚动位置为0,0,以便用户位于页面的左上角(除非链接包括#hash,在这种情况下它将滚动到具有匹配ID的元素)。

在某些情况下,您可能希望禁用此行为。向链接添加data-sveltekit-noscroll属性…

1
<a href="path" data-sveltekit-noscroll>Path</a>

…点击链接后将阻止滚动。

禁用选项

要禁用已启用的元素中的任何选项,请使用“off”值:

1
2
3
4
5
6
7
8
9
10
11
<div data-sveltekit-preload-data>
<!-- these links will be preloaded --> <a href="/a">a</a>
<a href="/b">b</a>
<a href="/c">c</a>

<div data-sveltekit-preload-data="off">
<!-- these links will NOT be preloaded --> <a href="/d">d</a>
<a href="/e">e</a>
<a href="/f">f</a>
</div>
</div>

要有条件地将属性应用于元素,请执行以下操作:

1
<div data-sveltekit-reload={shouldReload ? '' : 'off'}>

这是有效的,因为在HTML中,<element attribute>等同于<element attribute = "">

Service workers

服务工作者充当代理服务器,处理应用程序内的网络请求。这使得您的应用程序可以离线工作,但即使您不需要离线支持(或者由于正在构建的应用程序类型而无法实现),使用服务工作者通常也值得加快导航速度,通过预缓存已构建的 JS 和 CSS。

在 SvelteKit 中,如果您有一个src/service-worker.js文件(或 src/service-worker.jssrc/service-worker/index.js 等),它将被捆绑并自动注册。如果需要,可以更改服务工作者的位置。

如果需要使用自己的逻辑注册服务工作者或使用其他解决方案,则可以禁用自动注册。默认注册类似于以下内容:

1
2
3
4
5
if ('serviceWorker' in navigator) {
addEventListener('load', function () {
navigator.serviceWorker.register('./path/to/service-worker.js');
});
}

深入service worker

在 service worker 中,您可以访问 $service-worker 模块,该模块为您提供所有静态资源、构建文件和预渲染页面的路径。还提供了一个应用程序版本字符串,您可以使用它来创建唯一的缓存名称以及部署的base路径。如果您的 Vite 配置指定了 define(用于全局变量替换),则这也将应用于服务工作者以及服务器/客户端构建。

以下示例会急切地缓存已构建的应用程序和static文件,并在发生所有其他请求时缓存它们。这将使每个页面在访问后都能离线工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/// <reference types="@sveltejs/kit" />
import { build, files, version } from '$service-worker';


// Create a unique cache name for this deployment
const CACHE = `cache-${version}`;


const ASSETS = [
...build, // the app itself
...files // everything in `static`
];


self.addEventListener('install', (event) => {
// Create a new cache and add all files to it
async function addFilesToCache() {
const cache = await caches.open(CACHE);
await cache.addAll(ASSETS);
}


event.waitUntil(addFilesToCache());
});


self.addEventListener('activate', (event) => {
// Remove previous cached data from disk
async function deleteOldCaches() {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key);
}
}


event.waitUntil(deleteOldCaches());
});


self.addEventListener('fetch', (event) => {
// ignore POST requests etc
if (event.request.method !== 'GET') return;


async function respond() {
const url = new URL(event.request.url);
const cache = await caches.open(CACHE);


// `build`/`files` can always be served from the cache
if (ASSETS.includes(url.pathname)) {
return cache.match(url.pathname);
}


// for everything else, try the network first, but
// fall back to the cache if we're offline
try {
const response = await fetch(event.request);


if (response.status === 200) {
cache.put(event.request, response.clone());
}


return response;
} catch {
return cache.match(event.request);
}
}


event.respondWith(respond());
});

缓存时要小心!在某些情况下,过期的数据可能比离线不可用的数据更糟糕。由于浏览器会在缓存太满时清空缓存,因此您还应该小心缓存大型资产(如视频文件)。

发布期间

服务工作者在生产环境中进行捆绑,但在开发过程中不会进行捆绑。因此,只有支持服务工作者模块的浏览器才能在开发期间使用它们。如果您手动注册服务工作者,则需要在开发过程中传递 { type: 'module' } 选项:

1
2
3
4
5
6
import { dev } from '$app/environment';


navigator.serviceWorker.register('/service-worker.js', {
type: dev ? 'module' : 'classic'
});

在开发过程中,buildprerendered是空数组。

类型安全

为服务工作者设置适当的类型需要一些手动设置。在您的 service-worker.js 文件中,在文件顶部添加以下内容:

1
2
3
4
5
6
7
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />


const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self));

这将禁用像HTMLElement这样的DOM类型,因为它们在服务工作者中不可用,并实例化正确的全局变量。将self重新分配给sw允许您在过程中进行类型转换(有几种方法可以做到这一点,但最简单的方法需要没有额外文件)。在文件的其余部分使用sw而不是self。对SvelteKit类型的引用确保$service-worker导入具有适当的类型定义。

其他解决方案

SvelteKit的服务工作者实现是故意低级的。如果您需要更全面但也更有见解的解决方案,我们建议查看像Vite PWA插件这样使用Workbox的解决方案。对于关于服务工作者的更一般信息,我们建议参考MDN Web文档。

服务端优先modules

像一个好朋友一样,SvelteKit 会保守你的秘密。当在同一个代码库中编写后端和前端时,很容易意外地将敏感数据导入到前端代码中(例如包含 API 密钥的环境变量)。SvelteKit 提供了一种完全防止这种情况发生的方法:仅限服务器模块。

私有化环境变量

$env/static/private$env/dynamic/private 模块只能被导入到仅在服务器上运行的模块中,例如hooks.server.js+page.server.js。这些模块在模块部分有介绍。

属于你自己的modules

你可以通过两种方式将自己的模块设置为仅服务器可用:

  • 将文件名添加.server,例如secrets.server.js
  • 将它们放置在$lib/server中,例如$lib/server/secrets.js

如何工作

无论是直接还是间接地导入仅限于服务器的代码,只要您有面向公众的代码…

$lib/server/secrets.js

1
export const atlantisCoordinates = [/* redacted */];

src/routes/utils.js

1
2
3
4
export { atlantisCoordinates } from '$lib/server/secrets.js';


export const add = (a, b) => a + b;

src/routes/+page.svelte

1
2
3
<script>
import { add } from './utils.js';
</script>

…SvelteKit will error:

1
2
3
4
Cannot import $lib/server/secrets.js into public-facing code:
- src/routes/+page.svelte
- src/routes/utils.js
- $lib/server/secrets.js

尽管公共代码 - src/routes/+page.svelte - 仅使用 add 导出而不是 secret atlantisCoordinates 导出,但秘密代码可能最终会出现在浏览器下载的 JavaScript 中,因此导入链被认为是不安全的。

该功能还适用于动态导入,甚至包括像 await import(./${foo}.js) 这样的插值导入,只有一个小问题:在开发过程中,如果公共代码和仅服务器模块之间存在两个或多个动态导入,则非法导入将无法在第一次加载代码时检测到。

像Vitest这样的单元测试框架不区分仅服务器和公共界面代码。因此,在运行测试时,当process.env.TEST === 'true'时,非法导入检测被禁用。

Asset handling(资产处理)

缓存和内联

Vite将自动处理导入的资源以提高性能。哈希值将添加到文件名中,以便可以缓存,并且小于assetsInlineLimit的资源将被内联。

1
2
3
4
5
<script>
import logo from '$lib/assets/logo.png';
</script>

<img alt="The project logo" src={logo} />

如果您更喜欢在标记中直接引用资产,则可以使用预处理器,例如svelte-preprocess-import-assets。

对于通过CSS url()函数包含的资产,您可能会发现vitePreprocess很有用。

转换

您可能希望将图像转换为输出压缩图像格式,例如.webp.avif,具有不同设备大小的响应式图像,或者去除EXIF数据以保护隐私。对于静态包含的图像,您可以使用Vite插件(如vite-imagetools)。您还可以考虑使用CDN,在基于Accept HTTP标头和查询字符串参数提供适当转换后的图像。

快照

短暂的DOM状态,例如侧边栏的滚动位置、<input>元素的内容等,在从一个页面导航到另一个页面时会被丢弃。

例如,如果用户填写了表单但在提交之前点击了链接,然后点击浏览器的返回按钮,则他们填写的值将丢失。在需要保留该输入的情况下,可以对DOM状态进行快照,并在用户导航回来时恢复它。

要做到这一点,请从+page.svelte+layout.svelte中导出具有capturerestore 方法的快照对象:

+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
let comment = '';
/** @type {import('./$types').Snapshot<string>} */ export const snapshot = {
capture: () => comment,
restore: (value) => comment = value
};
</script>

<form method="POST">
<label for="comment">Comment</label>
<textarea id="comment" bind:value={comment} />
<button>Post comment</button>
</form>

当您离开此页面时, capture 功能会在页面更新之前立即调用,并将返回值与浏览器历史堆栈中的当前条目关联。如果您导航回来,则 restore 函数会在页面更新后立即使用存储的值进行调用。

数据必须可序列化为JSON,以便可以将其持久化到sessionStorage中。这允许在重新加载页面或用户从不同站点导航回来时恢复状态。

避免从capture返回非常大的对象 - 一旦被捕获,对象将在会话期间保留在内存中,在极端情况下可能太大而无法持久化到sessionStorage

打包

你可以使用SvelteKit构建应用程序和组件库,使用@sveltejs/package包(npm create svelte有一个选项可为您设置此内容)。

当您创建应用程序时,src/routes的内容是面向公众的东西;src/lib包含您的应用程序内部库。

组件库具有与SvelteKit应用程序完全相同的结构,只是src/lib是面向公众的部分,并且您的根package.json用于发布该软件包。 src/routes可能是文档或演示站点,以配合该库使用,也可能仅是在开发过程中使用的沙箱。

@sveltejs/package 运行 svelte-package 命令将获取 src/lib 目录的内容并生成一个 dist 目录(可以进行配置),其中包含以下内容:

  • src/lib目录下的所有文件。Svelte组件将被预处理,TypeScript文件将被转译为JavaScript。
  • 生成用于Svelte、JavaScript和TypeScript文件的类型定义(d.js文件)。您需要安装typescript >= 4.0.0才能使用此功能。类型定义放置在其实现旁边,手写的d.js文件会原样复制过来。您可以禁用生成,但我们强烈建议不要这样做——使用您库的人可能会使用TypeScript,并且他们需要这些类型定义文件。

@sveltejs/package 版本1 生成了一个 package.json 文件。现在不再这样做,它将使用您项目中的 package.json,并验证其是否正确。如果您仍在使用版本1,请参阅此 PR 获取迁移说明。

package.json的解剖结构

由于您现在正在为公共使用构建库,因此您的package.json文件的内容将变得更加重要。通过它,您可以配置包的入口点、发布到npm上的文件以及库所依赖的依赖项。让我们逐个浏览最重要的字段。

name

这是您的软件包名称。其他人可以使用该名称安装它,并在 https://npmjs.com/package/<name> 上可见。

1
2
3
{
"name": "your-library"
}

license

每个软件包都应该有一个许可证字段,以便人们知道如何使用它。一种非常流行的、在分发和重用方面非常自由且没有保修的许可证是MIT。

1
2
3
{
"license": "MIT"
}

在这里阅读更多相关信息。请注意,您的软件包中还应该包含一个许可证文件。

files

这会告诉npm要打包和上传到npm的哪些文件。它应该包含你的输出文件夹(默认为dist)。你的package.jsonREADMELICENSE将始终被包括在内,因此您不需要指定它们。

1
2
3
{
"files": ["dist"]
}

为了排除不必要的文件(例如单元测试或仅从src/routes等导入的模块),您可以将它们添加到.npmignore文件中。这将导致更小、安装更快的软件包。

exports

exports字段包含了该软件包的入口点。如果您通过npm create svelte@latest设置一个新的库项目,它将被设置为单个导出项,即软件包根目录:

1
2
3
4
5
6
7
8
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
}
}

这告诉打包工具和工具链,你的软件包只有一个入口点,即根目录,并且所有内容都应该通过它导入,就像这样:

1
import { Something } from 'your-library';

typesvelte键是导出条件。它们告诉工具在查找your-library导入时要导入哪个文件:

  • TypeScript看到types条件并查找类型定义文件。如果您没有发布类型定义,则省略此条件。
  • Svelte感知工具看到svelte条件,并知道这是一个Svelte组件库。如果您发布的库不导出任何Svelte组件,也可以在非Svelte项目中使用(例如,一个Svelte存储库),则可以将此条件替换为default

以前的 @sveltejs/package 版本还添加了 package.json 导出。这不再是模板的一部分,因为所有工具现在都可以处理没有显式导出 package.json 的情况。

您可以根据自己的喜好调整导出并提供更多入口。例如,如果您想直接公开 src/lib/Foo.svelte 组件而不是重新导出组件的 src/lib/index.js 文件,则可以创建以下导出映射…

1
2
3
4
5
6
7
8
{
"exports": {
"./Foo.svelte": {
"types": "./dist/Foo.svelte.d.ts",
"svelte": "./dist/Foo.svelte"
}
}
}

..你的库的使用者可以像这样导入组件:

1
import Foo from 'your-library/Foo.svelte';

请注意,如果您提供类型定义,则执行此操作需要额外的注意。在此处阅读有关警告的更多信息。

通常情况下,导出映射的每个键都是用户从您的包中导入某些内容所必须使用的路径,而值则是将被导入的文件路径或包含这些文件路径的导出条件映射。

svelte

这是一个遗留字段,使工具能够识别Svelte组件库。在使用svelte导出条件时不再需要它,但为了向后兼容旧的工具而保留它是很好的,这些旧工具还不知道导出条件。它应该指向你的根入口点。

1
2
3
{
"svelte": "./dist/index.js"
}

TypeScript

即使您不使用TypeScript,也应该为库提供类型定义,以便那些使用TypeScript的人在使用您的库时获得正确的智能提示。@sveltejs/package可以帮助您自动生成大部分类型定义。默认情况下,在打包库时,JavaScript、TypeScript和Svelte文件都会自动生成类型定义。您需要确保导出映射中的types条件指向正确的文件即可。通过npm create svelte@latest初始化一个库项目时,默认已经为根导出设置好了这一点。

但是如果你有其他东西而不是根导出——例如提供your-library/foo导入——你需要额外注意提供类型定义。不幸的是,默认情况下,TypeScript将无法解析像{ "./foo": { "types": "./dist/foo.d.ts", ... }}这样的导出类型条件。相反,它将在库根目录(即your-library/foo.d.ts而不是your-library/dist/foo.d.ts)中搜索foo.d.ts文件。要解决此问题,您有两个选择:

第一种选择是要求使用您的库的人将tsconfig.json(或jsconfig.json)中moduleResolution选项设置为bundler(从TypeScript 5开始可用,在未来最佳且推荐选项),node16nodenext 。这会让TypeScript实际查看exports map并正确解析类型。

第二种选择是利用TypeScript中名为typesVersions特性来连接各种类型定义。这是package.json内部字段之一,用于检查不同TypeScript版本的不同类型定义,并且还包含路径映射功能。我们利用该路径映射功能来实现我们想要的效果。对于上述foo导出,相应的typesVersions如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"exports": {
"./foo": {
"types": "./dist/foo.d.ts",
"svelte": "./dist/foo.js"
}
},
"typesVersions": {
">4.0": {
"foo": ["./dist/foo.d.ts"]
}
}
}

>4.0 告诉 TypeScript 如果使用的 TypeScript 版本大于 4(在实践中应该始终为真),则检查内部映射。内部映射告诉 TypeScript,your-library/foo 的类型定义可以在./dist/foo.d.ts中找到,这基本上复制了导出条件。你还可以使用 * 通配符来一次性地使许多类型定义可用而不必重复自己。请注意,如果选择 typesVersions,则必须通过它声明所有类型导入,包括根导入(其定义为 "index":[..])。

最佳实践

除非您打算仅供其他 SvelteKit 项目使用,否则应避免在包中使用类似 $app 的 SvelteKit 特定模块。例如,您可以使用 import { BROWSER } from 'esm-env'(请参阅 esm-env 文档),而不是使用 import { browser } from '$app/environment'。您还可以通过传递诸如当前 URL 或导航操作之类的 prop 来代替直接依赖于 $app/stores$app/navigation 等等。以这种更通用的方式编写应用程序也将使设置测试工具、UI 演示等变得更加容易。

确保通过 svelte.config.js(而不是vite.config.js tsconfig.json)添加别名,以便它们由 svelte-package 处理。

您应该仔细考虑对包所做的更改是否为错误修复、新功能或破坏性更改,并相应地更新软件包版本。请注意,如果从现有库中删除 exports 中的任何路径或其中任何export条件,则应将其视为破坏性更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"exports": {
".": {
"types": "./dist/index.d.ts",
// changing `svelte` to `default` is a breaking change:
"svelte": "./dist/index.js"
"default": "./dist/index.js"
},
// removing this is a breaking change:
"./foo": {
"types": "./dist/foo.d.ts",
"svelte": "./dist/foo.js",
"default": "./dist/foo.js"
},
// adding this is ok:
"./bar": {
"types": "./dist/bar.d.ts",
"svelte": "./dist/bar.js",
"default": "./dist/bar.js"
}
}
}

选项

svelte-package接受以下选项:

  • -w/--watch — 监听src/lib中的文件变化并重新构建包
  • -i/--input — 输入目录,其中包含所有包文件。默认为src/lib
  • -o/--o — 输出目录,处理后的文件将写入其中。您的package.json导出应指向该目录内的文件,并且files数组应包括该文件夹。默认为dist
  • -t/--types — 是否创建类型定义(d.js 文件)。我们强烈建议这样做,因为它有助于生态系统库质量提升。默认值为true

发布

发布生成的软件包:

1
npm publish

注意事项

所有相关文件导入都需要完全指定,遵循Node的ESM算法。这意味着对于像src/lib/something/index.js这样的文件,您必须包括带有扩展名的文件名:

1
2
import { something } from './something';
import { something } from './something/index.js';

如果您正在使用TypeScript,则需要以相同的方式导入.ts文件,但是要使用.js文件结尾而不是.ts文件结尾。(这是TypeScript设计决策之外我们无法控制的。)在tsconfig.jsonjsconfig.json中设置“moduleResolution”:“NodeNext”将有助于解决此问题。

除了Svelte文件(预处理)和TypeScript文件(转换为JavaScript),所有其他文件都会按原样复制。

该系列文章一共四篇,以下是系列文章链接:

SvelteKit中文1-开始
SvelteKit中文2-核心概念
SvelteKit中文3-编译发布
SvelteKit中文4-高级概念



关注我的微信公众号,可收到实时更新通知

公众号:土猛的员外


我们的创业项目已经上线!!!

TorchV AI,帮助企业快速进入AI时代!

具体详情,请点击官网咨询


最新内容,关注“土猛的员外”公众号