- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
导读
本文基于 wasm-bindgen 仓库中的weather_report示例(examples/weather_report),完整讲解如何用 Rust 编写 Wasm 模块,在浏览器中调用 OpenWeatherMap 天气 API、解析 JSON 响应、动态构建 DOM 表格与交互式地图,并借助spawn_local处理异步任务。读完本文,你将掌握#[wasm_bindgen(start)]入口、#[wasm_bindgen(module = "...")]导入外部 JS 函数、web-sys 的 DOM API 编程,以及 wasm-pack + webpack 的整套构建与本地运行流程。
示例概览:一个用 Rust 驱动浏览器 UI 的天气应用
weather_report是 wasm-bindgen 官方仓库中一个"重型"前端示例,与 hello_world、add 等最小示例不同,它把 Rust 当作完整的 UI 层来使用:
- 用户输入城市名并点击 Search 按钮;
- Rust 通过
reqwest向 OpenWeatherMap API 发起 HTTP 请求; - 用
jsoncrate 解析返回的 JSON; - 用
web_sys提供的Document、Element、HtmlInputElement等类型在页面上动态创建表格,展示温度、气压、湿度、日出日落、地理坐标; - 通过导入的外部 JS 函数
initialize(lat, lon)在页面上渲染 Google 地图标记。
官方文档 guide/src/examples/weather_report.md 对该示例的定位是:向 OpenWeather API 发起 HTTP 请求、解析 JSON 并据此渲染 UI,同时演示spawn_local在异步任务中的用法。
示例由以下文件组成:
| 文件 | 职责 |
|---|---|
| src/lib.rs | Rust 业务逻辑:DOM 构建、事件绑定、异步请求与渲染 |
| Cargo.toml | 依赖声明(wasm-bindgen、web-sys、reqwest、chrono 等) |
| index.html | 页面骨架与内联样式,引用 Google Maps API |
| index.js | webpack 入口,动态import('./pkg')加载 Wasm |
| util.js | 被 Rust 通过module导入的 JS 函数(Google Maps 渲染) |
| webpack.config.js | wasm-pack 插件与开发服务器配置 |
| package.json | npm 脚本与前端构建依赖 |
环境准备与依赖清单
Cargo 依赖解读
examples/weather_report/Cargo.toml 声明了本次示例的全部 Rust 依赖:
[package] authors = ["Ayush <ayushmishra2005@gmail.com>"] edition = "2021" name = "rust-webassembly-weather-reports" publish = false version = "0.0.0" [lib] crate-type = ["cdylib"] [dependencies] chrono = "0.4.11" gloo = "0.11" json = "0.12" reqwest = "0.13" wasm-bindgen = { path = "../../" } wasm-bindgen-futures = { path = "../../crates/futures" } [dependencies.web-sys] features = ["Document", "Element", "HtmlElement", "Window"] path = "../../crates/web-sys"需要重点说明的几点:
crate-type = ["cdylib"]是 Wasm 项目的标准配置,它把 crate 编译成 C 动态库形式的.wasm产物,供 wasm-pack 生成 JS 绑定。wasm-bindgen与wasm-bindgen-futures使用仓库相对路径(path = "../../"指向仓库根,path = "../../crates/futures"指向 futures crate),说明示例直接基于仓库源码开发;wasm-bindgen-futures正是提供spawn_local的 crate,其实现已在 crates/futures/src/lib.rs 中注明"thin shim re-exporting fromjs_sys::futures",即spawn_local实际定义于 js-sys 的 futures 模块。web-sys采用按 feature 裁剪的方式,这里只开启了Document、Element、HtmlElement、Window四个 feature。web-sys 的 API 面极其庞大(crates/web-sys/src 下有上千个绑定文件),按需开启 feature 是控制编译体积的标准做法。本示例运行时还会用到HtmlInputElement的dyn_into转换,但该转换本身来自 wasm-bindgen 的JsCasttrait,无需额外 feature。
前端构建依赖
examples/weather_report/package.json 提供了两个 npm 脚本:
{ "scripts": { "build": "webpack", "serve": "webpack serve" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "catalog:", "html-webpack-plugin": "catalog:", "copy-webpack-plugin": "catalog:", "webpack": "catalog:", "webpack-cli": "catalog:", "webpack-dev-server": "catalog:" } }其中@wasm-tool/wasm-pack-plugin负责在 webpack 构建时自动调用 wasm-pack 编译 Rust crate,html-webpack-plugin以 index.html 为模板生成页面,copy-webpack-plugin把assets目录(bootstrap 样式与图片)原样复制到产物目录。
构建与本地运行
在 examples/weather_report 目录下依次执行以下命令即可构建并启动示例:
$ npm install $ npm run build $ npm start然后打开浏览器访问http://localhost:8080即可运行示例。
关于命令的细节说明:
npm install会安装上述 devDependencies(依赖版本由仓库根部的 pnpm workspace catalog 统一解析)。npm run build触发 webpack.config.js 中的WasmPackPlugin:它读取crateDirectory: __dirname,自动对当前目录的 Cargo crate 执行wasm-pack build,将 Rust 编译为.wasm并生成pkg/下的 JS 胶水代码。- 页面输出到
../dist/weather_report,入口与模板均来自./index.js和./index.html。 - 构建模式为
development,并开启了experiments: { syncWebAssembly: true },允许 webpack 同步加载 WebAssembly 模块。
⚠️ 运行前提:OpenWeatherMap 与 Google Maps 均为外部服务。请先在 src/lib.rs 的
get_response()中填入你自己的 OpenWeather API Key(&appid=<apiKey>占位符),页面也需要可访问 Google Maps JS API(见 index.html 的<script>引入)。本示例内部逻辑与 GitHub Pages 上的预编译演示均依赖这些外部服务。
程序入口:#[wasm_bindgen(start)]
与大多数示例一样,weather_report 用#[wasm_bindgen(start)]标记一个run()函数作为 Wasm 模块加载后的自动执行入口(src/lib.rs):
#[wasm_bindgen(start)] fn run() -> Result<(), JsValue> { // ... }run()返回Result<(), JsValue>,Wasm 初始化过程中的任何 DOM 操作错误(如取不到window、document、body)都会以JsValue错误形式向上传播。入口内部的核心流程为:
- 通过
web_sys::window()获取全局window,再逐级取得document与body; - 调用辅助函数
create_div、create_input_box、create_submit_box创建搜索区与详情区元素; - 用
document.create_element手工拼装一个六行的天气信息表格(Pressure、Humidity、Sunrise、Sunset、Geo coords); - 为 Search 按钮挂载
EventListener,在点击回调中发起异步请求并渲染结果; - 最后调用
on_click.forget()防止闭包被回收(见下文)。
用 web-sys 动态构建 DOM
元素工厂函数
create_div、create_input_box、create_submit_box三个辅助函数展示了 web-sys 最基础的 DOM 编程范式(src/lib.rs):
fn create_div(document: &Document, id: &str, class: &str) -> Element { let div = document.create_element("div").unwrap(); div.set_id(id); div.set_class_name(class); div }document.create_element("div")返回Result<Element, JsValue>,随后通过set_id、set_class_name、set_attribute等方法设置属性。输入框与按钮也是用同样的方式创建的,例如create_submit_box依次设置type="button"、value="Search"、name="submit"、id="submit"与 Bootstrap 样式类(src/lib.rs)。
表格的逐节点组装
run()中有一段相当"啰嗦"但结构清晰的表格构建代码,它展示了append_child的嵌套组装方式(src/lib.rs):
let table_div = document.create_element("table")?; table_div.set_class_name("ReportStyles-table table-bordered table-striped"); let tbody_div = document.create_element("tbody")?; let ftr_div = document.create_element("tr")?; let ftd_div = document.create_element("td")?; ftd_div.set_class_name(" ReportStyles-firstTd"); let img_div = document.create_element("div")?; img_div.set_id("temp"); let std_div = document.create_element("td")?; std_div.set_class_name(" ReportStyles-secondTd"); let weather_div = document.create_element("div")?; weather_div.set_id("weather"); ftr_div.append_child(&ftd_div)?; ftd_div.append_child(&img_div)?; ftr_div.append_child(&std_div)?; std_div.append_child(&weather_div)?; // ... 其余行(Pressure/Humidity/Sunrise/Sunset/Geo coords)同理这种"先建元素、设属性、再挂子节点"的写法,本质上是把 HTML 的层级结构翻译成一系列 Rust 方法调用。每个带id的叶子节点(temp、weather、pressure、humidity、sunrise、sunset、geocoords、map_canvas)都是后续异步渲染时用set_inner_html填充数据的"插槽"。
页面最终布局由 index.html 中内联的ReportStyles-*样式控制:主容器.ReportStyles-mainContainer默认display: none,只有拿到 API 响应后才由 Rust 代码通过set_attribute("style", "display: block")显示出来(src/lib.rs)。
从 Rust 导入外部 JS:#[wasm_bindgen(module = "...")]
示例中 Google 地图的渲染不是用 web-sys 实现的,而是通过 wasm-bindgen 的模块导入机制调用手写 JS:
#[wasm_bindgen(module = "/util.js")] extern "C" { fn initialize(lat: f64, lon: f64); }module = "/util.js"让编译器把 util.js 当作 ES 模块导入,并在 Wasm 侧暴露initialize(lat, lon)函数。util.js 内部使用全局google.mapsAPI 创建地图与标记:
export function initialize(lat, lon) { var myLatlng = new google.maps.LatLng(lat, lon); var myOptions = { zoom: 3, center: myLatlng, mapTypeId: google.maps.MapTypeId.ROADMAP } var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions); var marker = new google.maps.Marker({ position: myLatlng, title:"Hello World!" }); marker.setMap(map); }initialize接收 Rust 传入的经纬度,把地图中心对准目标城市并打上标记。该函数在 Rust 侧是"fire-and-forget"式调用(不返回任何值),异步请求解析完成后直接以initialize(lat, lon)触发(src/lib.rs)。
事件绑定与闭包生命周期:EventListener 与 forget
Search 按钮的事件处理采用 gloo 的EventListener:
let on_click = EventListener::new(&submit_box, "click", move |_event| { // 读取输入框值,发起异步请求,渲染结果 }); on_click.forget();闭包内部先通过document.get_element_by_id("name")找到输入框,再dyn_into::<HtmlInputElement>()把它向下转型为具体类型并读取.value()(src/lib.rs)。dyn_into是 wasm-bindgen 的JsCasttrait 提供的方法,web_sys::HtmlInputElement在这里正是通过它获得,对应 Cargo.toml 中开启的HtmlElementfeature 家族的 DOM 类型体系。
闭包生命周期是本节的关键陷阱。代码末尾的注释明确说明了原因(src/lib.rs):
// When a Closure is dropped it will invalidate the associated JS closure. // Here we want JS callback to be alive for the entire duration of the program. // So we used `forget` leak this instance of Closure. // It should be used sparingly to ensure the memory leak doesn't affect the program too much.EventListener/Closure一旦被 Rust 侧 drop,对应的 JS 回调就会失效;为了让回调在整个程序生命周期内存活,必须调用forget()主动泄漏。注释同时提醒:这种泄漏"应当谨慎使用",以控制其对内存的影响。这是 Rust↔JS 事件绑定中极易踩坑、也最需要理解的设计点。
异步 HTTP 请求与 spawn_local
请求函数
async fn get_response(location: &str) -> JsonValue { let url1 = "http://api.openweathermap.org/data/2.5/weather?q="; let url2 = "&appid=<apiKey>"; let url = [url1, location, url2].concat(); let resp = reqwest::get(&url).await.unwrap().text().await.unwrap(); json::parse(&resp).unwrap() }get_response用reqwest::get发起请求并等待文本响应,再用jsoncrate 的json::parse得到JsonValue。运行前必须把&appid=<apiKey>替换为真实的 OpenWeather API Key——官方文档 guide/src/examples/weather_report.md 对此有明确提醒:"Please add your api key inget_response()before running this application."
spawn_local:在浏览器事件循环里跑 Future
Rust 的async函数不能直接在点击回调里.await——那会阻塞事件循环。示例的正确姿势是把 Future 交给wasm_bindgen_futures::spawn_local调度:
let response = get_response(input_value); spawn_local(async move { let parsed = response.await; // ... 提取字段并渲染 DOM });spawn_local会把 Future 挂在当前 JS 上下文(这里是浏览器的事件循环)上异步推进,直到await完成。正如官方文档所述,本示例正是为了展示spawn_local在异步任务处理中的用法。它由wasm-bindgen-futurescrate 提供,而该 crate 在 crates/futures/src/lib.rs 中被描述为"thin shim"——实现已迁移到 js-sys 的 futures 模块(js_sys::futures::spawn_local),这里只是为兼容旧 API 而原样 re-export。
从 JsonValue 提取数据并渲染
响应解析完成后,代码从JsonValue中逐字段取值(src/lib.rs):
let lon = parsed["coord"]["lon"].to_owned().as_f64().unwrap(); let lat = parsed["coord"]["lat"].to_owned().as_f64().unwrap(); initialize(lat, lon); let city_name: &str = &parsed["name"].to_owned().to_string(); let country_name: &str = &parsed["sys"]["country"].to_owned().to_string(); let place = [city_name, ",", country_name].concat(); let icon = &parsed["weather"][0]["icon"].to_owned().to_string(); let src = [ "<img src='http://openweathermap.org/img/w/", icon, ".png'>", " ", ] .concat(); let temp = (parsed["main"]["temp"].to_owned().as_f64().unwrap() - 273.15) as i64;值得注意的实现细节:
- OpenWeatherMap 返回的温度单位是开尔文,示例中显式做了
- 273.15换算成摄氏度,再as i64取整。 - 天气图标直接拼接 OpenWeatherMap 的图片 URL 生成
<img>标签。 - 城市名与国别拼成
"City,COUNTRY"格式显示在标题处。
随后用先前预留的带id元素完成填充:
temp_d.set_attribute("style", "display: block")...; city.set_inner_html(&place); image.set_inner_html(&content); weather.set_inner_html(&parsed["weather"][0]["main"].to_owned().to_string()); pressure.set_inner_html(&([p, " hpa"].concat())); humidity.set_inner_html(&([h, "%"].concat())); sunrise.set_inner_html(&get_time(sun_r)); sunset.set_inner_html(&get_time(sun_s)); geo.set_inner_html(&(["[", &lon.to_string(), ",", &lat.to_string(), "]"].concat()));set_inner_html是 web-sys 提供的便捷方法,直接把 HTML 字符串写入元素内部,省去了逐节点构建的繁琐。
时间戳格式化:chrono 的用法
OpenWeatherMap 的sunrise/sunset字段是 Unix 秒级时间戳,示例用 chrono 把它格式化成HH:MM:SS(src/lib.rs):
fn get_time(millis: u64) -> String { let d = UNIX_EPOCH + Duration::from_secs(millis); // Create DateTime from SystemTime let datetime = DateTime::<Utc>::from(d); // Formats the combined date and time with the specified format string. datetime.format("%H:%M:%S").to_string() }先把UNIX_EPOCH加上秒数得到SystemTime,再转成DateTime<Utc>,最后用%H:%M:%S输出 UTC 时刻——表格中对应的列标题正是"Sunrise[UTC]"与"Sunset[UTC]"。
运行效果
上图是 assets/images/weather.png 记录的示例运行截图:页面顶部为标题栏"WEATHER REPORT - Get the mood of your city on one click",中间是城市输入搜索框;查询"Delhi,IN"后,左侧表格展示了天气状况(霾)、温度、气压、湿度、日出日落时间与地理坐标,右侧则是由 util.js 通过 Google Maps 渲染的城市地图。整个过程(搜索 → 请求 → 渲染 → 地图定位)全部由 Rust 编译出的 Wasm 模块驱动,JS 仅承担入口加载与地图渲染两处薄层。
常见问题与调试要点
- 页面无数据显示:首先检查是否已在
get_response()中填入真实 API Key(&appid=<apiKey>占位符不会返回有效数据);其次确认npm install、npm run build均成功、pkg/已生成。 - 地图不显示:确认浏览器可访问 Google Maps JS API(index.html 通过
<script>直接加载),且initialize被正确导入——对应 src/lib.rs 的module = "/util.js"声明。 - 点击按钮后回调不触发:检查
on_click.forget()是否被调用;一旦忘记forget,EventListener被 drop 后 JS 侧闭包即失效(见 src/lib.rs 的注释说明)。 - API 域名限制:OpenWeatherMap 使用
http://(非 HTTPS)的请求 URL,部分现代浏览器可能以混合内容策略拦截,需要按浏览器提示调整运行环境。 - web-sys 类型转换:读取输入框值必须
dyn_into::<HtmlInputElement>()后调用.value(),因为get_element_by_id返回的是宽泛的Element类型(src/lib.rs)。
延伸阅读
- 仓库根 README.md 与 examples/README.md 概述了 wasm-bindgen 及其示例集的定位;
- 官方文档对应章节 guide/src/examples/weather_report.md 是本文的源头文档;
- 想深入异步机制,可阅读 crates/futures/src/lib.rs(
spawn_local的 re-export 说明)与 crates/js-sys/src/lib.rs 中的 futures 模块实现; - 其他由
#[wasm_bindgen(start)]驱动、配合 webpack 的示例(如 hello_world、console_log)可作为对照,理解从最小骨架到完整 UI 的演进路径。
- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
相关推荐
web-sys 实战:用 wasm-bindgen 构建调用 OpenWeather API 的天气报告应用
web sys 实战:用 wasm bindgen 构建调用 OpenWeather API 的天气报告应用 本指南基于 wasm bindgen 仓库中的 w
开发工具使用 AWS SDK for C++ 操作 IAM:从单个 Action 到完整用户-角色实战场景
使用 AWS SDK for C++ 操作 IAM:从单个 Action 到完整用户 角色实战场景 导读 本篇文章聚焦于当前仓库 cpp/example_cod
开发工具wasm-bindgen + web-sys 操作 DOM:从零构建并运行 "Hello from Rust!" 示例
wasm bindgen + web sys 操作 DOM:从零构建并运行 "Hello from Rust!" 示例 本文以 wasm bindgen 仓库中
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考