Svelte `{@html}` 标签完全指南:向组件注入原始 HTML 的原理、限制与安全实践
在 Svelte 中,普通插值 {content} 会自动对字符串进行转义以保证安全,而当你需要渲染已经清洗过的富文本、Markdown 转换产物或第三方 HTML 片段时,就必须使用模板语法中的 {@html ...} 标签。本篇基于 Svelte 官方文档 08-@html.md,完整覆盖 {@html} 的用法、表达式约束、样式作用域规则,并结合 Svelte 编译器与运行时源码,讲清它在客户端渲染、服务端渲染和 hydration 三个场景下的实际行为,帮助你在安全地引入原始 HTML 的同时理解其底层实现。
基本用法:注入原始 HTML
要向组件中注入未经转义的原始 HTML,使用 {@html ...} 标签,把表达式写在标签内部:
<article>
{@html content}
</article>
其中 content 是一个字符串表达式,其内容会被当作 HTML 直接解析并插入 DOM,而不是像 {content} 那样被转义为纯文本。
安全警告:官方文档明确要求——你必须对传入的字符串做转义/清洗处理,或者只填充自己可控的值,以防止 XSS 攻击。切勿渲染未经净化的内容(例如直接来自用户输入的 HTML)。
从 Svelte 编译器源码可以确认这一“零信任”设计:客户端实现 html.js 中明确注释了 @html 本质上就是 .innerHTML = ... 的等价物——由于安全考虑,innerHTML 赋值的 <script> 标签同样不会被执行,Svelte 运行时也没有做任何额外的脚本补执行逻辑。
表达式约束:必须是独立的合法 HTML
文档指出了两条硬性限制,这也是 {@html} 与普通标签插值最大的行为差异:
1. 内容必须是“可独立存在的合法 HTML”
下面这种写法是无效的,因为单独的 </div> 不是合法的 HTML 片段:
{@html '<div>'}content{@html '</div>'}
不能把一对开始/结束标签拆散到两个 {@html} 之间再“拼合”,每个 {@html} 表达式自身都必须能独立解析。仓库中的编译器错误测试 raw-mustaches-whitespace 同样验证了这类标签语法的严格性:写成 {@htmlfoo} 这种标签名后紧跟字符的形态会直接触发编译错误。
在解析阶段,{@html 是一个特殊的内建标签:解析器在 tag.js 中识别到 html 关键字后,会要求其后必须跟随空白、读取一个完整表达式、再闭合 },最终生成 AST 节点 HtmlTag。
2. 不会编译其中的 Svelte 代码
{@html '<div onclick=...>'} 中的 onclick、{#if} 等 Svelte 语法都不会被当作 Svelte 代码编译和绑定——它们只是普通文本,最终渲染出的 DOM 节点也不带 Svelte 的事件监听与更新逻辑。分析阶段的访问器 HtmlTag.js 中有一处关键处理:mark_subtree_dynamic(context.path) 会把包含 {@html} 的子树整体标记为动态子树,注释写明这是“为了修复非法 HTML”的必要操作——也就是说编译器不会尝试去“理解”注入的 HTML 结构,而是把控制权完全交给浏览器。
客户端渲染实现:模板元素、命名空间与 hydration
深入 客户端转换代码 与运行时 html() 函数,可以看清 {@html} 的三条核心执行路径:
1. 非受控路径(默认)——使用 <template> 包装解析。
当 {@html} 独立出现在模板中时,编译器会先插入一个注释锚点(push_comment),运行时通过创建 <template> 元素并设置其 innerHTML 来解析 HTML,再把解析出的 DocumentFragment 移动到锚点前。这个设计的直接原因就是文档中“必须是独立合法 HTML”的约束:<template> 要求内容是完整片段。若 {@html} 位于 <svg> 或 <math> 上下文,运行时会自动改用带对应命名空间的 <svg> / <math> 包装元素,保证 SVG/MathML 内容不被降级成 HTML 命名空间的节点。
2. 受控路径(controlled)——直接操作父元素 innerHTML。
当 {@html} 是某个父元素的唯一子内容时(例如 contenteditable 场景),运行时会直接 parent_node.innerHTML = value,绕开锚点注释。源码注释指出,这样也顺便解决了用户在 contenteditable 中手动删掉锚点注释导致更新失效的问题。仓库中的测试 html-tag-contenteditable 专门覆盖了这一场景。
3. 值未变化则跳过更新。
运行时用 if (value === (value = get_value() ?? '')) return 做快速去重:同一字符串值不会被重复写入 DOM,这是对频繁更新的富文本内容的一项重要性能保护。
hydration 时的行为: 在 hydration 阶段,运行时不会尝试修复服务端与客户端内容之间的差异——源码注释 明确写道“刻意不尝试修复不匹配,因为成本高且易错(而且内容不一致本身就是边缘情况)”。它只沿 DOM 寻找下一个锚点注释、把已有节点“认账”给 Svelte 管理;若找不到锚点则抛出 hydration mismatch 错误。相关测试可参考 html-tag-hydration 与 raw-repair。
此外运行时还支持 TrustedHTML 对象:若传入的是 CSP 机制下的 TrustedHTML(如 policy.createHTML(...) 的返回值),会直接赋值给 innerHTML 而不做字符串强制转换,见 html.js。
服务端渲染与异步表达式
在服务端(SSR)路径中,服务端 HtmlTag 转换 会生成 $.html(...) 调用,直接把(转义后的)字符串推入渲染输出流;若表达式内部包含 await(即标记为 async),则会包裹进一个异步子块并经由 $$renderer.push 输出——这对应 Svelte 的异步渲染能力。测试用例 async-html-tag(SSR) 和 async-html-tag(客户端) 分别验证了两种端的异步 {@html} 行为。
样式:{@html} 内容对 Svelte 是“不可见”的
文档的 Styling 一节指出:以 {@html} 方式渲染的内容对 Svelte 而言是“不可见”的,因此不会获得 scoped styles(作用域样式)。下面的写法是无效的,a 和 img 的样式会被当作未使用的选择器处理(在产物中被移除):
<article>
{@html content}
</article>
<style>
article {
a { color: hotpink }
img { width: 100% }
}
</style>
正确做法是使用 :global 修饰符来命中 <article> 内部的所有内容(对应 Svelte 文档中的 scoped styles 说明):
<style>
article:global {
a { color: hotpink }
img { width: 100% }
}
</style>
其根源在于:{@html} 的 HTML 是运行时才由浏览器解析生成的 DOM 节点,编译期的 CSS 分析器(如 css.js 所实现的剪枝逻辑)看不到这些节点,因此与它们相关的局部选择器会被判定为“未使用”。仓库中的 CSS 测试 global-block 与 global-local 正是验证 :global 与局部样式组合后的选择器输出。
实战建议小结
- 只把已净化(如经 DOMPurify 类库清洗)或自己生成的 HTML 传入
{@html},用户原始输入必须先经过转义/白名单过滤; - 每个
{@html}表达式必须能独立解析为合法 HTML,不能把标签拆到多个表达式中拼装; - 注入的内容不执行其中的
<script>,也不编译其中的 Svelte 语法,事件交互需通过事件委托等外部手段实现; - 依赖 CSS 时,用
:global选择器命中注入内容,否则样式会被编译期剪枝丢弃; - 需要验证具体行为时,可参考 hydration 测试目录 中的
raw-*、html-tag-*系列用例,以及 runtime-runes 测试 中关于转义字符的处理示例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00