SvelteKit 最新中文文档教程(13)—— Hooks

前言

Svelte,一个语法简洁、入门容易,面向未来的前端框架。

从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1

Svelte 以其独特的编译时优化机制著称,具有轻量级高性能易上手等特性,非常适合构建轻量级 Web 项目

为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。

如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。

Hooks

“Hooks” 是您声明的应用程序范围的函数,SvelteKit 会在响应特定事件时调用它们,让您能够对框架的行为进行更为精细的控制。

有三个 hook 文件,都是可选的:

  • src/hooks.server.js — 您的应用程序的服务端 hook
  • src/hooks.client.js — 您的应用程序的客户端 hook
  • src/hooks.js — 您的应用程序的在客户端和服务端都运行的 hook

这些模块中的代码会在应用程序启动时运行,这使得它们对初始化数据库客户端等操作很有用。

[!NOTE] 您可以通过 config.kit.files.hooks 配置这些文件的位置。

服务端 hook

以下 hook 可以添加到 src/hooks.server.js 中:

handle

这个函数在 SvelteKit 服务端每次接收到 request 时运行 — 无论是在应用程序运行时,还是在预渲染过程中 — 并决定response。

它接收一个表示请求的 event 对象和一个名为 resolve 的函数,该函数渲染路由并生成一个 Response。这允许您修改响应头或响应体,或完全绕过 SvelteKit(例如,用于以编程方式实现路由)。

/// 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;
}
/// 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;
}
/// 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; }

[!NOTE] 对静态资源的请求 — 包括已经预渲染的页面 — 不会由 SvelteKit 处理。

如果未实现,默认为 ({ event, resolve }) => resolve(event)

locals

要向请求中添加自定义数据(这些数据会传递给 +server.js 中的处理程序和服务端的 load 函数),可以填充 event.locals 对象,如下所示。

/// 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;
}
/// 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;
}
/// 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; }

您可以定义多个 handle 函数,并使用sequence 辅助函数执行它们。

resolve 还支持第二个可选参数,让您能够更好地控制响应的渲染方式。该参数是一个对象,可以包含以下字段:

  • transformPageChunk(opts: { html: string, done: boolean }): MaybePromise<string | undefined> — 对 HTML 应用自定义转换。如果 done 为 true,则是最后一个块。块不保证是格式良好的 HTML(例如,它们可能包含一个元素的开始标签但没有结束标签),但它们总是会在合理的边界处分割,比如 %sveltekit.head% 或布局/页面组件。
  • filterSerializedResponseHeaders(name: string, value: string): boolean — 确定当 load 函数使用 fetch 加载资源时,哪些头部应该包含在序列化的响应中。默认情况下,不会包含任何头部。
  • preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean — 确定应该在 <head> 标签中添加哪些文件以预加载。该方法在构建代码块时被调用,每个找到的文件都会被调用 — 例如,如果您在 +page.svelte 中有 import './styles.css,在访问该页面时,preload 将传入该 CSS 文件的解析路径进行调用。注意,在开发模式下不会调用 preload,因为它依赖于构建时的分析。预加载可以通过更早下载资源来提高性能,但如果不必要地下载太多内容也会适得其反。默认情况下,会预加载 jscss 文件。目前不会预加载 asset 文件,但我们可能会在评估反馈后添加此功能。
/// 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;
}
/// 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;
}
/// 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; }

注意,resolve(...) 永远不会抛出错误,它总是会返回一个带有适当状态码的 Promise<Response>。如果在 handle 期间其他地方抛出错误,这将被视为致命错误,SvelteKit 将根据 Accept 头部返回错误的 JSON 表示或回退错误页面 — 后者可以通过 src/error.html 自定义。您可以在这里阅读更多关于错误处理的信息。

handleFetch

这个函数允许您修改(或替换)在服务端上运行的 loadaction 函数中发生的 fetch 请求(或在预渲染期间)。

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

/// 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);
}
/// 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);
}
/// 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); }

认证凭据

对于同源请求,除非 credentials 选项设置为 "omit",否则 SvelteKit 的 fetch 实现会转发 cookieauthorization 头部。

对于跨源请求,如果请求 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:

/// 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);
}
/// 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);
}
/// 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); }

共享 hook

以下 hook 可以同时添加到 src/hooks.server.jssrc/hooks.client.js 中:

handleError

如果在加载或渲染期间抛出意外错误,此函数将被调用,并传入 erroreventstatus 代码和 message。这允许两件事:

  • 您可以记录错误
  • 您可以生成一个安全的、显示给用户的自定义错误表示,省略敏感的详细信息,如消息和堆栈跟踪。返回的值(默认为 { message })会成为 $page.error 的值。

对于从您的代码(或您的代码调用的库代码)抛出的错误,状态将为 500,消息将为 “Internal Error”。虽然 error.message 可能包含不应暴露给用户的敏感信息,但 message 是安全的(尽管对普通用户来说没有意义)。

要以类型安全的方式向 $page.error 对象添加更多信息,您可以通过声明 App.Error 接口(必须包含 message: string,以保证合理的回退行为)来自定义预期的形状。这允许您 — 例如 — 附加一个跟踪 ID,供用户在与技术支持人员通信时引用:

/// file: src/app.d.ts
declare global {
namespace App {
interface Error {
message: string;
errorId: string;
}
}
}
export {};
/// file: src/app.d.ts
declare global {
  namespace App {
    interface Error {
      message: string;
      errorId: string;
    }
  }
}

export {};
/// file: src/app.d.ts declare global { namespace App { interface Error { message: string; errorId: string; } } } export {};
/// 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
};
}
/// 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
  };
}
/// 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 }; }
/// 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
};
}
/// 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
  };
}
/// 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 }; }

[!NOTE] 在 src/hooks.client.js 中,handleError 的类型是 HandleClientError 而不是 HandleServerError,并且 event 是一个 NavigationEvent 而不是 RequestEvent

此函数不会因为预期的错误(那些使用从 @sveltejs/kit 导入的 error 函数抛出的错误)而被调用。

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

[!NOTE] 确保 handleError 永远不会抛出错误

init

这个函数在服务端创建或应用程序在浏览器中启动时运行一次,是执行异步工作(如初始化数据库连接)的有用位置。

[!NOTE] 如果您的环境支持顶级 await,init 函数实际上与在模块顶层编写初始化逻辑没有什么不同,但一些环境 — 尤其是 Safari — 不支持。

/// file: src/hooks.server.js
import * as db from '$lib/server/database';
/** @type {import('@sveltejs/kit').ServerInit} */
export async function init() {
await db.connect();
}
/// file: src/hooks.server.js
import * as db from '$lib/server/database';

/** @type {import('@sveltejs/kit').ServerInit} */
export async function init() {
  await db.connect();
}
/// file: src/hooks.server.js import * as db from '$lib/server/database'; /** @type {import('@sveltejs/kit').ServerInit} */ export async function init() { await db.connect(); }

[!NOTE]
在浏览器中,init 中的异步工作会延迟水合,所以要注意您在那里放什么。

通用 hook

以下 hook 可以添加到 src/hooks.js 中。通用 hook 在服务端和客户端都运行(不要与共享 hook 混淆,后者是特定环境的)。

reroute

这个函数在 handle 之前运行,允许您更改 URL 如何转换为路由。返回的路径名(默认为 url.pathname)用于选择路由及其参数。

例如,您可能有一个 src/routes/[[lang]]/about/+page.svelte 页面,它应该可以访问为 /en/about/de/ueber-uns/fr/a-propos。您可以用 reroute 来实现:

/// 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[url.pathname];
}
}
/// 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[url.pathname];
  }
}
/// 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[url.pathname]; } }

lang 参数将从返回的路径名正确派生。

使用 reroute 不会改变浏览器地址栏的内容,也不会改变 event.url 的值。

传输

这是一组 传输器,允许您跨服务端/客户端边界传递自定义类型 – 从 load 和 form actions 返回的类型。每个传输器都包含一个 encode 函数,该函数对服务端上的值进行编码(或对任何不是该类型的实例返回 false),以及一个相应的 decode 函数:

/// file: src/hooks.js
import { Vector } from '$lib/math';
/** @type {import('@sveltejs/kit').Transport} */
export const transport = {
Vector: {
encode: (value) => value instanceof Vector && [value.x, value.y],
decode: ([x, y]) => new Vector(x, y)
}
};
/// file: src/hooks.js
import { Vector } from '$lib/math';

/** @type {import('@sveltejs/kit').Transport} */
export const transport = {
  Vector: {
    encode: (value) => value instanceof Vector && [value.x, value.y],
    decode: ([x, y]) => new Vector(x, y)
  }
};
/// file: src/hooks.js import { Vector } from '$lib/math'; /** @type {import('@sveltejs/kit').Transport} */ export const transport = { Vector: { encode: (value) => value instanceof Vector && [value.x, value.y], decode: ([x, y]) => new Vector(x, y) } };

进一步阅读

  • 教程: Hooks

Svelte 中文文档

点击查看中文文档:SvelteKit 高级路由

系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!

此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog

欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。

来源链接:https://www.cnblogs.com/yayujs/p/18798758

© 版权声明
THE END
支持一下吧
点赞7 分享
评论 抢沙发
头像
请文明发言!
提交
头像

昵称

取消
昵称表情代码快捷回复

    暂无评论内容