前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >震惊-rust也能把前端写出花?

震惊-rust也能把前端写出花?

作者头像
sealyun
发布2021-09-08 15:13:13
1.4K0
发布2021-09-08 15:13:13
举报
文章被收录于专栏:sealyunsealyun

在追求技术先进性上面sealer作为一个非凡的项目果断也需要标新立异,走在时代最前沿。

在实现sealer cloud前端时,我们果断的选择了rust+wasm+yew框架,着实秀了一把。

| 为什么选rust+wasm

首先rust优秀到你难以想象,社区人说它曲线陡,其实对于c++都能撸的人来说这压根就不叫事。

你用rust获得了c/c++的性能却再也不用忍受内存安全问题,写c动不动core dump的人看到这篇文章趁早叛变。

对比go你不用再忍受gc的低性能,你会发现在高性能场景如果要在go上做优化的话几乎又回到结构体池减少gc的话题上,不如直接rust,也不用再忍受if err != nil。

目前没有哪个语言像rust那样对wasm支持那样好,那样成熟,比如区块链领域的substrate框架就完全是wasm的runtime了,这样整个链都可以动态升级,不会像以太坊那样动不动硬分叉。

wasm的好处就更牛掰了,哪儿都能跑是最重要的特点,浏览器可以跑,wasi可以在操作系统上跑,最重要的是编译产物比二进制少太多,几M到几KB的差别,所以做函数计算的兄弟们还没朝这个方向发展就自己反思去吧。

所以这一套组合拳下来你几乎可以从内核操作系统一直写到前端!

那么问题来了,写一个页面貌似都没有上述场景的需求,那我们为什么还选了rust +wasm?没错 为了装B!

| 从yew框架开始

yew 就是一个rust的前端框架。通过一系列工具链把rust代码编译成wasm运行在浏览器中。

| 创建一个app

代码语言:javascript
复制
cargo new yew-app

在Cargo.toml中配置如下信息:

代码语言:javascript
复制
[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中写代码:

代码语言:javascript
复制
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:

代码语言:javascript
复制
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>sealer cloud</title>
        <p>安装k8s就选sealer</p>
  </head>
</html>

| 运行app

trunk是一个非常方便的wasm打包工具

代码语言:javascript
复制
cargo install trunk wasm-bindgen-cli
rustup target add wasm32-unknown-unknown
trunk serve

| CSS

这个问题非常重要,我们肯定不希望我们写的UI丑陋,我这里集成的是 bulma

非常简单,只需要在index.html中加入css:

代码语言:javascript
复制
<!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宏里面就可以直接使用了:

代码语言:javascript
复制
    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源码 里面直接有具体的代码供参考。

代码语言:javascript
复制
.
├── 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更清晰

代码语言:javascript
复制
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里面就可以通过函数的方式把它们进行切分。

代码语言:javascript
复制
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中把模块暴露出去:

代码语言:javascript
复制
pub mod header;
pub mod image_list;

在main.rs中导入crate:

代码语言:javascript
复制
use crate::components::{header::Header, image_list::Images};

在main的主UI中导入header UI

通过

这样的方式即可

代码语言:javascript
复制
fn view(&self) -> Html {
    html! {
        <div>
          <Header /> 
          <Images />
        </div>
    }
}

| 镜像列表List循环处理

先定义一个列表数组:

代码语言:javascript
复制
pub struct Image {
    name: String,
    body: String, 
}

pub struct Images{
    // props: Props,
    images: Vec<Image>
}

在create函数中做一些初始化的事情:

代码语言:javascript
复制
    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中进行循环迭代:

代码语言:javascript
复制
   fn image_list(&self) -> Html {
       html! {
          <div class="columns is-multiline">
            {
                for self.images.iter().map(|image|{
                    self.image_info(image)
                })
            }
          </div>
       }
   }

这里给map传入的是一个匿名函数,改函数返回单个镜像的详情。

单个镜像信息如下渲染:

代码语言:javascript
复制
   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>
       }
   }

这样一个镜像列表就做好了,是不是非常简单。

| 定义路由

代码语言:javascript
复制
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.

代码语言:javascript
复制
fn view(&self) -> Html {
    html! {
        <div>
          <Header />
          <Router<AppRoute> render = Router::render(Self::switch) />
        </div>
    }
...

switch函数决定挑战的逻辑:

代码语言:javascript
复制
fn switch(route: AppRoute) -> Html {
     match route {
         AppRoute::Images => html! { <Images /> },
         AppRoute::ImageDetail(name)=> html! { <ImageDetail imageName=name /> }
     }
 }

非常简单优雅,不同的路由 match到不同的Model

| 参数传递

代码语言:javascript
复制
AppRoute::ImageDetail(name)=> html! { <ImageDetail imageName=name /> }

可以看到这一条路由里尝试把参数传递给ImageDetail页面。

ImageDetail结构体需要去接收这个参数:

代码语言:javascript
复制

pub struct ImageDetail{
   props: Props,
}

#[derive(Properties, Clone)]
pub struct Props {
    pub imageName: String, // 这个名字与imageName=name对应
}

初始化的时候给它赋值:

代码语言:javascript
复制
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
    ImageDetail{
        props,
    }
}

然后我们就可以去使用它了:

代码语言:javascript
复制
fn view(&self) -> Html {
    html! {
        <div>
        { "this is image info" }
        { self.props.imageName.to_string() }
        </div>
    }
}

| 跳转链接

imageList页面是如何跳转到ImageDetail页面的?

代码语言:javascript
复制
<Anchor route=AppRoute::ImageDetail(image.name.to_string())>
  <p>
    { image.name.to_string() }
  </p>
</Anchor>

这样image name就传递到子页面了,非常简单方便优雅。

| 与服务端数据交互

sealer cloud会与docker registry交互,未来你甚至可以把sealer cloud当作docker hub使用。

| 定义数据结构

可以看到registry返回的数据:

代码语言:javascript
复制
curl http://localhost:5000/v2/_catalog
{"repositories":["centos","golang"]}

所以对应返回的数据我们定义个数据结构:

代码语言:javascript
复制
#[derive(Deserialize, Debug, Clone)]
pub struct RegistryCatalog {
    pub repositories: Vec<String>,
}

这个结构体用来接收后台返回的数据,我们还需要Model的结构体:

代码语言:javascript
复制
pub struct Images {
    // props: Props,
    pub repos: Option<Vec<String>>,
    pub error: Option<String>,
    pub link: ComponentLink<Self>,
    pub task: Option<FetchTask>
}

消息枚举,通过不同的消息在页面更新时判断应该做什么样的动作:

代码语言:javascript
复制
#[derive(Debug)]
pub enum Msg {
    GetRegistryCatelog(Result<RegistryCatalog, anyhow::Error>),
}

| 页面首次初始化

首次初始化的时候向后台请求数据:

代码语言:javascript
复制
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函数处理。

代码语言:javascript
复制
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可以有多种消息类型,此处就写一个

代码语言:javascript
复制
#[derive(Debug)]
pub enum Msg {
    GetRegistryCatelog(Result<RegistryCatalog, anyhow::Error>),
}

response就是Result类型,Ok时把它放到结构体中保存就好。

最后渲染到页面上:

代码语言:javascript
复制
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代理:

代码语言:javascript
复制
docker run -p 5000:5000 -d --name registry registry:2.7.1

nginx.conf:

代码语言:javascript
复制
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;
}
代码语言:javascript
复制
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是否能通:

代码语言:javascript
复制
curl http://localhost:8001/v2/_catalog
{"repositories":["centos","golang"]}

然后代码里面访问nginx代理的这个地址即可

最终效果:

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

相关资料:

sealer cloud前端源码

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 sealyun 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • | 创建一个app
  • | 运行app
  • | CSS
  • | 代码结构
  • | 模块导入
    • 使用函数让你的html更清晰
      • | 在main中调用header模块
        • | 镜像列表List循环处理
        • | 在主页面中进行匹配
        • | 参数传递
        • | 跳转链接
        • | 页面首次初始化
        • | 页面更新接口
        • | 实际运行
        相关产品与服务
        容器镜像服务
        容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档