Rust Web 全栈开发(十二):构建 WebAssembly 应用
Rust Web 全栈开发(十二):构建 WebAssembly 应用
- Rust Web 全栈开发(十二):构建 WebAssembly 应用
- 项目配置
- 构建 wasm-client
- 构建项目
- 生成网页
- 构建网页
- 添加并下载依赖
- 测试
Rust Web 全栈开发(十二):构建 WebAssembly 应用
参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF
继续之前的 Actix 项目。
我们已经实现了一个 Web App,在网页端查看并操作数据库中教师的数据。现在我们想创建一个 WebAssembly App,查看并操作数据库中课程的数据。
项目配置
打开 webservice 的 Cargo.toml,添加配置:
actix-cors = "0.6.0-beta.10"
因为客户端和服务器在不同的端口,需要跨域。
修改 webservice/src/bin/teacher_service.rs:
use actix_cors::Cors;...#[actix_rt::main]
async fn main() -> io::Result<()> {// 检测并读取 .env 文件中的内容,若不存在也会跳过异常dotenv().ok();let db_url = env::var("DATABASE_URL").expect("DATABASE_URL 没有在 .env 文件里设置");// 创建数据库连接池let db_pool = MySqlPoolOptions::new().connect(&db_url).await.unwrap();let shared_data = web::Data::new(AppState {health_check_response: "I'm OK.".to_string(),visit_count: Mutex::new(0),// courses: Mutex::new(vec![]),db: db_pool,});let app = move || {let cors = Cors::default().allowed_origin("http://localhost:8082/").allowed_origin_fn(|origin, _req_head| {origin.as_bytes().starts_with(b"http://localhost")}).allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]).allowed_headers(vec![http::header::AUTHORIZATION,http::header::ACCEPT,http::header::CONTENT_TYPE,]).allowed_header(http::header::CONTENT_TYPE).max_age(3600);App::new().app_data(shared_data.clone()).app_data(web::JsonConfig::default().error_handler(|_err, _req| {MyError::InvalidInput("Please provide valid json input".to_string()).into()})).configure(general_routes).configure(course_routes).wrap(cors).configure(teacher_routes)};HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}
构建 wasm-client
回到 Actix 项目的最顶层,在终端中用下面的命令克隆项目模板:
cargo generate --git https://github.com/rustwasm/wasm-pack-template
wasm 模块名称为:wasm-client。
Actix 项目的 members 会自动添加:
在 wasm-client/src 目录下新建文件,编写代码。
wasm-client/src/models/mod.rs:
pub mod course;
wasm-client/src/models/course.rs:
use super::super::errors::MyError;
use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};#[derive(Debug, Serialize, Deserialize)]
pub struct Course {pub teacher_id: i32,pub id: i32,pub name: String,pub time: NaiveDateTime,pub description: Option<String>,pub format: Option<String>,pub structure: Option<String>,pub duration: Option<String>,pub price: Option<i32>,pub language: Option<String>,pub level: Option<String>,
}pub async fn get_courses_by_teacher(teacher_id: i32) -> Result<Vec<Course>, MyError> {let mut opts = RequestInit::new();opts.method("GET");opts.mode(RequestMode::Cors);let url = &format!("http://localhost:3000/courses/{}", teacher_id);let request = Request::new_with_str_and_init(&url, &opts)?;request.headers().set("Accept", "application/json")?;let window = web_sys::window().ok_or("no windows exists".to_string())?;let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;assert!(resp_value.is_instance_of::<Response>());let resp: Response = resp_value.dyn_into().unwrap();let json = JsFuture::from(resp.json()?).await?;let courses: Vec<Course> = json.into_serde().unwrap();Ok(courses)
}pub async fn delete_course(teacher_id: i32, course_id: i32) -> () {let mut opts = RequestInit::new();opts.method("DELETE");opts.mode(RequestMode::Cors);let url = format!("http://localhost:3000/courses/{}/{}", teacher_id, course_id);let request = Request::new_with_str_and_init(&url, &opts).unwrap();request.headers().set("Accept", "application/json").unwrap();let window = web_sys::window().unwrap();let resp_value = JsFuture::from(window.fetch_with_request(&request)).await.unwrap();assert!(resp_value.is_instance_of::<Response>());let resp: Response = resp_value.dyn_into().unwrap();let json = JsFuture::from(resp.json().unwrap()).await.unwrap();let _courses: Course = json.into_serde().unwrap();
}use js_sys::Promise;
use wasm_bindgen::prelude::*;#[wasm_bindgen]
pub async fn add_course(name: String, description: String) -> Result<Promise, JsValue> {let mut opts = RequestInit::new();opts.method("POST");opts.mode(RequestMode::Cors);let str_json = format!(r#"{{"teacher_id":1,"name": "{}","description": "{}"}}"#,name, description);opts.body(Some(&JsValue::from_str(str_json.as_str())));let url = "http://localhost:3000/courses/";let request = Request::new_with_str_and_init(&url, &opts)?;request.headers().set("Content-Type", "application/json")?;request.headers().set("Accept", "application/json")?;let window = web_sys::window().ok_or("no windows exists".to_string())?;let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;assert!(resp_value.is_instance_of::<Response>());let resp: Response = resp_value.dyn_into().unwrap();Ok(resp.json()?)
}
wasm-client/src/lib.rs:
mod utils;use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{HtmlButtonElement};// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;// "C" is the ABI the wasm target uses
#[wasm_bindgen]
extern "C" {fn alert(s: &str);fn confirm(s: &str) -> bool;#[wasm_bindgen(js_namespace = console)]fn log(s: &str);
}#[wasm_bindgen]
pub fn greet() {alert("Hello, wasm-client!");
}pub mod errors;
pub mod models;use models::course::Course;
use crate::models::course::delete_course;#[wasm_bindgen(start)]
pub async fn main() -> Result<(), JsValue> {let window = web_sys::window().expect("no global window exists");let document = window.document().expect("should have a document exists");let left_body = document.get_element_by_id("left-tbody").expect("left div not exists");let courses: Vec<Course> = models::course::get_courses_by_teacher(1).await.unwrap();for c in courses.iter() {let tr = document.create_element("tr")?;tr.set_attribute("id", format!("tr-{}", c.id).as_str())?;// course idlet td = document.create_element("td")?;td.set_text_content(Some(format!("{}", c.id).as_str()));tr.append_child(&td)?;// course namelet td = document.create_element("td")?;td.set_text_content(Some(c.name.as_str()));tr.append_child(&td)?;// course timelet td = document.create_element("td")?;td.set_text_content(Some(c.time.format("%Y-%m-%d").to_string().as_str()));tr.append_child(&td)?;// course descriptionlet td = document.create_element("td")?;if let Some(desc) = &c.description.clone() {td.set_text_content(Some(desc.as_str()));}tr.append_child(&td)?;// append buttonlet td = document.create_element("td")?;let btn: HtmlButtonElement = document.create_element("button").unwrap().dyn_into::<HtmlButtonElement>().unwrap();let cid = c.id;let click_closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {let r = confirm(format!("Are you sure to delete course {}?", cid).as_str());match r {true => {spawn_local(delete_course(1, cid));alert("deleted!");// reloadweb_sys::window().unwrap().location().reload().unwrap();}_ => {}}}) as Box<dyn Fn(_)>);// convert to `Function` and pass to `addEventListener`btn.add_event_listener_with_callback("click", click_closure.as_ref().unchecked_ref())?;// prevent memory leakclick_closure.forget();btn.set_attribute("class", "btn btn-danger btn-sm")?;btn.set_text_content(Some("Delete"));td.append_child(&btn)?;tr.append_child(&td)?;left_body.append_child(&tr)?;}Ok(())
}
wasm-client/src/errors.rs:
use serde::Serialize;#[derive(Debug, Serialize)]
pub enum MyError {SomeError(String),
}impl From<String> for MyError {fn from(error: String) -> Self {MyError::SomeError(error)}
}impl From<wasm_bindgen::JsValue> for MyError {fn from(js_value: wasm_bindgen::JsValue) -> Self {MyError::SomeError(js_value.as_string().unwrap())}
}
构建项目
cd 到 wasm-pack-template 目录下,执行命令:
wasm-pack build
构建成功:
当构建完成后,我们可以在 pkg 目录中找到它的工件:
生成网页
在 wasm-client 目录下运行这个命令:
npm init wasm-app www
等待构建成功。
构建网页
修改 wasm-client/www/index.html:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Hello wasm-pack!</title><!--引入bootstrap 5--><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<nav class="navbar navbar-dark bg-primary"><div class="container-fluid"><a class="navbar-brand" href="#">Wasm-client</a></div>
</nav>
<div class="m-3" style="height: 600px"><div class="row"><div class="col"><div class="card border-info mb-3"><div class="card-header">Course</div><div class="card-body"><button type="button" class="btn btn-primary">Add</button></div><table class="table table-hover table-bordered table-sm"><thead><tr><th scope="col">ID</th><th scope="col">Name</th><th scope="col">Time</th><th scope="col">Description</th><th scope="col">Option</th></tr></thead><tbody id="left-tbody"></tbody></table></div></div><div class="col"><div class="card border-info mb-3"><div class="card-header">Add Course</div><div class="card-body"><form class="row g-3 needs-validation" id="form"><div class="mb-3"><label for="name" class="form-label">Course Name</label><input type="text" class="form-control" id="name" required placeholder="Please fill in Course name"><div class="invalid-feedback">Please fill in course name</div></div><div class="mb-3"><label for="description" class="form-label">Description</label><textarea class="form-control" id="description" rows="3" placeholder="Please fill in description"></textarea></div><div class="col-12"><button type="submit" class="btn btn-primary">Submit</button></div></form></div></div></div></div>
</div>
<script src="./bootstrap.js"></script>
</body>
</html>
修改 wasm-client/www/index.js:
import * as wasm from "wasm-client";const myForm = document.getElementById("form");myForm.addEventListener("submit", (e) => {e.preventDefault();const name = document.getElementById("name").value;const desc = document.querySelector("#description").value;wasm.add_course(name, desc).then((json) => {alert('添加成功!')window.location.reload();});
});
添加并下载依赖
向 wasm-client/www/package.json 中添加依赖:
"dependencies": {"wasm-client": "file:../pkg"},
通过在 wasm-client/www 子目录下运行 npm install,确保本地开发服务器及其依赖已经安装:
测试
先 cd 到 webservice,执行 cargo run 命令,把服务器运行起来
新建一个终端,在新终端中 cd 到 wasm-client/www 目录中,运行以下命令:
npm run start
构建成功:
将 Web 浏览器导航到 localhost:8082/: