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。读完本文,你将掌握pending、failed、onerror三个属性的完整用法,理解 Svelte 编译器与运行时如何协作实现错误边界,并学会通过transformError在 SSR 场景下安全地将服务端错误传递给浏览器端。
什么是<svelte:boundary>,能捕获什么
基本形态如下:
<svelte:boundary onerror={handler}>...</svelte:boundary>该特性允许你「围起」应用的一部分,从而:
- 在内部的
await表达式 首次解析期间提供占位 UI; - 处理渲染过程或 effect 运行期间发生的错误,并在错误发生时渲染降级 UI。
有两条边界条件必须清楚:
- 错误被处理后,原内容会被移除。如果某个 boundary 处理了错误(提供了
failedsnippet、onerror处理器或两者都有),它现有的内容将被移除,替换为错误 UI。 - 只捕获渲染过程中的错误。发生在渲染过程之外的错误——例如事件处理器中、
setTimeout回调里或普通异步工作里抛出的错误——不会被错误边界捕获。
从编译器实现看,这一语义有两处对应的证据:分析阶段的访问器会调用mark_subtree_dynamic把整个 boundary 子树标记为动态节点(见 packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js);而运行时的错误冒泡只沿 effect 树向上寻找带BOUNDARY_EFFECT标记的节点(见下文「错误如何冒泡」一节),事件处理器不属于 effect 树,因此天然不在捕获范围内。
三个属性:pending、failed与onerror
<svelte:boundary>本身不做任何事情,除非至少提供以下属性之一。编译器在分析阶段对属性名做了严格白名单校验,只接受onerror、failed、pending三个合法属性名,出现其他属性或非法属性值会直接抛出编译错误(见 SvelteBoundary 分析访问器):
const valid = ['onerror', 'failed', 'pending']; // 对不在白名单内的属性抛出 svelte_boundary_invalid_attributepending:首次渲染的加载占位
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函数时,它会以同样的error和reset两个参数被调用。这适合把错误上报到监控服务:
<svelte:boundary onerror={(e) => report(e)}> ... </svelte:boundary>也可以利用error和reset在 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 中):
handle_error(error)拿到当前active_effect。如果错误发生在子树创建阶段(REACTION_RAN未置位且不是$effect),直接throw让异常自然冒泡,直到遇到能处理它的 boundary;- 否则调用
invoke_error_boundary(error, effect),它沿 effect 树逐级向上走,寻找带BOUNDARY_EFFECT标志且未销毁的节点,找到后调用对应Boundary实例的error(error)方法并返回; - 如果一路走到树顶仍无 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 期间产生的错误,其
message和stack中可能包含敏感信息,强烈建议先脱敏(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 源码,此处仅作说明)。
mount和hydrate函数同样接受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-race、async-boundary-update-while-pending:验证 pending 期间的更新等边界场景。
服务端行为则由 packages/svelte/src/internal/server/renderer.test.ts 覆盖。
小结
| 能力 | 属性 / 选项 | 触发时机 |
|---|---|---|
| 加载占位 | pendingsnippet | 仅首次创建,内部await全部解析后撤除 |
| 错误降级 UI | failed(error, reset)snippet | 渲染/effect 阶段抛错且 boundary 已接管 |
| 错误带出 | onerror(error, reset) | 与failed同时触发,可在 boundary 外使用 |
| 服务端错误转换 | render/mount/hydrate的transformError(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),仅供参考