Rust Web 全栈开发(十二):构建 WebAssembly 应用

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 id
        let td = document.create_element("td")?;
        td.set_text_content(Some(format!("{}", c.id).as_str()));
        tr.append_child(&td)?;
        // course name
        let td = document.create_element("td")?;
        td.set_text_content(Some(c.name.as_str()));
        tr.append_child(&td)?;
        // course time
        let 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 description
        let 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 button
        let 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!");

                        // reload
                        web_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 leak
        click_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/:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值