Yew 框架 inner_html 示例详解:如何手动渲染未转义 HTML
2026/9/19 5:55:16 网站建设 项目流程

Yew 框架 inner_html 示例详解:如何手动渲染未转义 HTML

【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

导读

本篇文章围绕 Yew 官方仓库中的inner_html示例(位于 examples/inner_html)展开,讲解如何在不借助html!宏的前提下,通过Html::from_html_unchecked将一段原始 HTML 字符串直接注入虚拟 DOM 并完成渲染。读完本文,你将掌握 Yew 中"原始 HTML"(VRaw)这一 VNode 变体的核心用法、浏览器端与 SSR 端的渲染行为差异,以及它在 DOM 中的实际挂载与 diff 原理,从而在需要渲染富文本、内联 SVG 等场景时做出正确的技术选型。

示例概览:一个不经过html!宏的组件

inner_html示例的核心思路非常直接:整个页面的内容被写成一段常量 HTML 字符串,保存在 Rust 源码中,渲染时 Yew 查询 DOM、创建元素并把这段 HTML 赋值给元素的innerHTML

示例的组件实现极为精简(examples/inner_html/src/main.rs):

use yew::{Component, Context, Html}; const HTML: &str = include_str!("document.html"); pub struct App; impl Component for App { type Message = (); type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self } fn view(&self, _ctx: &Context<Self>) -> Html { Html::from_html_unchecked(HTML.into()) } } fn main() { yew::Renderer::<App>::new().render(); }

这里值得注意的三个要点:

  1. include_str!将 HTML 文件编译进二进制:HTML 内容存放在同目录的 document.html 中,通过include_str!在编译期嵌入,运行时零 IO。
  2. Html::from_html_unchecked绕过宏解析view不再返回html! { ... }宏产物,而是直接把字符串包装为Html类型返回。
  3. Renderer::<App>::new().render()标准挂载方式:与其它客户端渲染(CSR)示例完全一致,入口不受影响。

概念一:手动创建Html,绕过html!

正常情况下,Yew 开发者通过html!宏书写声明式模板。而本示例演示的是完全不同的路径——直接构造Html值。

从源码看,Html本质上是虚拟节点枚举VNode的类型别名,而VNode一共包含八种变体(packages/yew/src/virtual_dom/vnode.rs):

  • VTag:绑定到 DOMElement的虚拟标签;
  • VText:绑定到文本节点;
  • VComp:绑定到子组件;
  • VList:一组节点的持有者;
  • VPortal:渲染到文档其它位置的传送门;
  • VRef:持有任意Node的引用,用于节点替换;
  • VSuspense:可挂起的文档片段;
  • VRaw:一段原始 HTML 字符串——这正是本示例使用的变体。

from_html_unchecked的实现非常直白(packages/yew/src/virtual_dom/vnode.rs):

pub fn from_html_unchecked(html: AttrValue) -> Self { VNode::VRaw(VRaw { html }) }

VRaw本身也只是一个极简的结构体(packages/yew/src/virtual_dom/vraw.rs):

pub struct VRaw { pub html: AttrValue, }

方法名中的unchecked(未检查)是刻意为之的:该方法不会对 HTML 做任何消毒或校验,传入的字符串是否合法、是否包含恶意脚本,完全由开发者自己负责。官方文档的注释也明确警告:The contents are not sanitized or validated. You, as the developer, are responsible to ensure the HTML string passed to this method are valid and not malicious

html!宏的混用

from_html_unchecked返回的Html可以像普通节点一样被嵌入到html!宏的模板中。官方 API 文档给出了这样的用法示例(packages/yew/src/virtual_dom/vnode.rs):

use yew::{AttrValue, Html, html}; let parsed = Html::from_html_unchecked(AttrValue::from("<div>content</div>")); let _: Html = html! { <div> {parsed} </div> };

这意味着你可以在大部分模板仍由html!宏生成的前提下,仅对某一段需要原样输出的富文本内容(如来自 Markdown 渲染器、CMS 后端或用户富文本编辑器的 HTML)使用VRaw局部注入。

概念二:借助web-sys操纵 DOM 的底层实现

虽然示例的组件代码中没有直接调用web-sys,但它引入了相关依赖(examples/inner_html/Cargo.toml):

[dependencies] yew = { path = "../../packages/yew", features = ["csr"] } gloo.workspace = true [dependencies.web-sys] workspace = true features = ["console", "Document", "Element", "Node", "Window"]

Yew 框架内部正是在 DOM 捆绑层(dom_bundle)使用web-sysDocumentElementNode等类型来实现VRaw的真实挂载。这部分逻辑位于 packages/yew/src/dom_bundle/braw.rs。

浏览器端:innerHTML + childNodes 提取

BRaw::create_elements揭示了核心机制(packages/yew/src/dom_bundle/braw.rs):

fn create_elements(html: &str, parent_namespace: Option<&str>) -> Vec<Node> { let div = if parent_namespace.is_some() { gloo::utils::document() .create_element_ns(parent_namespace, "div") .unwrap() } else { gloo::utils::document().create_element("div").unwrap() }; div.set_inner_html(html); let children = div.child_nodes(); // ... 将 childNodes 转成 Vec<Node> 返回 }

流程分为三步:

  1. 创建一个临时<div>元素(若父节点处于 SVG 或 MathML 命名空间,则使用create_element_ns在对应命名空间下创建);
  2. 通过set_inner_html把原始 HTML 字符串赋给临时元素;
  3. 取回child_nodes(),将解析出的子节点列表作为待插入 DOM 的节点集合。

之后attach会把这些节点逐个插入到父元素中指定位置,并记录第一个节点作为reference以及节点总数(packages/yew/src/dom_bundle/braw.rs)。这样设计是为了后续的 detach(移除)与 shift(迁移)能基于引用节点与计数完成整段操作,而不需要逐个记录子节点。

命名空间处理:为何 SVG 可以内联

示例的 HTML 片段中包含内联 SVG(examples/inner_html/src/document.html):

<h2>Inline HTML with SVG</h2> <p> The whole contents of this page is stored as a constant HTML string in the Rust source code. The code queries the DOM, creates a new element, and applies this snippet of HTML to the element's innerHTML. </p> <svg height="250" width="500"> <polygon points="220,10 300,210 170,250 123,234" style="fill: lime; stroke: purple; stroke-width: 1" /> Sorry, your browser does not support inline SVG. </svg>

attach在创建元素前会检查父节点的命名空间 URI(packages/yew/src/dom_bundle/braw.rs):若父节点属于SVG_NAMESPACEMATHML_NAMESPACE(定义于 packages/yew/src/virtual_dom/vtag.rs),则临时元素也在相同命名空间下创建,从而保证插入的 SVG 节点解析正确。

差分更新:字符串整体替换而非逐节点 diff

VRawreconcile_node实现了一个实用的优化(packages/yew/src/dom_bundle/braw.rs):当新旧 HTML 字符串完全相等时,直接复用原有节点位置,不做任何 DOM 操作;只有字符串发生变化时,才走reconcile分支。而reconcile中由于"无法对字符串内部做细粒度 diff"(源码注释原文:we don't have a way to diff what's changed in the string),会先移除旧节点再重新附加新节点(packages/yew/src/dom_bundle/braw.rs)。

这一点对性能调优有直接指导意义:把频繁变化的内容拆出VRaw,只让真正静态的富文本走原始 HTML 注入,可以避免整段 DOM 被反复销毁重建。

服务端渲染(SSR)行为差异

VRaw还实现了 SSR 场景下的输出逻辑(packages/yew/src/virtual_dom/vraw.rs):在服务端,HTML 字符串会被直接写入输出流,不做任何解析或处理;若开启了可水合(hydratable)模式,则会额外包裹收集标签以便客户端水合时识别原始节点边界。

因此同一段VRaw在两种渲染目标下语义一致:客户端借助innerHTML解析,服务端原样输出,二者都不会转义内容。

安全边界:为什么叫unchecked

这是使用本示例技术时最重要的红线:

  • 不转义VRaw中的 HTML 不会被转义为文本节点,而是作为真实 HTML 解析;
  • 不消毒:框架不会剥离<script>onerror等危险内容;
  • 责任在开发者:如果注入内容包含用户输入,必须先自行消毒(例如接入 DOMPurify 类方案)再传入from_html_unchecked

只有在内容来源完全可信(如自己维护的常量、受控的富文本渲染管线)时,才应该放心使用这一 API。官方在方法文档中同样以Warning标注了这一约束。

运行示例

在仓库根目录下,使用 Trunk 开发服务器启动该示例:

cd examples/inner_html trunk serve --open

相关配套文件:

  • examples/inner_html/index.html:入口页面,通过<link contenteditable="false">【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew

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

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

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

立即咨询