Yew Mount Point 实战:用 web-sys 接管挂载流程,让 Yew 应用与原生 DOM 共存
2026/9/19 3:32:54 网站建设 项目流程

Yew Mount Point 实战:用 web-sys 接管挂载流程,让 Yew 应用与原生 DOM 共存

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

这篇技术指南围绕 Yew 官方示例examples/mount_point展开,讲解如何在 Yew 项目中绕过yew::start_app的默认挂载行为,借助web-sysgloo提供的 JavaScript API 先向页面写入原生 DOM(如 Canvas 画布),再通过yew::Renderer::<App>::with_root(...)将 Yew 组件挂载到指定的挂载点,最终实现"原生内容不受 Yew 重渲染影响、两者和平共存"的混合渲染方案。读完本文,你将掌握自定义挂载点、自定义根元素的完整流程,以及它与多实例挂载(two_apps示例)的关系。

示例概览:绿色方块、输入框与倒序文本

examples/mount_point页面呈现的效果由两部分组成:

  1. 原生 DOM 部分:一个由web-sys绘制的 100×100 绿色方块(Canvas),直接追加到<body>中,完全不经过 Yew 的虚拟 DOM;
  2. Yew 组件部分:一个输入框与一段文本,文本内容实时显示输入框中字符串的逆序结果。

两者的组合说明了一个核心事实:Yew 并不要求占据整个页面。只要把 Yew 组件挂载到明确的根节点上,页面其余部分可以由原生 DOM、第三方库或另一个 Yew 应用自由支配。

Concepts:本示例要演示的两项核心技术

原文档明确列出了两个概念要点:

  • 使用 web-sys 调用 JavaScript API:本示例通过web-sysDocumentHtmlCanvasElementCanvasRenderingContext2d等类型直接操作浏览器 DOM 和 Canvas,这是纯 Rust 前端开发的常用方式;
  • 接管通常由yew::start_app处理的挂载过程:示例不再依赖start_app自动选择挂载位置,而是手动创建挂载点并调用Renderer::with_root精确控制 Yew 应用渲染到哪个元素上。

需要说明的是,当前版本的仓库中yew::start_app函数已不再以同名形式导出(搜索packages/yew/src中已无start_app的定义),取而代之的是yew::Renderer。这与原文档的写作时间有关——文档描述的"通常由yew::start_app处理"在历史上确实成立,而在本仓库的现行代码中,等价能力由Renderer提供(见下文源码分析)。

完整源码剖析

示例的核心代码位于examples/mount_point/src/main.rs,分为"Yew 组件"与"原生挂载流程"两部分。

Yew 组件:一个带状态的反转文本组件

use wasm_bindgen::JsValue; use web_sys::{ CanvasRenderingContext2d, Document, HtmlCanvasElement, HtmlInputElement, InputEvent, }; use yew::{Component, Context, Html, TargetCast, html}; pub enum Msg { UpdateName(String), } pub struct App { name: String, } impl Component for App { type Message = Msg; type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self { name: "Reversed".to_owned(), } } fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool { match msg { Msg::UpdateName(new_name) => { self.name = new_name; true } } } fn view(&self, ctx: &Context<Self>) -> Html { html! { <div> <input value={self.name.clone()} oninput={ctx.link().callback(|e: InputEvent| { let input = e.target_unchecked_into::<HtmlInputElement>(); Msg::UpdateName(input.value()) })} /> <p>{ self.name.chars().rev().collect::<String>() }</p> </div> } } }

要点说明:

  • 初始文本为"Reversed",因此页面加载后输入框内是Reversed,其下方文本显示逆序的desreveR
  • oninput回调通过e.target_unchecked_into::<HtmlInputElement>()(来自yew::TargetCasttrait)把事件目标强转为输入框元素,再读取input.value()取得当前输入值;
  • view中使用self.name.chars().rev().collect::<String>()实现按字符逆序,true返回值通知调度器本组件状态已变化、需要重渲染。

原生 Canvas 绘制:绿色方块

fn create_canvas(document: &Document) -> HtmlCanvasElement { let canvas = HtmlCanvasElement::from(JsValue::from(document.create_element("canvas").unwrap())); canvas.set_width(100); canvas.set_height(100); let ctx = CanvasRenderingContext2d::from(JsValue::from(canvas.get_context("2d").unwrap().unwrap())); ctx.set_fill_style_str("green"); ctx.fill_rect(10., 10., 50., 50.); canvas }

这段代码展示了"用 web-sys 直接操作浏览器 API"的典型手法:

  1. document.create_element("canvas")创建原生<canvas>元素,通过JsValue::from转换为HtmlCanvasElementwasm_bindgen::JsValue是 WebAssembly 边界上的通用值包装);
  2. 设置画布尺寸为 100×100;
  3. 通过canvas.get_context("2d")获取 2D 绘图上下文CanvasRenderingContext2d
  4. 设置填充色为绿色(set_fill_style_str("green")),在(10, 10)处绘制 50×50 的矩形。

注意此处的坐标以像素为单位,画布 100×100、矩形 50×50,位于画布左上角偏移 10px 处,因此页面左侧会看到一个明显的绿色方块。

主流程:手动接管挂载

fn main() { let document = gloo::utils::document(); let body = document.query_selector("body").unwrap().unwrap(); let canvas = create_canvas(&document); // This canvas won't be overwritten by yew! body.append_child(&canvas).unwrap(); let mount_point = document.create_element("div").unwrap(); let class_list = mount_point.class_list(); class_list.add_1("mount-point").unwrap(); body.append_child(&mount_point).unwrap(); yew::Renderer::<App>::with_root(mount_point).render(); }

主流程分为四步:

  1. 获取页面文档与 bodygloo::utils::document()返回当前Documentdocument.query_selector("body")定位到<body>
  2. 先挂原生内容:把 Canvas 追加到<body>。源码注释// This canvas won't be overwritten by yew!直接点明意图——由于 Canvas 在 Yew 接管之前、且位于 Yew 挂载点之外,Yew 后续的虚拟 DOM 更新不会触碰它;
  3. 创建 Yew 挂载点:动态创建一个带mount-pointclass 的<div>,追加到<body>末尾;
  4. 挂载 Yew 应用yew::Renderer::<App>::with_root(mount_point).render()App组件渲染进该 div。

关键点在于:Yew 只会管理挂载点内部的内容。挂载点之外的 DOM(本例中的 Canvas)不在 Yew 虚拟 DOM 的管辖范围内,因此无论如何重渲染都不会被覆盖或清除。

自定义根元素:Renderer::with_root 的源码级解读

Renderer定义于packages/yew/src/renderer.rs,是当前版本 Yew 应用的标准入口(模块注释即写明 "This is the main entry point of a Yew application")。它的构造方式有四种:

构造方法根元素属性(Properties)适用场景
Renderer::<COMP>::new()<body>默认值最简单,整页应用
Renderer::<COMP>::with_root(root)自定义元素默认值本示例,自定义挂载点
Renderer::<COMP>::with_props(props)<body>自定义需要向根组件传参且整页渲染
Renderer::<COMP>::with_root_and_props(root, props)自定义元素自定义既要指定挂载点又要传参

本示例使用的是第二种。其实现如下(packages/yew/src/renderer.rs#L56-L68):

impl<COMP> Renderer<COMP> where COMP: BaseComponent<Properties: Default> + 'static, { /// Creates a [Renderer] that renders into the document body with default properties. pub fn new() -> Self { Self::default() } /// Creates a [Renderer] that renders into a custom root with default properties. pub fn with_root(root: Element) -> Self { Self::with_root_and_props(root, Default::default()) } }

从这里可以得出两个实现事实:

  • with_root要求组件类型参数COMP满足Properties: Default(本示例AppProperties(),天然满足);
  • with_root内部委托给with_root_and_props,以Default::default()作为根组件属性。

最终渲染动作发生在render()(同文件#L91-L95),它会设置默认 panic hook(将 Rust panic 输出到浏览器控制台,可通过set_custom_panic_hook覆盖),然后调用AppHandle::<COMP>::mount_with_props(self.root, ...)mount_with_props定义于packages/yew/src/app_handle.rs#L31-L42,其中先clear_element(&host)清空宿主元素内部,再通过BSubtree::create_root(&host)Scope::mount_in_place(...)建立虚拟 DOM 子树并完成首轮渲染。

这里蕴含一个值得注意的行为:mount_with_props会先清空挂载点内部,所以如果直接把 Canvas 塞进挂载点 div 内部,它同样会被清除。这正是示例先把 Canvas 追加到<body>、再单独创建挂载点 div 的原因——保持挂载点内部为空,把原生内容放在挂载点外部

给挂载点传属性的扩展

如果根组件需要接收属性,可以使用with_root_and_propsRendererwith_root_and_propspackages/yew/src/renderer.rs#L87-L89)直接构造Self { root, props },然后同样经render()完成挂载。例如在自定义根上挂载一个需要Properties的组件:

let root = document.create_element("div").unwrap(); yew::Renderer::<App>::with_root_and_props(root, AppProps { /* ... */ }).render();

环境与依赖配置

Cargo.toml:CSR 特性与 web-sys features

examples/mount_point/Cargo.toml展示了运行本示例所需的依赖组合:

[dependencies] wasm-bindgen.workspace = true yew = { path = "../../packages/yew", features = ["csr"] } gloo.workspace = true [dependencies.web-sys] workspace = true features = [ "CanvasRenderingContext2d", "Document", "DomTokenList", "Element", "HtmlCanvasElement", "Node", "Window" ]
  • yew通过path指向仓库内的packages/yew,并启用csr(Client-Side Rendering)特性——Renderer类型正是在#[cfg(feature = "csr")]条件下编译的;
  • web-sys按需开启本示例用到的模块:Document(create_element/query_selector)、HtmlCanvasElement(Canvas 元素)、CanvasRenderingContext2d(2D 绘图)、Node(append_child)等;
  • gloo用于gloo::utils::document()快捷获取Document

index.html 与 Trunk.toml

examples/mount_point/index.html<body>是空的,挂载所需的 DOM 全部由main()在运行时创建:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Yew • Mount Point</title> <link>trunk serve --open

examples/mount_point目录下执行该命令即可启动 Trunk 开发服务器并自动打开浏览器。运行前提是:

  • 已安装 Rust 工具链与wasm32-unknown-unknowntarget(rustup target add wasm32-unknown-unknown);
  • 已安装 Trunk(cargo install trunk);
  • 本地能够解析yew = { path = "../../packages/yew" }的相对依赖路径(即在本仓库的 examples 目录下运行)。

与 two_apps 示例的关联与差异

原文档在 Improvements 一节中注明:本示例与examples/two_apps非常相似,二者应当合并为单个示例。两个示例确实共享同一核心 API——自定义挂载点:

维度mount_pointtwo_apps
挂载数量单实例双实例
挂载方式动态创建 div 后with_root按 CSS 选择器查找已存在元素后with_root
原生 DOM 混排有(Canvas 在挂载点之外)无(页面全部由 Yew 渲染)
实例间通信不涉及通过Scope::send_message/AppHandle互发消息

examples/two_apps/src/main.rs中的挂载辅助函数如下(#L72-L76):

fn mount_app(selector: &'static str) -> AppHandle<App> { let document = gloo::utils::document(); let element = document.query_selector(selector).unwrap().unwrap(); yew::Renderer::<App>::with_root(element).render() }

它的两个实例分别挂载到.first-app.second-app两个选择器命中的元素上,然后通过AppHandlesend_messageAppHandleScope实现Deref,见packages/yew/src/app_handle.rs#L69)在实例之间互发消息,实现标题的 Ping-Pong 传递。

这两个示例共同说明了Renderer::with_root的通用价值:它既能用于"Yew 与原生 DOM 混排"(mount_point),也能用于"同一页面运行多个 Yew 应用实例"(two_apps)。合并建议仅是维护层面的规划(避免两个高度相似的示例重复),不影响各自当前的参考价值。

进阶应用:挂载策略的几种典型场景

综合上述源码事实,Renderer的自定义根机制可支撑以下典型场景:

  1. 渐进式接入现有页面:老页面无需整体重写,只在一个div上挂载 Yew 组件,其余部分保持原样——与 mount_point 的原生 Canvas 混排思路一致;
  2. 多实例同页:把不同业务模块分别挂到不同根节点,实现模块化隔离(two_apps 的用法);
  3. 与 SSR 水合衔接Renderer在启用hydration特性时还提供hydrate()方法(packages/yew/src/renderer.rs#L98-L111),可对服务端渲染输出的 HTML 进行水合,对应 Yew 的ServerRendererpackages/yew/src/server_renderer.rs)体系;
  4. 动态创建与销毁应用render()返回的AppHandle拥有update(props)destroy()方法(packages/yew/src/app_handle.rs#L55-L66),可在运行时更新根组件属性或销毁整个应用——这是dyn_create_destroy_apps示例(examples/dyn_create_destroy_apps)所演示的能力。

小结

examples/mount_point用不到百行代码演示了 Yew 挂载机制的精确控制:原生 Canvas 先于 Yew 写入<body>,Yew 应用随后挂载到独立的 div 上,二者互不干扰。其技术核心是yew::Renderer::with_root(以及底层的Renderer::with_root_and_propsAppHandle::mount_with_props调用链),这一 API 让 Yew 从"整页框架"变成了可以按需嵌入任意根元素的组件渲染引擎。无论是混排原生 DOM、同页多实例,还是接入 SSR 水合,理解自定义挂载点是掌握 Yew 应用生命周期管理的第一步。

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

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

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

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

立即咨询