乐趣区

关于云计算:rustwasmyew前端开发教程

最近在思考给 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 集群整体打包!

退出移动版