Svelte 深入解析 `<svelte:boundary>`:渲染错误边界与加载占位的特殊元素
2026/9/5 15:26:12 网站建设 项目流程

Svelte 深入解析<svelte:boundary>:渲染错误边界与加载占位的特殊元素

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

<svelte:boundary>是 Svelte 5.3.0 引入的特殊元素,用于把应用的局部区域「隔离」起来:它既能在内部的await表达式尚未解析完成时展示加载占位 UI,也能捕获渲染和副作用运行期间抛出的错误并提供降级 UI。读完本文,你将掌握pendingfailedonerror三个属性的完整用法,理解 Svelte 编译器与运行时如何协作实现错误边界,并学会通过transformError在 SSR 场景下安全地将服务端错误传递给浏览器端。

什么是<svelte:boundary>,能捕获什么

基本形态如下:

<svelte:boundary onerror={handler}>...</svelte:boundary>

该特性允许你「围起」应用的一部分,从而:

  • 在内部的await表达式 首次解析期间提供占位 UI;
  • 处理渲染过程或 effect 运行期间发生的错误,并在错误发生时渲染降级 UI。

有两条边界条件必须清楚:

  1. 错误被处理后,原内容会被移除。如果某个 boundary 处理了错误(提供了failedsnippet、onerror处理器或两者都有),它现有的内容将被移除,替换为错误 UI。
  2. 只捕获渲染过程中的错误。发生在渲染过程之外的错误——例如事件处理器中、setTimeout回调里或普通异步工作里抛出的错误——不会被错误边界捕获。

从编译器实现看,这一语义有两处对应的证据:分析阶段的访问器会调用mark_subtree_dynamic把整个 boundary 子树标记为动态节点(见 packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js);而运行时的错误冒泡只沿 effect 树向上寻找带BOUNDARY_EFFECT标记的节点(见下文「错误如何冒泡」一节),事件处理器不属于 effect 树,因此天然不在捕获范围内。

三个属性:pendingfailedonerror

<svelte:boundary>本身不做任何事情,除非至少提供以下属性之一。编译器在分析阶段对属性名做了严格白名单校验,只接受onerrorfailedpending三个合法属性名,出现其他属性或非法属性值会直接抛出编译错误(见 SvelteBoundary 分析访问器):

const valid = ['onerror', 'failed', 'pending']; // 对不在白名单内的属性抛出 svelte_boundary_invalid_attribute

pending:首次渲染的加载占位

pendingsnippet 在 boundary首次创建时显示,并持续可见,直到 boundary 内部所有await表达式都解析完成:

<svelte:boundary> <p>{await delayed('hello!')}</p> {#snippet pending()} <p>loading...</p> {/snippet} </svelte:boundary>

注意一个关键限制:pendingsnippet不会为后续的异步更新显示。对于这些场景,应使用$effect.pending()

在运行时源码中可以印证这个机制:Boundary 类 内部维护着一个待处理计数器#pending_count。首次渲染时如果计数大于 0,主体内容会被移入一个离屏DocumentFragment#offscreen_fragment),同时在原位渲染pendingsnippet;每当一个await解析完成,计数减一,归零时调用#resolve把离屏片段挂回 DOM 并暂停 pending effect。相关测试样例可在 packages/svelte/tests/runtime-runes/samples/async-boundary-pending-live 中查看。

补充说明:在 Svelte 官方 playground 中,应用本身就渲染在一个带空pendingsnippet 的 boundary 内,因此无需手动创建 boundary 即可使用await

failed:错误降级 UI

如果提供了failedsnippet,当 boundary 内部抛出错误时它会被渲染,接收error和一个用于重建内容的reset函数两个参数:

<svelte:boundary> <FlakyComponent /> {#snippet failed(error, reset)} <button onclick={reset}>oops! try again</button> {/snippet} </svelte:boundary>

与传给组件的 snippet 一样,failedsnippet 有两种提供方式——显式作为属性传入:

<svelte:boundary {failed}>...</svelte:boundary>

或在 boundary 内部直接声明(如上例)。这两种方式在客户端代码生成阶段被统一处理:客户端 SvelteBoundary 转换器 会把名为failed/pending的 snippet 块提取到属性对象中,最终生成形如$.boundary(node, { failed, pending }, (anchor) => { ... })的运行时调用(见 代码生成处)。

reset的语义在运行时有严格约束:Boundary.#create_reset 中用did_reset标志保证reset只能有效调用一次(重复调用在开发模式会给出警告),并禁止在onerror执行过程中调用reset

onerror:把错误带出 boundary

提供onerror函数时,它会以同样的errorreset两个参数被调用。这适合把错误上报到监控服务:

<svelte:boundary onerror={(e) => report(e)}> ... </svelte:boundary>

也可以利用errorreset在 boundary 外部渲染错误 UI:

<script> let error = $state(null); let reset = $state(() => {}); function onerror(e, r) { error = e; reset = r; } </script> <svelte:boundary {onerror}> <FlakyComponent /> </svelte:boundary> {#if error} <button onclick={() => { error = null; reset(); }}> oops! try again </button> {/if}

错误再冒泡规则:如果错误发生在onerror函数内部(或你重新抛出该错误),它会交给上层的父 boundary 处理。这一行为直接对应运行时实现:invoke_onerror 中捕获到异常后调用invoke_error_boundary(err, this.#effect && this.#effect.parent),从当前 boundary 的父 effect 继续向上查找。

错误如何冒泡到最近的 boundary

客户端运行时的错误捕获链路可以概括为三步(均在 packages/svelte/src/internal/client/error-handling.js 中):

  1. handle_error(error)拿到当前active_effect。如果错误发生在子树创建阶段REACTION_RAN未置位且不是$effect),直接throw让异常自然冒泡,直到遇到能处理它的 boundary;
  2. 否则调用invoke_error_boundary(error, effect),它沿 effect 树逐级向上走,寻找带BOUNDARY_EFFECT标志且未销毁的节点,找到后调用对应Boundary实例的error(error)方法并返回;
  3. 如果一路走到树顶仍无 boundary 接管,则最终抛出原始错误。

而 Boundary.error 内部还有一层保险:如果该 boundary 既没有onerror也没有failed,它会直接throw error把错误继续上抛给更外层的 boundary。此外,当错误发生在批处理(batch)的 fork 中时,处理动作会被推迟到oncommit,先跳过受影响的 main/pending/failed 三个 effect,避免脏状态提交。

SSR 下的行为与transformError

默认情况下,错误边界对服务端没有任何效果——如果渲染过程中出错,整个render(...)调用会失败。

从 5.51 版本起,对于带failedsnippet 的 boundary,可以通过给render(...)传入transformError函数来控制这一行为:

// @errors: 1005 import { render } from 'svelte/server'; import App from './App.svelte'; const { head, body } = await render(App, { transformError: (error) => { // log the original error, with the stack trace... console.error(error); // ...and return a sanitized user-friendly error // to display in the `failed` snippet return { message: 'An error occurred!' }; }; });

规则与限制如下:

  • transformError必须返回一个可 JSON 序列化的对象,该对象将用于渲染failedsnippet,并被序列化后嵌入 HTML,供浏览器端水合(hydrate)该 snippet;
  • 如果transformError抛出(或重新抛出)错误,整个render(...)将带着该错误失败;
  • 安全提示:SSR 期间产生的错误,其messagestack中可能包含敏感信息,强烈建议先脱敏(redact)再发送到浏览器,切勿原样透传;
  • 如果 boundary 配有onerror处理器,它会在水合时以反序列化后的错误对象被调用。

服务端实现位于 packages/svelte/src/internal/server/renderer.js:Renderer构造时把options.transformError挂到全局配置(见 render 选项定义),boundary(props, children_fn)方法为边界内容创建子渲染器并标记#boundary;异步收集内容失败时调用transformError得到转换后的错误,再由#serialize_failed_boundary将其序列化进 HTML 注释标记,作为该 boundary 的起始标记(见 serialize 实现)。服务端行为有对应测试覆盖,见 packages/svelte/src/internal/server/renderer.test.ts。

如果你是通过 SvelteKit 等框架使用 Svelte,通常无法直接访问render(...)调用——框架需要代你配置transformError。SvelteKit 计划通过handleError钩子提供此支持(该仓库不包含 SvelteKit 源码,此处仅作说明)。

mounthydrate函数同样接受transformError选项,默认是恒等函数。与render一样,该函数在错误传给failedsnippet 或onerror处理器之前对其进行转换。在客户端运行时中,这一机制体现为 Boundary 构造时的继承逻辑:

// Inherit transform_error from parent boundary, or use the provided one, or default to identity this.transform_error = transform_error ?? this.parent?.transform_error ?? ((e) => e);

即 transform 函数从父 boundary 继承、或使用显式传入值、或默认为恒等函数。处理错误时,#handle_error会先经过transform_error转换,且支持transformError返回 Promise 的异步场景(见 异步分支处理)。

水合:服务端已失败的 boundary 如何交给客户端

当服务端已通过transformError渲染出failedUI 时,客户端水合需要无缝接管。Boundary 构造函数的水合分支 通过读取起始注释的数据判断服务端渲染的是哪种状态:

  • 注释以HYDRATION_START_FAILED开头:说明服务端渲染了 failed snippet,其后紧跟JSON.parse解析出的序列化错误对象,客户端据此创建reset并立即渲染failedsnippet;
  • 注释为HYDRATION_START_ELSE:服务端渲染的是 pending 内容,客户端通过#hydrate_pending_content先显示 pending,再在微任务中把真实内容渲染到离屏片段,待await全部解析后替换;
  • 其他情况:按已解析内容正常水合。

值得注意的细节:水合阶段不允许onerror修改状态,因此 invoke_onerror 被排入微任务队列延迟执行。

服务端代码生成的一个优化

还有一个从 服务端 SvelteBoundary 转换器 中可以看到的实现细节:如果 boundary 没有failedsnippet 或failed属性,编译器在 SSR 输出中会完全跳过 boundary 包装,直接输出子内容,以节省字节并提升运行时性能——这与文档中「默认情况下服务端错误边界无效果」的表述完全一致。

实战验证路径

本文描述的客户端行为均有一手可运行的测试样例,位于 packages/svelte/tests/runtime-runes/samples/ 下,例如:

  • async-boundary-reset:验证reset后内容重建;
  • async-boundary-pending-live:验证 pending 占位的显示与撤除;
  • async-boundary-nav-raceasync-boundary-update-while-pending:验证 pending 期间的更新等边界场景。

服务端行为则由 packages/svelte/src/internal/server/renderer.test.ts 覆盖。

小结

能力属性 / 选项触发时机
加载占位pendingsnippet仅首次创建,内部await全部解析后撤除
错误降级 UIfailed(error, reset)snippet渲染/effect 阶段抛错且 boundary 已接管
错误带出onerror(error, reset)failed同时触发,可在 boundary 外使用
服务端错误转换render/mount/hydratetransformError(5.51+)返回 JSON 可序列化对象,供failed渲染与水合

<svelte:boundary>的三个属性都是可选的,按需组合即可:只要pending解决首次加载体验,只要failed+reset实现可恢复的降级 UI,只要onerror完成错误上报,而transformError则打通了 SSR 与浏览器端之间的错误传递链路。所有源码依据均可在上述链接指向的文件中直接查证。

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询