最近在思考给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/yewyew = "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-clirustup target add wasm32-unknown-unknowntrunk 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集群整体打包!