rust&wasm&yew前端开发教程

最近在考虑给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, value: i64, }impl Component for Model { type Message = Msg; type Properties = (); fn create(_props: Self::Properties, link: ComponentLink) -> 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! {{ self.value }
} } }fn main() { yew::start_app::(); }

这里要注意的地方是callback函数会触发update, 那update到底应该去做什么由消息决定。
Msg就是个消息的枚举,根据不同的消息做不同的事。
再写个index.html:
sealer cloud - 锐客网安装k8s就选sealer

运行app trunk是一个非常方便的wasm打包工具
cargo install trunk wasm-bindgen-cli rustup target add wasm32-unknown-unknown trunk serve

CSS 【rust&wasm&yew前端开发教程】这个问题非常重要,我们肯定不希望我们写的UI丑陋,我这里集成的是 bulma
非常简单,只需要在index.html中加入css:
Sealer Cloud - 锐客网

然后我们的html宏里面就可以直接使用了:
fn view(&self) -> Html { html! {{ "[sealer](https://github.com/alibaba/sealer) is greate!" }{ "Button" }{ "安装k8s请用sealer, 打包集群请用sealer, sealer实现分布式软件Build&Share&Run!" }
} }

代码结构 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! { } } }

我们一定要避免把很多html都写在一个代码块中,yew里面就可以通过函数的方式把它们进行切分。
impl Header { fn logo_name(&self) -> Html { html! { { "Sealer Cloud" }} } ... }

这样看起来就很清晰,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
通过 这样的方式即可
fn view(&self) -> Html { html! {} }

镜像列表List循环处理 先定义一个列表数组:
pub struct Image { name: String, body: String, }pub struct Images{ // props: Props, images: Vec }

在create函数中做一些初始化的事情:
fn create(props: Self::Properties, _: ComponentLink) -> 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! {{ for self.images.iter().map(|image|{ self.image_info(image) }) }} }

这里给map传入的是一个匿名函数,改函数返回单个镜像的详情。
单个镜像信息如下渲染:
fn image_info(&self,image: &Image) -> Html { html! {{ image.name.to_string() }
{ image.body.to_string() }
} }

这样一个镜像列表就做好了,是不是非常简单。
定义路由
use yew_router::prelude::*; #[derive(Switch,Clone)] pub enum AppRoute { #[to = "/images/{name}"] ImageDetail(String), #[to = "/images"] Images }pub type Anchor = RouterAnchor

我们这里有两个页面,一个images列表对应的URL是/images,
另外一个image详情页面,对应的URL是/image/{name},
我们把image名称作为跳转的参数。
这里的Images和ImageDetail是我们之前定义的Model,不了解的翻我之前文章。
在主页面中进行匹配 整个body中根据URL的不同展示不同的Model UI.
fn view(&self) -> Html { html! {} ...

switch函数决定挑战的逻辑:
fn switch(route: AppRoute) -> Html { match route { AppRoute::Images => html! { }, AppRoute::ImageDetail(name)=> html! { } } }

非常简单优雅,不同的路由 match到不同的Model
参数传递
AppRoute::ImageDetail(name)=> html! { }

可以看到这一条路由里尝试把参数传递给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 { ImageDetail{ props, } }

然后我们就可以去使用它了:
fn view(&self) -> Html { html! {{ "this is image info" } { self.props.imageName.to_string() }} }

跳转链接 imageList页面是如何跳转到ImageDetail页面的?
{ image.name.to_string() }

这样image name就传递到子页面了,非常简单方便优雅。
详细的代码大家可以在如下资料中找到~
相关资料:
sealer cloud前端源码
定义数据结构 可以看到registry返回的数据:
curl http://localhost:5000/v2/_catalog {"repositories":["centos","golang"]}

所以对应返回的数据我们定义个数据结构:
#[derive(Deserialize, Debug, Clone)] pub struct RegistryCatalog { pub repositories: Vec, }

这个结构体用来接收后台返回的数据,我们还需要Model的结构体:
pub struct Images { // props: Props, pub repos: Option, pub error: Option, pub link: ComponentLink, pub task: Option }

消息枚举,通过不同的消息在页面更新时判断应该做什么样的动作:
#[derive(Debug)] pub enum Msg { GetRegistryCatelog(Result), }

页面首次初始化 首次初始化的时候向后台请求数据:
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>>| { 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), }

response就是Result类型,Ok时把它放到结构体中保存就好。
最后渲染到页面上:
fn view(&self) -> Html { html! {{ self.image_list() }} }fn image_list(&self) -> Html { match &self.repos { Some(images) => { html! {{ for images.iter().map(|image|{ self.image_info(image) }) }} } None => { html! { {"image not found"}
} } } }

如此就完成了整个数据请求的绑定渲染。
实际运行 因为请求docker registry会有跨域问题,所以整个nginx代理:
docker run -p 5000:5000 -d --name registry registry:2.7.1

nginx.conf:
usernginx; worker_processes1; error_log/var/log/nginx/error.log warn; pid/var/run/nginx.pid; events { worker_connections1024; }http { include/etc/nginx/mime.types; default_typeapplication/octet-stream; log_formatmain'$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.logmain; sendfileon; #tcp_nopushon; keepalive_timeout65; server { listen8000; #监听8000端口,可以改成其他端口 server_namelocalhost; # 当前服务的域名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; } }#gzipon; 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集群整体打包!

    推荐阅读