ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于 wasm-bindgen 与 web-sys 打造 Canvas 画板:Paint 示例全解析

基于 wasm-bindgen 与 web-sys 打造 Canvas 画板:Paint 示例全解析 开发工具【免费下载链接】wasm-bindgenFacilitating high-level interactions between Wasm modules and JavaScript项目地址https://gitcode.com/gh_mirrors/wa/wasm-bindgen点击查看免费下载Paint 是 wasm-bindgen 仓库中一个典型的DOM 交互型示例它用 Rust 直接创建canvas元素、获取 2D 绘图上下文、并通过 JavaScript 闭包监听鼠标事件最终在浏览器中实现一个可拖拽画线的迷你画板程序。本文以 examples/paint/README.md 为骨架结合 src/lib.rs、Cargo.toml 与 webpack.config.js 等仓库源码带你完整走通从构建、运行到逐行理解绘图逻辑的实战流程读完即可照搬这套wasm-bindgen web-sys 事件闭包的模式去开发自己的浏览器图形应用。示例概览一个用 Rust 写成的画板Paint 示例的核心目标非常朴素在浏览器页面中创建一个 640x480 的canvas画布用户按下鼠标开始落笔拖动鼠标绘制路径松开鼠标结束一笔。整个过程完全由 Rust 代码驱动——DOM 操作、CSS 样式、Canvas 2D API 和鼠标事件监听都通过web-sys绑定实现#[wasm_bindgen(start)]入口在 Wasm 模块加载后自动执行。官方对该示例的定位在 guide/src/examples/paint.md 中有明确描述A simple painting program并指出其Cargo.toml启用了操作 DOM、事件和 2D canvas 所必需的特性。也就是说这个示例是理解Rust 如何直接接管浏览器绘图与交互的最佳入门素材之一。构建与运行一条命令起服务仓库内 examples/paint/README.md 给出了本地运行的全部操作步骤全文核心只有两条信息用npm run serve构建并启动开发服务器然后在浏览器访问http://localhost:8080即可看到效果$ npm run serve这条命令的实际执行链是 package.json 中预定义的脚本{ scripts: { build: webpack, serve: webpack serve } }也就是说npm run serve本质上调用的是webpack servewebpack-dev-server。它由仓库根目录的 pnpm workspace见 pnpm-workspace.yaml统一管理依赖版本package.json中的catalog:即工作区目录引用catalog protocol声明的开发依赖包括wasm-tool/wasm-pack-plugin在 webpack 构建流程中自动调用 wasm-pack 编译 Rust cratehtml-webpack-plugin以index.html为模板生成 HTMLwebpack/webpack-cli/webpack-dev-server构建与开发服务器本体。在运行之前建议先执行一次npm install安装依赖仓库采用 pnpm workspace若使用 pnpm 则为pnpm install由于该示例依赖wasm-bindgen、web-sys、js-sys的本地源码版本见下文Cargo.toml的path依赖无需发布到 crates.io 即可直接编译这也是仓库内示例通用的做法。Cargo.toml为 DOM 与 Canvas 精准开启 web-sys 特性examples/paint/Cargo.toml 是整个示例的 Rust 侧配置几个要点逐一看[package] authors [The wasm-bindgen Developers] edition 2021 name wasm-bindgen-paint publish false version 0.0.0 [lib] crate-type [cdylib]crate-type [cdylib]这是 wasm 最终产物最常用的 crate 类型wasm-bindgen 官方文档guide/src/examples/hello-world.md专门强调过largely used for wasm final artifacts today即把 Rust 库编译成可以被 JS 侧直接import的 Wasm 模块publish false示例不发布到 crates.io仅供仓库内使用。依赖部分值得细读它充分体现了 wasm-bindgen 生态的三件套[dependencies] js-sys { path ../../crates/js-sys } wasm-bindgen { path ../../ } [dependencies.web-sys] features [ CanvasRenderingContext2d, CssStyleDeclaration, Document, Element, EventTarget, HtmlCanvasElement, HtmlElement, MouseEvent, Node, Window, ] path ../../crates/web-syswasm-bindgen提供核心宏与运行时#[wasm_bindgen]、Closure等对应仓库根目录的src/js-sys提供 ECMAScript 标准 API 的绑定对应 crates/js-sysweb-sys提供 Web APIDOM、Canvas、事件等的绑定对应 crates/web-sys且采用 feature 门控——只有显式开启的特性才会被编译这正是上方features列表的意义。对照 src/lib.rs 的代码逐行看每个特性都有真实用途web-sys 特性对应代码用途Window/Document/Node/Elementweb_sys::window()、document()、create_element、append_childHtmlCanvasElementdyn_into::HtmlCanvasElement()类型转换CanvasRenderingContext2dget_context(2d)与begin_path/move_to/line_to/strokeCssStyleDeclarationcanvas.style().set_property(border, solid)EventTarget/MouseEventadd_event_listener_with_callback与鼠标事件对象这种按需开启特性的设计是 web-sys 的核心使用模式不要盲目开启全部特性而是对照代码逐项列出可显著减小最终 Wasm 体积。源码逐段拆解start 入口到事件闭包examples/paint/src/lib.rs 共 57 行完整实现了整个画板逻辑。下面按代码执行顺序拆解每一段。1. 入口#[wasm_bindgen(start)]use std::cell::Cell; use std::rc::Rc; use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] fn start() - Result(), JsValue {#[wasm_bindgen(start)]是 wasm-bindgen 提供的一个特殊入口属性被标记的函数会在 Wasm 模块实例化完成后自动执行无需 JS 侧显式调用。返回Result(), JsValue允许把 DOM 操作中的错误直接以JsValue形式向上抛出。注意这里提前引入了Rc和Cell——它们将在闭包共享状态环节发挥关键作用。2. 创建画布、设置样式并挂载let document web_sys::window().unwrap().document().unwrap(); let canvas document .create_element(canvas)? .dyn_into::web_sys::HtmlCanvasElement()?; document.body().unwrap().append_child(canvas)?; canvas.set_width(640); canvas.set_height(480); canvas.style().set_property(border, solid)?;这段代码通过web_sys依次完成取window→ 取document→create_element(canvas)创建元素 → 用dyn_into将泛型Element向下转型为HtmlCanvasElement→ 追加到body→ 设置 640x480 尺寸 → 通过style()返回CssStyleDeclaration加边框样式。?运算符配合Result(), JsValue让错误传播非常简洁dyn_into则是 wasm-bindgen 提供的 DOM 类型向下转型 API用于把基类实例转换为具体的子类类型。3. 获取 2D 绘图上下文并准备共享状态let context canvas .get_context(2d)? .unwrap() .dyn_into::web_sys::CanvasRenderingContext2d()?; let context Rc::new(context); let pressed Rc::new(Cell::new(false));get_context(2d)返回Option先unwrap再dyn_into成CanvasRenderingContext2d。随后是关键设计Rc让多个闭包安全共享同一个 context 引用RcCellbool作为鼠标是否按下的共享标志位。之所以用Cell而非RefCell是因为这里只需要get/set一个布尔值Cell的开销更小、语义更贴合之所以用Rc而非Arc是因为这些闭包都只在主线程的事件循环中运行不需要跨线程同步。4. mousedown落笔起点{ let context context.clone(); let pressed pressed.clone(); let closure Closure::dyn FnMut(_)::new(move |event: web_sys::MouseEvent| { context.begin_path(); context.move_to(event.offset_x() as f64, event.offset_y() as f64); pressed.set(true); }); canvas.add_event_listener_with_callback(mousedown, closure.as_ref().unchecked_ref())?; closure.forget(); }这是 wasm-bindgen 事件闭包的标准用法Closure::dyn FnMut(_)::new(...)把 Rust 闭包包装成可传给 JS 的函数指针其中_表示闭包参数类型由编译器推断add_event_listener_with_callback(mousedown, ...)注册监听closure.as_ref().unchecked_ref()把闭包转成 JS 可识别的函数引用closure.forget()至关重要默认情况下Closure是有生命周期的 Rust 对象作用域结束即被回收JS 侧将得到悬空指针forget()放弃 Rust 侧所有权让该闭包常驻内存、由 JS 侧长期持有直至页面卸载。这是 wasm-bindgen 官方文档guide/src/reference 中 closures 相关章节反复强调的生命周期要点。mousedown回调中begin_path()开启新路径move_to将画笔移动到鼠标当前坐标pressed.set(true)记录按下状态。offset_x/offset_y是相对事件目标画布的坐标需转为f64以匹配 Canvas API 参数。5. mousemove拖动绘制{ let context context.clone(); let pressed pressed.clone(); let closure Closure::dyn FnMut(_)::new(move |event: web_sys::MouseEvent| { if pressed.get() { context.line_to(event.offset_x() as f64, event.offset_y() as f64); context.stroke(); context.begin_path(); context.move_to(event.offset_x() as f64, event.offset_y() as f64); } }); canvas.add_event_listener_with_callback(mousemove, closure.as_ref().unchecked_ref())?; closure.forget(); }mousemove回调只在pressed.get()为 true鼠标处于按下状态时才绘制line_to连接到新坐标、stroke()画线随后立即begin_path()move_to重新开一条路径并把起点设到当前点。这个画一笔就重开路径的技巧避免了线段之间出现不需要的连接线是 Canvas 连续绘制最常见的工程手法。每个闭包内部都通过context.clone()/pressed.clone()拿到自己的共享引用move关键字把克隆的引用移入闭包。6. mouseup收笔{ let closure Closure::dyn FnMut(_)::new(move |event: web_sys::MouseEvent| { pressed.set(false); context.line_to(event.offset_x() as f64, event.offset_y() as f64); context.stroke(); }); canvas.add_event_listener_with_callback(mouseup, closure.as_ref().unchecked_ref())?; closure.forget(); }mouseup时先把pressed复位为false防止松开后 mousemove 继续画再补上最后一笔line_tostroke保证按下→移动→松开这一整条路径在松开瞬间完整闭合。最后函数返回Ok(())整个初始化过程结束。前端配套极简 JS 入口与 webpack 管线index.js一条 import 启动一切examples/paint/index.js 是整个 JS 侧的全部代码import(./pkg) .catch(console.error);动态import(./pkg)加载 wasm-pack 生成的pkg目录产物加载后 Wasm 模块实例化#[wasm_bindgen(start)]入口随即自动执行画布便出现在页面上catch(console.error)负责在加载失败时输出错误。index.html空壳模板examples/paint/index.html 只有html、head与空的body——因为画布元素完全由 Rust 代码动态创建并 append 到 bodyHTML 本身不需要任何静态内容。这也反衬出该示例一切 DOM 操作皆在 Rust的特性。webpack.config.jswasm-pack 插件与异步 Wasmexamples/paint/webpack.config.js 是构建管线的关键module.exports { entry: ./index.js, output: { path: path.resolve(__dirname, .., dist, paint), filename: index.js, }, plugins: [ new HtmlWebpackPlugin({ template: index.html }), new WasmPackPlugin({ crateDirectory: __dirname }), ], mode: development, experiments: { asyncWebAssembly: true } };entry: ./index.jsJS 入口即上面那条动态 importWasmPackPlugin({ crateDirectory: __dirname })告诉插件当前目录即 examples/paint就是 Rust crate 所在目录构建时自动执行 wasm-pack 编译并产出pkgexperiments.asyncWebAssembly: true启用 webpack 5 的异步 WebAssembly 支持让import(./pkg)能按需异步加载 Wasm输出目录统一落在仓库根dist/paint下。官方文档的补充说明仓库内 guide/src/examples/paint.md 是对本示例的官方文档化描述要点与上文分析一一对应对Cargo.toml的说明它启用了操作 DOM、事件和 2D canvas 所必需的特性对src/lib.rs的说明创建canvas元素、应用 CSS 样式、加入文档、获取 2D 渲染上下文并添加鼠标事件监听器。本文对这两点的展开特性清单与源码用途对照、闭包生命周期、绘制路径技巧即是对官方文档的源码级深化可作为阅读官方文档时的补充参考。小结与延伸方向Paint 示例虽小却浓缩了 wasm-bindgen 在浏览器图形/交互场景下的全部核心模式#[wasm_bindgen(start)]自动入口 Result(), JsValue错误传播web-sys的 DOM 创建、类型向下转型dyn_into与按需特性开启Closure::dyn FnMut(_)unchecked_ref()forget()三段式事件监听以及Rc/Cell共享可变状态的惯用法Canvas 连续绘制的重开路径技巧。如果想要继续深入仓库内还有大量可对照学习的示例理解闭包进阶用法可看 guide/src/examples/closures.md 与 examples/closures想了解不使用打包器的纯原生 ES 模块方案可参考 guide/src/examples/without-a-bundler.md若对更复杂的 DOM 交互感兴趣examples/todomvc 和 examples/webgl 提供了规模更大的实战样板。在此基础上你完全可以仿照 Paint 的骨架快速实现自定义绘图工具、白板协作应用或图形编辑器。赞分享开发工具【免费下载链接】wasm-bindgenFacilitating high-level interactions between Wasm modules and JavaScript项目地址https://gitcode.com/gh_mirrors/wa/wasm-bindgen点击查看免费下载相关推荐wasm-bindgen Julia Set 分形绘制示例用 Rust 与 web-sys 在 Canvas 上生成 Julia 集wasm bindgen Julia Set 分形绘制示例用 Rust 与 web sys 在 Canvas 上生成 Julia 集 导读 本文以 wasm开发工具大麦自动抢票工具实操手册部署、配置与成功率优化全解析大麦自动抢票工具实操手册部署、配置与成功率优化全解析 ticket purchase 是一个针对大麦网的自动抢票开源工具基于 Python 实现网页端与移动开发工具使用 wasm-bindgen 与 web-sys 在 Rust 中绘制 2D Canvas从零实现一个笑脸示例使用 wasm bindgen 与 web sys 在 Rust 中绘制 2D Canvas从零实现一个笑脸示例 本文以 wasm bindgen 仓库自带的开发工具上一篇Miniflux 2 域名管理子域名与路径路由配置下一篇摄影爱好者的福音Album AI如何帮你管理几T照片库创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进