博主信息
落伍程序猿
博文
430
粉丝
0
评论
0
访问量
10738
积分:0
P豆:989

rust&wasm&yew前端开发教程

2021年10月18日 20:48:44阅读数:19博客 / 落伍程序猿

最近在考虑给[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

通过

这样的方式即可

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就传递到子页面了,非常简单方便优雅。

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

定义数据结构

可以看到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和一个请求成功后的回调函数callbackcallback内部接收到数据后返回对应的消息类型即可触发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代理的这个地址即可


版权申明:本博文版权归博主所有,转载请注明地址!如有侵权、违法,请联系admin@php.cn举报处理!

全部评论

文明上网理性发言,请遵守新闻评论服务协议

条评论