Yew 与 WebGL 实战:用 NodeRef + web-sys 在 Rust/Wasm 中绘制 Canvas 渲染循环
【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew
Yew 是一个使用 Rust 编写可靠且高效 Web 应用的框架(Rust / Wasm framework for creating reliable and efficient web applications)。本文以仓库中的 examples/webgl 示例为主线,讲解如何用 Yew 组件驱动 WebGL:通过NodeRef获取<canvas>DOM 元素、用web-sys初始化 GL 上下文、编写着色器并建立基于requestAnimationFrame的渲染循环。读完本文,你将掌握在 Yew 应用中直接调用浏览器 WebGL API 的完整落地路径,包括类型转换、着色器绑定、缓冲区上传与逐帧更新等关键环节。
示例概览:Yew 官方 WebGL Demo 做了什么
examples/webgl是 Yew 仓库中一个极简但完整的 WebGL 示例。它的目标非常明确:
- 用 Yew 渲染一个
<canvas>元素; - 通过
NodeRef拿到真实的HtmlCanvasElement; - 用
web-sys的WebGlRenderingContext初始化 GL 上下文; - 编写基础顶点着色器与片元着色器,绘制两个覆盖整个画布的三角形;
- 借助
requestAnimationFrame建立渲染循环,用u_time时间统一变量驱动画面颜色持续变化。
该示例对应的 HTML 入口 index.html 十分精简,只声明了data-trunk rel="rust"让 Trunk 完成 Rust 到 Wasm 的构建:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Yew • WebGL</title> <link>pub struct App { node_ref: NodeRef, } impl Component for App { type Message = (); type Properties = (); fn create(_ctx: &Context<Self>) -> Self { Self { node_ref: NodeRef::default(), } } fn view(&self, _ctx: &Context<Self>) -> Html { html! { <canvas ref={self.node_ref.clone()} /> } } // ... }从源码结构看,NodeRef在 packages/yew/src/html/mod.rs 中定义为pub struct NodeRef(Rc<RefCell<NodeRefInner>>),内部持有Option<Node>,且实现了Default、Clone与ImplicitClone,因此可以在组件字段与html!宏的ref属性之间安全克隆传递。
在 rendered 生命周期中取出节点
DOM 只有在渲染完成后才存在,因此获取节点必须放在rendered回调里。示例在first_render为true时才启动渲染循环(examples/webgl/src/main.rs):
fn rendered(&mut self, _ctx: &Context<Self>, first_render: bool) { // Only start the render loop if it's the first render // There's no loop cancellation taking place, so if multiple renders happen, // there would be multiple loops running. That doesn't *really* matter here because // there's no props update and no SSR is taking place, but it is something to keep in // consideration if !first_render { return; } let canvas = self.node_ref.cast::<HtmlCanvasElement>().unwrap(); let gl: GL = canvas .get_context("webgl") .unwrap() .unwrap() .dyn_into() .unwrap(); Self::render_gl(gl); }其中cast::<HtmlCanvasElement>()对应NodeRef::cast的实现(packages/yew/src/html/mod.rs):它要求目标类型满足AsRef<Node> + From<JsValue>,从而把泛型Node转成具体的HtmlCanvasElement。示例源码注释也提醒了first_render判断的意义:由于这里没有做循环取消,若组件多次渲染就会启动多个渲染循环;当前场景没有 props 更新、也不涉及 SSR,所以无碍,但这是值得留意的通用注意事项。
此外文档明确提示:不要手动修改 Yew 渲染出的 DOM 树,NodeRef应当被视为只读访问(见 node-refs.mdx)。WebGL 的绘图发生在 canvas 的 GL 上下文中,不属于对 DOM 树的篡改,因此与本约定并不冲突。
核心概念二:用 web-sys 调用浏览器 JavaScript API
web-sys是 wasm-bindgen 生态中提供 Web API 绑定的 crate。本示例通过 Cargo.toml 按需启用所需 feature:
[dependencies] js-sys.workspace = true wasm-bindgen.workspace = true yew = { path = "../../packages/yew", features = ["csr"] } [dependencies.web-sys] workspace = true features = [ 'HtmlCanvasElement', 'WebGlBuffer', 'WebGlProgram', 'WebGlRenderingContext', 'WebGlShader', 'WebGlUniformLocation', ]这些 feature 全部来自工作区统一声明的web-sys = "0.3.70"(见仓库根目录 Cargo.toml)。启用后即可在 Rust 中以近乎原生的类型安全方式调用浏览器 API,例如:
let canvas = self.node_ref.cast::<HtmlCanvasElement>().unwrap(); let gl: GL = canvas .get_context("webgl") .unwrap() .unwrap() .dyn_into() .unwrap();这里get_context("webgl")返回的是一个Option<JsValue>形式的Result,需要用两次unwrap()再加一次dyn_into()才能得到WebGlRenderingContext。dyn_into来自wasm_bindgen::JsCasttrait,是 wasm-bindgen 中做动态类型转换的标准手段。
从零搭建渲染管线:缓冲区、着色器与程序
render_gl函数集中演示了 WebGL 渲染管线的标准装配流程(examples/webgl/src/main.rs)。着色器源码通过include_str!在编译期直接嵌入 Rust 二进制,避免了运行时网络加载:
let vert_code = include_str!("./basic.vert"); let frag_code = include_str!("./basic.frag");顶点数据与缓冲区
示例用 6 个顶点(即两个三角形)铺满整个裁剪空间,形成覆盖全画布的四边形:
let vertices: Vec<f32> = vec![ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, ]; let vertex_buffer = gl.create_buffer().unwrap(); let verts = js_sys::Float32Array::from(vertices.as_slice()); gl.bind_buffer(GL::ARRAY_BUFFER, Some(&vertex_buffer)); gl.buffer_data_with_array_buffer_view(GL::ARRAY_BUFFER, &verts, GL::STATIC_DRAW);关键点在于:Rust 的Vec<f32>不能直接交给 WebGL,必须先经js_sys::Float32Array::from(...)转成 JavaScript 的 TypedArray,再通过buffer_data_with_array_buffer_view上传到 GPU,这正是js-sys在示例中发挥的作用。
着色器编译与链接
接下来依次创建顶点着色器与片元着色器,编译后链接成着色器程序(examples/webgl/src/main.rs):
let vert_shader = gl.create_shader(GL::VERTEX_SHADER).unwrap(); gl.shader_source(&vert_shader, vert_code); gl.compile_shader(&vert_shader); let frag_shader = gl.create_shader(GL::FRAGMENT_SHADER).unwrap(); gl.shader_source(&frag_shader, frag_code); gl.compile_shader(&frag_shader); let shader_program = gl.create_program().unwrap(); gl.attach_shader(&shader_program, &vert_shader); gl.attach_shader(&shader_program, &frag_shader); gl.link_program(&shader_program); gl.use_program(Some(&shader_program));这套“创建 → 写源码 → 编译 → 挂载 → 链接 → 启用”的流程与原生 JavaScript WebGL 教程完全对应,只是把每个 API 都换成了 Rust 类型安全的调用。
顶点属性与 uniform 绑定
顶点位置通过 attributea_position交给着色器,时间通过 uniformu_time传入(examples/webgl/src/main.rs):
let position = gl.get_attrib_location(&shader_program, "a_position") as u32; gl.vertex_attrib_pointer_with_i32(position, 2, GL::FLOAT, false, 0, 0); gl.enable_vertex_attrib_array(position); let time = gl.get_uniform_location(&shader_program, "u_time"); gl.uniform1f(time.as_ref(), timestamp as f32); gl.draw_arrays(GL::TRIANGLES, 0, 6);vertex_attrib_pointer_with_i32告诉 GPU 每个顶点取 2 个FLOAT分量(x、y),步长与偏移为 0;u_time是片元着色器中驱动颜色变化的核心参数,首次绘制时传入0.0。
两个着色器的配合
顶点着色器 basic.vert 非常朴素,只把二维位置透传为裁剪坐标:
precision mediump float; attribute vec2 a_position; void main() { gl_Position = vec4(a_position, 0.0, 1.0); }片元着色器 basic.frag 用u_time的正弦值生成随时间流动的 RGB 颜色:
precision mediump float; uniform float u_time; void main() { float r = sin(u_time * 0.0003); float g = sin(u_time * 0.0005); float b = sin(u_time * 0.0007); gl_FragColor = vec4(r, g, b, 1.0); }三个正弦函数使用不同频率系数(0.0003 / 0.0005 / 0.0007),使红、绿、蓝三通道以不同速率振荡,从而产生持续渐变的动态画面。
渲染循环:requestAnimationFrame 与闭包的自引用技巧
示例的逐帧更新没有使用gloo的渲染封装,而是直接调用window.requestAnimationFrame。它在 main.rs 中封装为一个静态方法:
fn request_animation_frame(f: &Closure<dyn FnMut()>) { window() .unwrap() .request_animation_frame(f.as_ref().unchecked_ref()) .expect("should register `requestAnimationFrame` OK"); }难点在于:每一帧回调执行完后要再次注册下一帧,即闭包需要“引用自己”。示例采用Rc<RefCell<Option<Closure>>>先占位、后填充的标准技巧(examples/webgl/src/main.rs):
let cb = Rc::new(RefCell::new(None)); *cb.borrow_mut() = Some(Closure::wrap(Box::new({ let cb = cb.clone(); move || { // This should repeat every frame timestamp += 20.0; gl.uniform1f(time.as_ref(), timestamp as f32); gl.draw_arrays(GL::TRIANGLES, 0, 6); App::request_animation_frame(cb.borrow().as_ref().unwrap()); } }) as Box<dyn FnMut()>)); App::request_animation_frame(cb.borrow().as_ref().unwrap());这段代码的含义:
- 创建可变的共享容器
cb,初始为None; - 用
Closure::wrap把 Rust 闭包包装成 wasm-bindgen 可传给 JS 的回调; - 闭包内部先克隆
cb,实现循环引用注册——每一帧结束时调用App::request_animation_frame把同一个闭包再次交给浏览器; timestamp += 20.0让时间每帧递增,驱动u_time变化,随后重新上传 uniform 并draw_arrays重绘。
示例源码注释还专门解释了为什么不复用gloo-render:request_animation_frame存在每帧额外的闭包包装逻辑,属于不必要的开销;这里只构造一次包装闭包,性能更优。
启动方式与依赖说明
运行命令
按官方 README,在examples/webgl目录下用 Trunk 开发服务器启动即可:
trunk serve --open该命令会编译 Rust 到 Wasm、启动本地开发服务器并自动在浏览器打开页面,看到由正弦颜色驱动的全屏渐变画面即代表 WebGL 管线正常工作。完整可运行前提包括:本机装有 Rust 工具链、wasm32-unknown-unknowntarget 与trunk命令行工具。
依赖约束
示例依赖全部来自工作区统一定义(Cargo.toml):wasm-bindgen 0.2、js-sys 0.3、web-sys 0.3.70。由于 WebGL 的获取与绘制都发生在浏览器环境,示例以features = ["csr"]依赖本地yew包(examples/webgl/Cargo.toml),即纯客户端渲染模式,不需要服务端渲染能力。
延伸:官方 README 中列出的改进方向
README 的 “Improvements” 一节给出了一条明确的后续路线:使用更加花哨的着色器(a much more flashy shader)。当前 basic.frag 仅用三个正弦函数做颜色渐变,是最小可运行示例。感兴趣的读者可以从以下几点入手扩展:
- 增加
varying插值变量,让颜色随顶点位置在片元间平滑过渡; - 引入
WebGLBuffer之外的多属性布局(法线、UV 等),把示例升级为三维网格渲染; - 加入交互:监听鼠标/触摸事件修改 uniform,实现实时形变或粒子系统;
- 注意示例注释中提到的循环生命周期管理——若引入 props 更新或 SSR,需要为渲染循环增加取消逻辑,避免多次启动。
小结
通过examples/webgl这个示例,可以完整看到 Yew 与浏览器底层 API 协同工作的三种关键能力:一是NodeRef在rendered生命周期中把虚拟 DOM 桥接到真实 DOM 节点(packages/yew/src/html/mod.rs);二是web-sys以类型安全方式调用 WebGL 全套 API;三是Rc<RefCell<Option<Closure>>>模式实现可自续的requestAnimationFrame渲染循环。这套“组件 + 引用 + 原生 API”的组合拳,是后续在 Yew 中实现 WebGL 游戏、数据可视化或 GPU 计算等高阶场景的基础模板。
【免费下载链接】yewRust / Wasm framework for creating reliable and efficient web applications项目地址: https://gitcode.com/gh_mirrors/ye/yew
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考