SvelteKit 最新中文文档教程(13)—— Hooks
<h2 id="前言">前言</h2><p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="hooks">Hooks</h2>
<p>“Hooks” 是您声明的应用程序范围的函数,SvelteKit 会在响应特定事件时调用它们,让您能够对框架的行为进行更为精细的控制。</p>
<p>有三个 hook 文件,都是可选的:</p>
<ul>
<li><code>src/hooks.server.js</code> — 您的应用程序的服务端 hook</li>
<li><code>src/hooks.client.js</code> — 您的应用程序的客户端 hook</li>
<li><code>src/hooks.js</code> — 您的应用程序的在客户端和服务端都运行的 hook</li>
</ul>
<p>这些模块中的代码会在应用程序启动时运行,这使得它们对初始化数据库客户端等操作很有用。</p>
<blockquote>
<p>[!NOTE] 您可以通过 <code>config.kit.files.hooks</code> 配置这些文件的位置。</p>
</blockquote>
<h2 id="服务端-hook">服务端 hook</h2>
<p>以下 hook 可以添加到 <code>src/hooks.server.js</code> 中:</p>
<h3 id="handle">handle</h3>
<p>这个函数在 SvelteKit 服务端每次接收到 request 时运行 — 无论是在应用程序运行时,还是在预渲染过程中 — 并决定response。</p>
<p>它接收一个表示请求的 <code>event</code> 对象和一个名为 <code>resolve</code> 的函数,该函数渲染路由并生成一个 <code>Response</code>。这允许您修改响应头或响应体,或完全绕过 SvelteKit(例如,用于以编程方式实现路由)。</p>
<pre><code class="language-js">/// file: src/hooks.server.js
/** @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;
}
</code></pre>
<blockquote>
<p>[!NOTE] 对静态资源的请求 — 包括已经预渲染的页面 — 不会由 SvelteKit 处理。</p>
</blockquote>
<p>如果未实现,默认为 <code>({ event, resolve }) => resolve(event)</code>。</p>
<h3 id="locals">locals</h3>
<p>要向请求中添加自定义数据(这些数据会传递给 <code>+server.js</code> 中的处理程序和服务端的 <code>load</code> 函数),可以填充 <code>event.locals</code> 对象,如下所示。</p>
<pre><code class="language-js">/// file: src/hooks.server.js
// @filename: ambient.d.ts
type User = {
name: string;
}
declare namespace App {
interface Locals {
user: User;
}
}
const getUserInformation: (cookie: string | void) => Promise<User>;
// @filename: index.js
// ---cut---
/** @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;
}
</code></pre>
<p>您可以定义多个 <code>handle</code> 函数,并使用<code>sequence</code> 辅助函数执行它们。</p>
<p><code>resolve</code> 还支持第二个可选参数,让您能够更好地控制响应的渲染方式。该参数是一个对象,可以包含以下字段:</p>
<ul>
<li><code>transformPageChunk(opts: { html: string, done: boolean }): MaybePromise<string | undefined></code> — 对 HTML 应用自定义转换。如果 <code>done</code> 为 true,则是最后一个块。块不保证是格式良好的 HTML(例如,它们可能包含一个元素的开始标签但没有结束标签),但它们总是会在合理的边界处分割,比如 <code>%sveltekit.head%</code> 或布局/页面组件。</li>
<li><code>filterSerializedResponseHeaders(name: string, value: string): boolean</code> — 确定当 <code>load</code> 函数使用 <code>fetch</code> 加载资源时,哪些头部应该包含在序列化的响应中。默认情况下,不会包含任何头部。</li>
<li><code>preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean</code> — 确定应该在 <code><head></code> 标签中添加哪些文件以预加载。该方法在构建代码块时被调用,每个找到的文件都会被调用 — 例如,如果您在 <code>+page.svelte</code> 中有 <code>import './styles.css</code>,在访问该页面时,<code>preload</code> 将传入该 CSS 文件的解析路径进行调用。注意,在开发模式下不会调用 <code>preload</code>,因为它依赖于构建时的分析。预加载可以通过更早下载资源来提高性能,但如果不必要地下载太多内容也会适得其反。默认情况下,会预加载 <code>js</code> 和 <code>css</code> 文件。目前不会预加载 <code>asset</code> 文件,但我们可能会在评估反馈后添加此功能。</li>
</ul>
<pre><code class="language-js">/// file: src/hooks.server.js
/** @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;
}
</code></pre>
<p>注意,<code>resolve(...)</code> 永远不会抛出错误,它总是会返回一个带有适当状态码的 <code>Promise<Response></code>。如果在 <code>handle</code> 期间其他地方抛出错误,这将被视为致命错误,SvelteKit 将根据 <code>Accept</code> 头部返回错误的 JSON 表示或回退错误页面 — 后者可以通过 <code>src/error.html</code> 自定义。您可以在这里阅读更多关于错误处理的信息。</p>
<h3 id="handlefetch">handleFetch</h3>
<p>这个函数允许您修改(或替换)在服务端上运行的 <code>load</code> 或 <code>action</code> 函数中发生的 <code>fetch</code> 请求(或在预渲染期间)。</p>
<p>例如,当用户执行客户端导航到相应页面时,您的 <code>load</code> 函数可能会向公共 URL(如 <code>https://api.yourapp.com</code>)发出请求,但在 SSR 期间,直接访问 API 可能更有意义(绕过位于它和公共互联网之间的代理和负载均衡器)。</p>
<pre><code class="language-js">/// file: src/hooks.server.js
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ request, fetch }) {
if (request.url.startsWith('https://api.yourapp.com/')) {
// 克隆原始请求,但改变 URL
request = new Request(
request.url.replace('https://api.yourapp.com/', 'http://localhost:9999/'),
request
);
}
return fetch(request);
}
</code></pre>
<p><strong>认证凭据</strong></p>
<p>对于同源请求,除非 <code>credentials</code> 选项设置为 <code>"omit"</code>,否则 SvelteKit 的 <code>fetch</code> 实现会转发 <code>cookie</code> 和 <code>authorization</code> 头部。</p>
<p>对于跨源请求,如果请求 URL 属于应用程序的子域,则会包含 <code>cookie</code> — 例如,如果您的应用程序在 <code>my-domain.com</code> 上,而您的 API 在 <code>api.my-domain.com</code> 上,cookie 将包含在请求中。</p>
<p>如果您的应用程序和 API 在兄弟子域上 — 例如 <code>www.my-domain.com</code> 和 <code>api.my-domain.com</code> — 那么属于共同父域(如 <code>my-domain.com</code>)的 cookie 将不会被包含,因为 SvelteKit 无法知道 cookie 属于哪个域。在这些情况下,您需要使用 <code>handleFetch</code> 手动包含 cookie:</p>
<pre><code class="language-js">/// file: src/hooks.server.js
// @errors: 2345
/** @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);
}
</code></pre>
<h2 id="共享-hook">共享 hook</h2>
<p>以下 hook 可以同时添加到 <code>src/hooks.server.js</code> 和 <code>src/hooks.client.js</code> 中:</p>
<h3 id="handleerror">handleError</h3>
<p>如果在加载或渲染期间抛出意外错误,此函数将被调用,并传入 <code>error</code>、<code>event</code>、<code>status</code> 代码和 <code>message</code>。这允许两件事:</p>
<ul>
<li>您可以记录错误</li>
<li>您可以生成一个安全的、显示给用户的自定义错误表示,省略敏感的详细信息,如消息和堆栈跟踪。返回的值(默认为 <code>{ message }</code>)会成为 <code>$page.error</code> 的值。</li>
</ul>
<p>对于从您的代码(或您的代码调用的库代码)抛出的错误,状态将为 500,消息将为 "Internal Error"。虽然 <code>error.message</code> 可能包含不应暴露给用户的敏感信息,但 <code>message</code> 是安全的(尽管对普通用户来说没有意义)。</p>
<p>要以类型安全的方式向 <code>$page.error</code> 对象添加更多信息,您可以通过声明 <code>App.Error</code> 接口(必须包含 <code>message: string</code>,以保证合理的回退行为)来自定义预期的形状。这允许您 — 例如 — 附加一个跟踪 ID,供用户在与技术支持人员通信时引用:</p>
<pre><code class="language-ts">/// file: src/app.d.ts
declare global {
namespace App {
interface Error {
message: string;
errorId: string;
}
}
}
export {};
</code></pre>
<pre><code class="language-js">/// file: src/hooks.server.js
// @errors: 2322 2353
// @filename: ambient.d.ts
declare module '@sentry/sveltekit' {
export const init: (opts: any) => void;
export const captureException: (error: any, opts: any) => void;
}
// @filename: index.js
// ---cut---
import * as Sentry from '@sentry/sveltekit';
Sentry.init({/*...*/})
/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, event, status, message }) {
const errorId = crypto.randomUUID();
// 与 https://sentry.io/ 集成的示例
Sentry.captureException(error, {
extra: { event, errorId, status }
});
return {
message: '哎呀!',
errorId
};
}
</code></pre>
<pre><code class="language-js">/// file: src/hooks.client.js
// @errors: 2322 2353
// @filename: ambient.d.ts
declare module '@sentry/sveltekit' {
export const init: (opts: any) => void;
export const captureException: (error: any, opts: any) => void;
}
// @filename: index.js
// ---cut---
import * as Sentry from '@sentry/sveltekit';
Sentry.init({/*...*/})
/** @type {import('@sveltejs/kit').HandleClientError} */
export async function handleError({ error, event, status, message }) {
const errorId = crypto.randomUUID();
// 与 https://sentry.io/ 集成的示例
Sentry.captureException(error, {
extra: { event, errorId, status }
});
return {
message: '哎呀!',
errorId
};
}
</code></pre>
<blockquote>
<p>[!NOTE] 在 <code>src/hooks.client.js</code> 中,<code>handleError</code> 的类型是 <code>HandleClientError</code> 而不是 <code>HandleServerError</code>,并且 <code>event</code> 是一个 <code>NavigationEvent</code> 而不是 <code>RequestEvent</code>。</p>
</blockquote>
<p>此函数不会因为预期的错误(那些使用从 <code>@sveltejs/kit</code> 导入的 <code>error</code> 函数抛出的错误)而被调用。</p>
<p>在开发过程中,如果由于 Svelte 代码中的语法错误而发生错误,传入的错误会附加一个 <code>frame</code> 属性,突出显示错误的位置。</p>
<blockquote>
<p>[!NOTE] 确保 <code>handleError</code> 永远不会抛出错误</p>
</blockquote>
<h3 id="init">init</h3>
<p>这个函数在服务端创建或应用程序在浏览器中启动时运行一次,是执行异步工作(如初始化数据库连接)的有用位置。</p>
<blockquote>
<p>[!NOTE] 如果您的环境支持顶级 await,<code>init</code> 函数实际上与在模块顶层编写初始化逻辑没有什么不同,但一些环境 — 尤其是 Safari — 不支持。</p>
</blockquote>
<pre><code class="language-js">/// file: src/hooks.server.js
import * as db from '$lib/server/database';
/** @type {import('@sveltejs/kit').ServerInit} */
export async function init() {
await db.connect();
}
</code></pre>
<blockquote>
<p>[!NOTE]<br>
在浏览器中,<code>init</code> 中的异步工作会延迟水合,所以要注意您在那里放什么。</p>
</blockquote>
<h2 id="通用-hook">通用 hook</h2>
<p>以下 hook 可以添加到 <code>src/hooks.js</code> 中。通用 hook 在服务端和客户端都运行(不要与共享 hook 混淆,后者是特定环境的)。</p>
<h3 id="reroute">reroute</h3>
<p>这个函数在 <code>handle</code> 之前运行,允许您更改 URL 如何转换为路由。返回的路径名(默认为 <code>url.pathname</code>)用于选择路由及其参数。</p>
<p>例如,您可能有一个 <code>src/routes/[]/about/+page.svelte</code> 页面,它应该可以访问为 <code>/en/about</code> 或 <code>/de/ueber-uns</code> 或 <code>/fr/a-propos</code>。您可以用 <code>reroute</code> 来实现:</p>
<pre><code class="language-js">/// file: src/hooks.js
// @errors: 2345
// @errors: 2304
/** @type {Record<string, string>} */
const translated = {
'/en/about': '/en/about',
'/de/ueber-uns': '/de/about',
'/fr/a-propos': '/fr/about'
};
/** @type {import('@sveltejs/kit').Reroute} */
export function reroute({ url }) {
if (url.pathname in translated) {
return translated;
}
}
</code></pre>
<p><code>lang</code> 参数将从返回的路径名正确派生。</p>
<p>使用 <code>reroute</code> 不会改变浏览器地址栏的内容,也不会改变 <code>event.url</code> 的值。</p>
<h3 id="传输">传输</h3>
<p>这是一组 <em>传输器</em>,允许您跨服务端/客户端边界传递自定义类型 - 从 <code>load</code> 和 form actions 返回的类型。每个传输器都包含一个 <code>encode</code> 函数,该函数对服务端上的值进行编码(或对任何不是该类型的实例返回 false),以及一个相应的 <code>decode</code> 函数:</p>
<pre><code class="language-js">/// file: src/hooks.js
import { Vector } from '$lib/math';
/** @type {import('@sveltejs/kit').Transport} */
export const transport = {
Vector: {
encode: (value) => value instanceof Vector && ,
decode: () => new Vector(x, y)
}
};
</code></pre>
<h2 id="进一步阅读">进一步阅读</h2>
<ul>
<li>教程: Hooks</li>
</ul>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档:SvelteKit 高级路由</p>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18798758
頁:
[1]