美文网首页
rust&wasm&yew前端开发教程

rust&wasm&yew前端开发教程

作者: sealyun | 来源:发表于2021-08-19 14:25 被阅读0次

    最近在考虑给sealer 写个云产品,我们叫sealer cloud, 让用户在线就可以完成k8s集群的自定义,分享,运行。

    作为一个先进的系统,必须有高大上的前端技术才能配得上!为了把肌肉秀到极限,决定使用 rust+wasm实现。

    这里和传统后端语言在后端渲染html返回给前端完全不一样,是真正的把rust代码编译成wasm运行在浏览器中

    从此和js说拜拜,前后端都用rust写

    不得不佩服rust的牛逼,从内核操作系统一直写到前端,性能还这么牛逼。

    yew框架

    yew 就是一个rust的前端框架。通过一系列工具链把rust代码编译成wasm运行在浏览器中。

    创建一个app

    cargo new yew-app
    

    在Cargo.toml中配置如下信息:

    [package]
    name = "yew-app"
    version = "0.1.0"
    edition = "2018"
    
    [dependencies]
    # you can check the latest version here: https://crates.io/crates/yew
    yew = "0.18"
    

    在src/main.rs中写代码:

    use yew::prelude::*;
    
    enum Msg {
        AddOne,
    }
    
    struct Model {
        // `ComponentLink` is like a reference to a component.
        // It can be used to send messages to the component
        link: ComponentLink<Self>,
        value: i64,
    }
    
    impl Component for Model {
        type Message = Msg;
        type Properties = ();
    
        fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
            Self {
                link,
                value: 0,
            }
        }
    
        fn update(&mut self, msg: Self::Message) -> ShouldRender {
            match msg {
                Msg::AddOne => {
                    self.value += 1;
                    // the value has changed so we need to
                    // re-render for it to appear on the page
                    true
                }
            }
        }
    
        fn change(&mut self, _props: Self::Properties) -> ShouldRender {
            // Should only return "true" if new properties are different to
            // previously received properties.
            // This component has no properties so we will always return "false".
            false
        }
    
        fn view(&self) -> Html {
            html! {
                <div>
                    <button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button>
                    <p>{ self.value }</p>
                </div>
            }
        }
    }
    
    fn main() {
        yew::start_app::<Model>();
    }
    

    这里要注意的地方是callback函数会触发update, 那update到底应该去做什么由消息决定。
    Msg就是个消息的枚举,根据不同的消息做不同的事。

    再写个index.html:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>sealer cloud</title>
            <p>安装k8s就选sealer</p>
      </head>
    </html>
    

    运行app

    trunk是一个非常方便的wasm打包工具

    cargo install trunk wasm-bindgen-cli
    rustup target add wasm32-unknown-unknown
    trunk serve
    

    CSS

    这个问题非常重要,我们肯定不希望我们写的UI丑陋,我这里集成的是 bulma

    非常简单,只需要在index.html中加入css:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Sealer Cloud</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
      </head>
      <body>
    
      </body>
    </html>
    

    然后我们的html宏里面就可以直接使用了:

        fn view(&self) -> Html {
            html! {
                <div>
                <nav class="navbar is-primary">
                <div class="navbar-brand navbar-item">
                    { "Sealer Cloud" }
                </div>
                </nav>
                <section class="section">
                    <div class="container">
                    <h1 class="title">
                        { "[sealer](https://github.com/alibaba/sealer) is greate!" }
                    </h1>
                    <a class="button is-dark">
                      { "Button" }
                    </a>
                    <p class="subtitle">
                        { "安装k8s请用sealer, 打包集群请用sealer, sealer实现分布式软件Build&Share&Run!" } 
                    </p>
                    </div>
                </section>
                </div>
            }
        }
    

    代码结构

    sealer源码 里面直接有具体的代码供参考。

    当然有兴趣的同学可以参与到项目开发中来。

    .
    ├── components
    │   ├── footer.rs
    │   ├── header.rs # UI的header
    │   ├── image_info.rs
    │   ├── image_list.rs # 主体内容,镜像列表
    │   └── mod.rs
    ├── main.rs # 主函数
    ├── routes
    │   ├── login.rs
    │   └── mod.rs
    ├── services
    │   ├── mod.rs
    │   └── requests.rs
    └── types
    

    模块导入

    使用函数让你的html更清晰

    impl Component for Header {
    ...
        fn view(&self) -> Html {
            html! {
                <nav class="navbar is-primary block" role="navigation" aria-label="main navigation">
                    { self.logo_name() }
                    { self.search() }
                    { self.login() }
                </nav>
            }
        }
    }
    

    我们一定要避免把很多html都写在一个代码块中,yew里面就可以通过函数的方式把它们进行切分。

    impl Header {
       fn logo_name(&self) -> Html {
           html! {
               <div class="navbar-brand">
                <div class="navbar-item">
                    <i class="far fa-cloud fa-2x fa-pull-left"></i>
                    <strong> { "Sealer Cloud" }</strong>
                </div>
               </div>
           }
       }
    ...
    }
    

    这样看起来就很清晰,view函数里调用下面的一个个Html模块。

    在main中调用header模块

    我们在header中已经实现了一个Header的Component,首先在mod.rs中把模块暴露出去:

    pub mod header;
    pub mod image_list;
    

    在main.rs中导入crate:

    use crate::components::{header::Header, image_list::Images};
    

    在main的主UI中导入header UI

    通过 <Header />这样的方式即可

    fn view(&self) -> Html {
        html! {
            <div>
              <Header /> 
              <Images />
            </div>
        }
    }
    

    镜像列表List循环处理

    先定义一个列表数组:

    pub struct Image {
        name: String,
        body: String, 
    }
    
    pub struct Images{
        // props: Props,
        images: Vec<Image>
    }
    

    在create函数中做一些初始化的事情:

        fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
            Images{
                images: vec![
                    Image {
                      name: String::from("kubernetes:v1.19.9"),
                      body: String::from("sealer base image, kuberntes alpine, without calico")
                    },
                    ...]
    

    在UI中进行循环迭代:

       fn image_list(&self) -> Html {
           html! {
              <div class="columns is-multiline">
                {
                    for self.images.iter().map(|image|{
                        self.image_info(image)
                    })
                }
              </div>
           }
       }
    

    这里给map传入的是一个匿名函数,改函数返回单个镜像的详情。

    单个镜像信息如下渲染:

       fn image_info(&self,image: &Image) -> Html {
           html! {
            <div class="column is-6">
              <div class="card">
                <header class="card-header">
                  <p class="card-header-title">
                    { image.name.to_string() }
                  </p>
                  <button class="card-header-icon" aria-label="more options">
                  <span class="icon">
                    <i class="fal fa-expand" aria-hidden="true"></i>
                  </span>
                </button>
                </header>
                  <div class="card-content">
                  <div class="content">
                    { image.body.to_string() }
                     <br />
                    <time datetime="2016-1-1">{ "11:09 PM - 1 Jan 2016" }</time>
                  </div>
                  </div>
               </div>
            </div>
           }
       }
    

    这样一个镜像列表就做好了,是不是非常简单。

    定义路由

    use yew_router::prelude::*;
    
    #[derive(Switch,Clone)]
    pub enum AppRoute {
        #[to = "/images/{name}"]
        ImageDetail(String),
        #[to = "/images"]
        Images
    }
    
    pub type Anchor = RouterAnchor<AppRoute>
    

    我们这里有两个页面,一个images列表对应的URL是/images,

    另外一个image详情页面,对应的URL是/image/{name},

    我们把image名称作为跳转的参数。

    这里的Images和ImageDetail是我们之前定义的Model,不了解的翻我之前文章。

    在主页面中进行匹配

    整个body中根据URL的不同展示不同的Model UI.

    fn view(&self) -> Html {
        html! {
            <div>
              <Header />
              <Router<AppRoute> render = Router::render(Self::switch) />
            </div>
        }
    ...
    
    

    switch函数决定挑战的逻辑:

    fn switch(route: AppRoute) -> Html {
         match route {
             AppRoute::Images => html! { <Images /> },
             AppRoute::ImageDetail(name)=> html! { <ImageDetail imageName=name /> }
         }
     }
    

    非常简单优雅,不同的路由 match到不同的Model

    参数传递

    AppRoute::ImageDetail(name)=> html! { <ImageDetail imageName=name /> }
    

    可以看到这一条路由里尝试把参数传递给ImageDetail页面。

    ImageDetail结构体需要去接收这个参数:

    
    pub struct ImageDetail{
       props: Props,
    }
    
    #[derive(Properties, Clone)]
    pub struct Props {
        pub imageName: String, // 这个名字与imageName=name对应
    }
    

    初始化的时候给它赋值:

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

    然后我们就可以去使用它了:

    fn view(&self) -> Html {
        html! {
            <div>
            { "this is image info" }
            { self.props.imageName.to_string() }
            </div>
        }
    }
    

    跳转链接

    imageList页面是如何跳转到ImageDetail页面的?

    <Anchor route=AppRoute::ImageDetail(image.name.to_string())>
      <p>
        { image.name.to_string() }
      </p>
    </Anchor>
    

    这样image name就传递到子页面了,非常简单方便优雅。

    详细的代码大家可以在如下资料中找到~

    相关资料:

    sealer cloud前端源码

    定义数据结构

    可以看到registry返回的数据:

    curl http://localhost:5000/v2/_catalog
    {"repositories":["centos","golang"]}
    

    所以对应返回的数据我们定义个数据结构:

    #[derive(Deserialize, Debug, Clone)]
    pub struct RegistryCatalog {
        pub repositories: Vec<String>,
    }
    

    这个结构体用来接收后台返回的数据,我们还需要Model的结构体:

    pub struct Images {
        // props: Props,
        pub repos: Option<Vec<String>>,
        pub error: Option<String>,
        pub link: ComponentLink<Self>,
        pub task: Option<FetchTask>
    }
    

    消息枚举,通过不同的消息在页面更新时判断应该做什么样的动作:

    #[derive(Debug)]
    pub enum Msg {
        GetRegistryCatelog(Result<RegistryCatalog, anyhow::Error>),
    }
    

    页面首次初始化

    首次初始化的时候向后台请求数据:

    fn rendered(&mut self, first_render: bool) {
        if first_render {
            ConsoleService::info("view app");
            let request = Request::get("http://localhost:8001/v2/_catalog")
                .body(Nothing)
                .expect("could not build request.");
            let callback = self.link.callback(
                |response: Response<Json<Result<RegistryCatalog, anyhow::Error>>>| {
                    let Json(data) = response.into_body();
                    Msg::GetRegistryCatelog(data)
                },
            );
            let task = FetchService::fetch(request, callback).expect("failed to start request");
            self.task = Some(task);
        }
    }
    
    • first_render用于判断是不是第一次渲染,不写在这里面可能会导致页面渲染死循环。
    • ConsoleService::info 可以帮助我们往控制台打印调试信息
    • 定义一个request和一个请求成功后的回调函数callback
    • callback内部接收到数据后返回对应的消息类型即可触发update函数(后面介绍)
    • fetch函数触发http请求调用,传入request与callback
    • 重中之重,一定要把task存到self.task里面,不然task立马被回收会触发一个The user aborted a request的错误

    页面更新接口

    在first render的时候我们向后台请求了数据,callback函数会触发update的调用。
    把消息带过去交给update函数处理。

    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        use Msg::*;
        match msg {
            GetRegistryCatelog(response) => match response {
                Ok(repos) => {
                    ConsoleService::info(&format!("info {:?}", repos)); 
                    self.repos = Some(repos.repositories);
                }
                Err(error) => {
                    ConsoleService::info(&format!("info {:?}", error.to_string())); 
                },
            },
        }
        true
    }
    

    msg可以有多种消息类型,此处就写一个

    #[derive(Debug)]
    pub enum Msg {
        GetRegistryCatelog(Result<RegistryCatalog, anyhow::Error>),
    }
    

    response就是Result<RegistryCatalog, anyhow::Error>类型,Ok时把它放到结构体中保存就好。

    最后渲染到页面上:

    fn view(&self) -> Html {
        html! {
            <div class="container column is-10">
            { self.image_list() }
            </div>
        }
    }
    
    fn image_list(&self) -> Html {
        match &self.repos {
            Some(images) => {
                html! {
                 <div class="columns is-multiline">
                   {
                       for images.iter().map(|image|{
                           self.image_info(image)
                       })
                   }
                 </div>
                }
            }
            None => {
                html! {
                  <p> {"image not found"} </p>
                }
            }
        }
    }
    

    如此就完成了整个数据请求的绑定渲染。

    实际运行

    因为请求docker registry会有跨域问题,所以整个nginx代理:

    docker run -p 5000:5000 -d --name registry registry:2.7.1
    

    nginx.conf:

    user  nginx;
    worker_processes  1;
    
    error_log  /var/log/nginx/error.log warn;
    pid        /var/run/nginx.pid;
    
    
    events {
        worker_connections  1024;
    }
    
    
    http {
        include       /etc/nginx/mime.types;
        default_type  application/octet-stream;
    
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for"';
    
        access_log  /var/log/nginx/access.log  main;
    
        sendfile        on;
        #tcp_nopush     on;
    
        keepalive_timeout  65;
    
        server {
            listen       8000;  #监听8000端口,可以改成其他端口
            server_name  localhost; # 当前服务的域名
    
            location / {
              if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE' always;
                add_header 'Access-Control-Allow-Headers' '*' always;
                add_header 'Access-Control-Max-Age' 1728000 always;
                add_header 'Content-Length' 0;
                add_header 'Content-Type' 'text/plain; charset=utf-8';
                return 204;
              }
    
              if ($request_method ~* '(GET|POST|DELETE|PUT)') {
                add_header 'Access-Control-Allow-Origin' '*' always;
              }
              proxy_pass http://172.17.0.3:5000; # the registry IP or domain name
              proxy_http_version 1.1;
            }
        }
    
        #gzip  on;
    
        include /etc/nginx/conf.d/*.conf;
    }
    
    docker run -d --name registry-proxy -p 8001:8000 \
      -v /Users/fanghaitao/nginx/nginx.conf:/etc/nginx/nginx.conf nginx:1.19.0
    

    测试registry api是否能通:

    curl http://localhost:8001/v2/_catalog
    {"repositories":["centos","golang"]}
    

    然后代码里面访问nginx代理的这个地址即可
    kubernetes一键安装

    sealer集群整体打包!

    相关文章

      网友评论

          本文标题:rust&wasm&yew前端开发教程

          本文链接:https://www.haomeiwen.com/subject/bstabltx.html