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