news 2026/10/6 2:06:27

用 wasm-bindgen + web-sys 构建天气查询 Web 应用:从 OpenWeatherMap API 到 DOM 渲染的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 wasm-bindgen + web-sys 构建天气查询 Web 应用:从 OpenWeatherMap API 到 DOM 渲染的完整实践
  • 开发工具

【免费下载链接】wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载

导读

本文基于 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.rsRust 业务逻辑:DOM 构建、事件绑定、异步请求与渲染
Cargo.toml依赖声明(wasm-bindgen、web-sys、reqwest、chrono 等)
index.html页面骨架与内联样式,引用 Google Maps API
index.jswebpack 入口,动态import('./pkg')加载 Wasm
util.js被 Rust 通过module导入的 JS 函数(Google Maps 渲染)
webpack.config.jswasm-pack 插件与开发服务器配置
package.jsonnpm 脚本与前端构建依赖

环境准备与依赖清单

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错误形式向上传播。入口内部的核心流程为:

  1. 通过web_sys::window()获取全局window,再逐级取得document与body;
  2. 调用辅助函数create_div、create_input_box、create_submit_box创建搜索区与详情区元素;
  3. 用document.create_element手工拼装一个六行的天气信息表格(Pressure、Humidity、Sunrise、Sunset、Geo coords);
  4. 为 Search 按钮挂载EventListener,在点击回调中发起异步请求并渲染结果;
  5. 最后调用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 仅承担入口加载与地图渲染两处薄层。

常见问题与调试要点

  1. 页面无数据显示:首先检查是否已在get_response()中填入真实 API Key(&appid=<apiKey>占位符不会返回有效数据);其次确认npm install、npm run build均成功、pkg/已生成。
  2. 地图不显示:确认浏览器可访问 Google Maps JS API(index.html 通过<script>直接加载),且initialize被正确导入——对应 src/lib.rs 的module = "/util.js"声明。
  3. 点击按钮后回调不触发:检查on_click.forget()是否被调用;一旦忘记forget,EventListener被 drop 后 JS 侧闭包即失效(见 src/lib.rs 的注释说明)。
  4. API 域名限制:OpenWeatherMap 使用http://(非 HTTPS)的请求 URL,部分现代浏览器可能以混合内容策略拦截,需要按浏览器提示调整运行环境。
  5. 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

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载
上一篇:在 Laravel 中安装 daisyUI 5:Tailwind CSS 组件库的完整接入指南
下一篇:Apache Gluten函数支持详解:如何扩展原生执行引擎的功能

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 1:54:34

Java基础面试:默认值、引用与boolean大小,哪些说法容易背错?

原笔记整理了语言特点、面向对象、八种基本类型、命名和 instanceof。大方向可以保留&#xff0c;但几个答案把不同层次混在一起&#xff1a;字段的默认值套到局部变量、字节码的表示套到内存大小、语言风格套到性能高低。 这次按 Java 17 规范核对&#xff0c;并用完整程序和…

作者头像 李华