SvelteKit Hooks:控制应用级行为

SvelteKitAdvanced

SvelteKit Hooks

Hook 是你声明的应用级函数。SvelteKit 会在特定事件发生时调用它们,让你细致控制框架行为。

有三种 Hook 文件,它们都是可选的:

  • src/hooks.server.js:服务端 Hook。
  • src/hooks.client.js:客户端 Hook。
  • src/hooks.js:同时在客户端与服务端运行的 Hook。

这些模块中的代码会在应用启动时执行,适合初始化数据库客户端等工作。

handle

可添加到 src/hooks.server.js。

每当 SvelteKit 服务器收到请求时,此函数都会运行并决定响应,包括应用运行期间和预渲染期间。它接收表示请求的 event 对象,以及渲染路由并生成 Response 的 resolve 函数。你可以修改响应头、响应体,甚至完全绕过 SvelteKit,例如以编程方式实现路由。

src/hooks.server
/** @type {import('@sveltejs/kit/hooks').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;
}
import type { Handle } from '@sveltejs/kit/hooks';

export const handle: Handle = async ({ event, resolve }) => {
	if (event.url.pathname.startsWith('/custom')) {
		return new Response('custom response');
	}

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

静态资源请求(包括已经预渲染好的页面)不由 SvelteKit 处理。

如果 handle 由客户端发起的远程函数请求触发,route、params 和 url 表示调用远程函数的页面,而非 SvelteKit 为它创建的端点。不要用这些值判断用户是否有权访问数据,因为它们属于可被操纵的请求内容。导航时查询也不会自动重跑,除非导航导致查询参数改变,因此使用这些值时应留意这一点。

未实现时,默认行为为 ({ event, resolve }) => resolve(event)。

预渲染时,SvelteKit 会爬取页面中的链接并渲染发现的路由,调用 handle 及 load 等路由依赖。如果某些代码不应在此阶段执行,需先检查应用是否正在构建。

可以定义多个 handle,通过 sequence 辅助函数依次执行。

resolve 还接受可选的第二个参数,它是一个用于控制响应渲染的对象,可包含以下字段:

  • transformPageChunk(opts: { html: string, done: boolean }): MaybePromise<string | undefined>:自定义 HTML 转换。done 为真表示最后一块。每一块不保证是完整合法的 HTML,例如可能只有开始标签;但一定在合理边界处分割,如 %sveltekit.head% 或布局/页面组件边界。
  • filterSerializedResponseHeaders(name: string, value: string): boolean:决定 load 中通过 fetch 加载资源时,哪些响应头应进入序列化响应。默认不包含任何响应头。
  • preload(input: { type: 'js' | 'css' | 'asset', path: string } | { type: 'font', path: string, filename: string }): boolean:决定预加载哪些文件。通常通过在 <head> 中添加 <link> 实现;启用 output.linkHeaderPreload 后,动态渲染页面改用 Link 响应头。构建代码块时发现的每个文件都会传给该方法,例如页面导入的 CSS。开发模式没有构建时分析,因此不会调用它。预加载可提前下载资源,但无谓下载过多也会降低性能。默认预加载 JS 和 CSS;目前不预加载 asset,后续可能根据反馈调整。字体输入还含有相对项目根目录的源文件路径 filename,可用它代替哈希路径进行筛选。
src/hooks.server
/** @type {import('@sveltejs/kit/hooks').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;
}
import type { Handle } from '@sveltejs/kit/hooks';

export const handle: Handle = async ({ 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 自定义。详见错误处理文档。

locals

要向请求附加自定义数据,并传给 +server.js 处理器及服务端 load,可以填充 event.locals:

src/hooks.server
/** @type {import('@sveltejs/kit/hooks').Handle} */
export async function handle({ event, resolve }) {
	event.locals.user = await getUserInformation(event.cookies.get('sessionid'));

	const response = await resolve(event);

	// Note that modifying response headers isn't always safe.
	// Response objects can have immutable headers
	// (e.g. Response.redirect() returned from an endpoint).
	// Modifying immutable headers throws a TypeError.
	// In that case, clone the response or avoid creating a
	// response object with immutable headers.
	response.headers.set('x-custom-header', 'potato');

	return response;
}
import type { Handle } from '@sveltejs/kit/hooks';

export const handle: Handle = async ({ event, resolve }) => {
	event.locals.user = await getUserInformation(event.cookies.get('sessionid'));

	const response = await resolve(event);

	// Note that modifying response headers isn't always safe.
	// Response objects can have immutable headers
	// (e.g. Response.redirect() returned from an endpoint).
	// Modifying immutable headers throws a TypeError.
	// In that case, clone the response or avoid creating a
	// response object with immutable headers.
	response.headers.set('x-custom-header', 'potato');

	return response;
};

handleFetch

可添加到 src/hooks.server.js。

此 Hook 允许修改或替换服务端(包括预渲染)中 event.fetch 的结果,调用位置可以是端点、load、action、handle、handleError 或 reroute。

例如,客户端导航到页面时,load 请求 https://api.yourapp.com;而在 SSR 期间,直接访问内部 API、绕过公网代理和负载均衡器可能更合适。

src/hooks.server
/** @type {import('@sveltejs/kit/hooks').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);
}
import type { HandleFetch } from '@sveltejs/kit/hooks';

export const handleFetch: HandleFetch = async ({ 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);
};

event.fetch 遵循浏览器凭据模型。同源请求会转发 cookie 与 authorization,除非 credentials 为 omit。跨源请求中,如果目标是应用域名的子域,也会包含 cookie,例如应用位于 my-domain.com,API 位于 api.my-domain.com。

例外是应用与 API 分别位于同级子域,例如 www.my-domain.com 与 api.my-domain.com。属于共同父域 my-domain.com 的 cookie 不会自动转发,因为 SvelteKit 不知道 cookie 所属的域。此时需在 handleFetch 中手动添加:

src/hooks.server
/** @type {import('@sveltejs/kit/hooks').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);
}
import type { HandleFetch } from '@sveltejs/kit/hooks';
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
	if (request.url.startsWith('https://api.my-domain.com/')) {
		request.headers.set('cookie', event.request.headers.get('cookie'));
	}

	return fetch(request);
};

handleError

可添加到 src/hooks.server.js 和 src/hooks.client.js。

加载、渲染或响应请求时发生的错误会调用此函数,允许你:

  • 记录错误。
  • 生成可安全展示给用户的错误表示,省略敏感消息或堆栈等细节。

除 event 外,Hook 还接收用于区分错误来源的 kind 和错误本身:

  • app:由应用的 error(...) 产生。error 是符合 App.Error 的错误体,默认直接使用该错误体。
  • framework:由 SvelteKit 产生,例如 404、405、413。error 为 { status, message },其中 message 是 Not Found 等安全文本;默认返回相同对象。
  • validation(仅服务端):远程函数参数未通过其 Standard Schema 校验。error 为 { status: 400, message: 'Bad Request' },issues 包含校验问题。默认只返回错误对象,除非显式返回,否则不会公开 issues。要访问校验库特有的字段,可给 HandleServerError 传入问题类型,例如 HandleServerError<CustomIssue>。
  • unknown:来源于你的代码或所调用代码,但具体原因未知。error 是抛出的值,可能包含不宜公开的信息。默认返回 { status: 500, message: 'Internal Error' }。

下一节“Errors”进一步解释这些类别。重定向不是错误,不会进入该 Hook。

返回值应符合 App.Error;status 和 message 可省略,只有要覆盖以上默认值时才返回它们。

如果为 App.Error 增加了必填属性,Hook 必须返回这些属性。

通过扩展 App.Error 接口,可以类型安全地向 page.error 添加信息。内置的 status 和 message 已经存在,无需重新声明。例如,添加一个跟踪 ID,便于用户联系支持人员时提供:

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

export {};
src/hooks.server
import * as Sentry from '@sentry/sveltekit';

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

/** @type {import('@sveltejs/kit/hooks').HandleServerError} */
export async function handleError({ kind, error, event }) {
	if (kind === 'app') {
		// you created this error with `error(...)`, so it already
		// matches `App.Error` — pass it through unchanged
		return error;
	}

	const errorId = crypto.randomUUID();

	if (kind === 'framework') {
		// a 404 (or similar) — `error.status` and `error.message` are safe to
		// expose, so we keep them and just add our own property
		return { ...error, errorId };
	}

	// example integration with https://sentry.io/
	Sentry.captureException(error, {
		extra: { event, errorId }
	});

	// `status` and `message` are optional — we only override `message`,
	// so the status stays at its default of 500
	return {
		message: 'Whoops!',
		errorId
	};
}
import * as Sentry from '@sentry/sveltekit';
import type { HandleServerError } from '@sveltejs/kit/hooks';

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

export const handleError: HandleServerError = async ({ kind, error, event }) => {
	if (kind === 'app') {
		// you created this error with `error(...)`, so it already
		// matches `App.Error` — pass it through unchanged
		return error;
	}

	const errorId = crypto.randomUUID();

	if (kind === 'framework') {
		// a 404 (or similar) — `error.status` and `error.message` are safe to
		// expose, so we keep them and just add our own property
		return { ...error, errorId };
	}

	// example integration with https://sentry.io/
	Sentry.captureException(error, {
		extra: { event, errorId }
	});

	// `status` and `message` are optional — we only override `message`,
	// so the status stays at its default of 500
	return {
		message: 'Whoops!',
		errorId
	};
};
src/hooks.client
import * as Sentry from '@sentry/sveltekit';

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

/** @type {import('@sveltejs/kit/hooks').HandleClientError} */
export async function handleError({ kind, error, event }) {
	if (kind === 'app') {
		return error;
	}

	const errorId = crypto.randomUUID();

	if (kind === 'framework') {
		return { ...error, errorId };
	}

	// example integration with https://sentry.io/
	Sentry.captureException(error, {
		extra: { event, errorId }
	});

	return {
		message: 'Whoops!',
		errorId
	};
}
import * as Sentry from '@sentry/sveltekit';
import type { HandleClientError } from '@sveltejs/kit/hooks';

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

export const handleError: HandleClientError = async ({ kind, error, event }) => {
	if (kind === 'app') {
		return error;
	}

	const errorId = crypto.randomUUID();

	if (kind === 'framework') {
		return { ...error, errorId };
	}

	// example integration with https://sentry.io/
	Sentry.captureException(error, {
		extra: { event, errorId }
	});

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

在 src/hooks.client.js 中,handleError 的类型为 HandleClientError,而非 HandleServerError;event 为 NavigationEvent,而非 RequestEvent。

已经由服务端 Hook 转换过的错误,不会再次交给客户端 Hook。

开发期间,如果错误来自 Svelte 代码的语法错误,传入的 error 会附带一个突出显示错误位置的 frame 属性。

确保 handleError 本身永远不抛出错误。

init

可添加到 src/hooks.server.js 和 src/hooks.client.js。

服务器创建或浏览器应用启动时,此函数运行一次,适合执行初始化数据库连接等异步工作。

如果目标环境支持顶层 await,init 与在模块顶层编写初始化逻辑并无本质区别;对不支持该特性的目标环境,应使用合适的兼容方式。原文将 Safari 列为需要留意的环境。

src/hooks.server
import * as db from '#lib/server/database.js';

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

export const init: ServerInit = async () => {
	await db.connect();
};

浏览器中的异步 init 会延迟水合,应谨慎选择其中的工作。

reroute

可添加到 src/hooks.js,在服务端和客户端均运行。

该函数在 handle 之前执行,用于改变 URL 到路由的映射。返回的路径名(默认 url.pathname)用于选择路由和参数。

例如,src/routes/[[lang]]/about/+page.svelte 需要通过 /en/about、/de/ueber-uns、/fr/a-propos 访问,可这样使用 reroute:

src/hooks

/** @type {Record<string, string>} */
const translated = {
	'/en/about': '/en/about',
	'/de/ueber-uns': '/de/about',
	'/fr/a-propos': '/fr/about',
};

/** @type {import('@sveltejs/kit/hooks').Reroute} */
export function reroute({ url }) {
	if (url.pathname in translated) {
		return translated[url.pathname];
	}
}
import type { Reroute } from '@sveltejs/kit/hooks';
const translated: Record<string, string> = {
	'/en/about': '/en/about',
	'/de/ueber-uns': '/de/about',
	'/fr/a-propos': '/fr/about',
};

export const reroute: Reroute = ({ url }) => {
	if (url.pathname in translated) {
		return translated[url.pathname];
	}
};

lang 参数会从返回的路径名中正确提取。

reroute 不会改变浏览器地址栏,也不会改变 event.url。

从 2.18 起,reroute 可以是异步函数,例如向后端获取路由决策。务必保持快速,否则会拖慢导航。需要获取数据时,应使用参数提供的 fetch,它具有 load 中 fetch 的同类优势;但由于尚未确定路由,handleFetch 无法获取 params 和 id。

src/hooks

/** @type {import('@sveltejs/kit/hooks').Reroute} */
export async function reroute({ url, fetch }) {
	// Ask a special endpoint within your app about the destination
	if (url.pathname === '/api/reroute') return;

	const api = new URL('/api/reroute', url);
	api.searchParams.set('pathname', url.pathname);

	const result = await fetch(api).then(r => r.json());
	return result.pathname;
}
import type { Reroute } from '@sveltejs/kit/hooks';
export const reroute: Reroute = async ({ url, fetch }) => {
	// Ask a special endpoint within your app about the destination
	if (url.pathname === '/api/reroute') return;

	const api = new URL('/api/reroute', url);
	api.searchParams.set('pathname', url.pathname);

	const result = await fetch(api).then(r => r.json());
	return result.pathname;
};

reroute 被视为纯函数和幂等函数:相同输入必须得到相同输出,且不产生副作用。SvelteKit 基于这些假设在客户端缓存结果,因此每个唯一 URL 只调用一次。

transport

可添加到 src/hooks.js,在服务端和客户端均运行。

这是一个传输器集合,用于将 load 或表单 action 返回的自定义类型跨越服务端/客户端边界传输。每个传输器包含服务端编码值的 encode(非该类型实例时返回假值),以及对应的 decode:

src/hooks
import { Vector } from '#lib/math.js';

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

export const transport: Transport = {
	Vector: {
		encode: (value) => value instanceof Vector && [value.x, value.y],
		decode: ([x, y]) => new Vector(x, y)
	}
};

延伸阅读

  • 教程:Hooks

在 GitHub 编辑本页;llms.txt 文档入口。

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

请登录后发表评论

    暂无评论内容