5 个版本

0.1.6 2023 年 2 月 20 日
0.1.5 2023 年 2 月 14 日
0.1.4 2023 年 2 月 3 日
0.1.3 2022 年 10 月 29 日
0.1.2 2022 年 10 月 29 日

#1954 in 网络编程

每月 24 次下载

MIT 许可证

58KB
1K SLoC

Comet

Documentation Status GitHub license GitHub release PRs Welcome

响应式同构 Rust 网络框架。

索引

  1. 简介
  2. 功能
  3. 入门指南
  4. 快速浏览
  5. 待办事项列表

简介

开发中,这还是一个早期的简单原型。不要期待任何功能正常工作,预期会有很多功能经常崩溃。

Comet 是一个使用 Rust + Wasm 构建的网络框架 <3。它从 MeteorJS、Seed-rs、Yew 等框架中汲取灵感。

本 crate 旨在成为一个包含所有功能的同构响应式框架。

  - You keep saying 'Isomorphic', but why ?

在这个背景下,同构意味着你只为客户端和服务器编写一个程序。
一个 crate。一个。两者都适用。是的。
这意味着我们大量依赖宏和代码生成,带来了所有的好处和坏处,但它允许实现许多功能,几乎无样板代码,并在不同方面提供一些生活质量的提升。

  - Ok, and how is it reactive then ?

它在许多方面都是响应式的,首先是通过其 component 系统,它将小块逻辑封装到一个 HTML 模板系统中,并且可以将你的结构体的方法直接绑定到 JS 事件,从而触发只渲染更改的组件。还有一个在 PostgreSQL 数据库之上的响应式层,它允许监视某些查询随时间变化,并通过 WebSocket 向所有监视这些变化的客户端发送推送通知,在需要时触发渲染。

访问 示例文件夹


功能

  • 同构客户端/服务器
  • 响应式视图
  • 虚拟 DOM
  • 客户端缓存
  • 基于 PostgreSQL 的响应式数据库
  • 每次你的结构体更改时自动生成数据库(Alpha)
  • WebSocket
  • 自动协议生成
  • 远程过程调用
  • (几乎) 无样板代码

入门指南

安装 Comet 二进制文件和依赖项

$> cargo install comet-cli

你需要安装并运行一个 PostgreSQL 实例。

如果您的系统上没有找到,Comet 将在第一次运行时使用 cargo install 安装以下 crate

  • wasm-pack
  • diesel-cli

创建一个简单的递增计数器

$> comet new my_counter && cd my_counter

Cargo.toml 中已经设置了依赖项

comet-web = "0.1.5"

这个新创建的项目包含您开始所需的所有内容。您的旅程从 src/main.rs 开始。
方便的是,这个生成的文件已经是你能想到的最简单的递增计数器了

use comet::prelude::*;

pub struct Counter {
    pub value: i32,
}

component! {
    Counter {
        button click: self.value += 1 {
            self.value 
        }
    }
}

comet::run!(Counter { value: 0 });

运行它

将数据库地址设置为环境变量

/!\ 警告:此数据库将在启动时以及每次模型更改时被完全清除
这并不理想,但是,嘿!这仍然是一个正在进行中的项目 :p

$> export DATABASE_URL="postgres://your_user:your_password@localhost/your_db"

实际上运行你的项目

$> comet run

这将下载并安装构建和运行你的crate所需的工具。

[✓] Installing wasm-pack
[✓] Installing diesel-cli
[✓] Diesel setup
[✓] Migrating database
[✓] Patching schema
[✓] Building client
[✓] Building server
[✓] Running
 -> Listening on 0.0.0.0:8080

然后转到 https://127.0.0.1:8080


快速浏览

轻松定义DOM

use comet::prelude::*;

struct MyStruct {
    my_value: String,
    my_height: u32,
}

component! {
    MyStruct {
	// Here #my_id defined the id,
	// and the dot .class1 and .class2 add some classes to the element
	// The #id must always preceed the classes, if any
	div #my_id.class1.class2 {
	    span {
		// You can access your context anywhere
		self.my_value.as_str()
	    }
	    // Define style properties
	    div style: { height: self.my_height } {
		"Another child"
	    }
	}
    }
};

使用条件渲染和循环

use comet::prelude::*;

struct MyComponent {
    show: bool,
    value: HashMap<String, i32>,
}

component! {
    MyComponent {
	div {
	    div {
		// Conditional rendering with if
		if self.show {
		    "Visible !"
		}
		button click: self.show = !self.show {
		    "Toggle"
		}
	    }
	    div {
		// Use a for-like loop.
		for (key, value) in self.value {
		    div {
			key.as_str()
			value
		    }
		}
		button click: self.value.push(42)  {
		    "Add a number"
		}
	    }
	}
    }
}

将你的变量绑定到响应事件的 input 字段

目前这仅限于 inputselect 字段
每个绑定应该是唯一的,即每个绑定使用不同的变量,否则你会遇到冲突

use comet::prelude::*;

struct MyStruct {
    value: String,
    current_id: i32,
}

component! {
    MyStruct {
	div {
	    input bind: self.value {}
            select bind: self.current_id {
                option value: 0 {
                    "-- Choose a value --"
                }
                for id in 1..9 {
                    option value: (id) {
                        id
                    }
                }
            }
	    self.value.as_str()
            self.current_id
	}
    }
}

在它们之间嵌入你的组件

use comet::prelude::*;

struct Child {
    value: String,
}

component! {
    Child {
	div {
	    self.value
	}
    }
}

struct Parent {
    // You need to wrap your components with a Shared<T> that is basically an Arc<RwLock<T>>
    // This is necessary for your states to persist and be available between each render
    child: Shared<Child>,
}

component! {
    Parent {
	div {
	    // To include a component, just include it like any other variable
	    self.child.clone()
	}
    }
}

免费提供数据库持久性

到目前为止,所有之前的示例都是仅客户端的。现在是时候引入一些持久性了。

使用 #[model] 宏派生,你可以访问为你的类型实现了许多默认数据库方法的访问

    - async Self::fetch(i32)  -> Result<T, String>;  
    - async Self::list()      -> Result<Vec<T>, String>;  
    - async self.save()       -> Result<(), String>;
    - async Self::delete(i32) -> Result<(), String>;

String 错误类型很快就将变为一个真正的错误类型。

你可以添加自己的数据库查询方法,请参阅下面的 数据库查询

use comet::prelude::*;

// You just have to add this little attribute to your type et voila !
// It will add a field `id: i32` to the struct, for database storing purpose
// Also, when adding/changing a field to this struct, the db will 
// automatically update its schema and generate new diesel bindings
#[model]
struct Todo {
    title: String,
    completed: bool,
}

impl Todo {
    pub async fn toggle(&mut self) {
        self.completed = !self.completed;

        // This will save the model in the db
        self.save().await;
    }
}

component! {
    Todo {
	div {
	    self.id
	    self.title.as_str()
	    self.completed
            button click: self.toggle().await {
               "Toggle"
	    }
	}
    }
}

// This will create a new Todo in db every time this program runs
comet::run!(Todo::default().create().await.unwrap());

远程过程调用

注意:涉及 #[rpc] 宏的结构必须在根模块(即 src/main.rs)中是可访问的

use comet::prelude::*;

// If you have other mods that use `#[rpc]`, you have to import them explicitly
// in the root (assuming this file is the root). This is a limitation that will not last, hopefully
mod other_mod;
use other_mod::OtherComponent;

#[model]
#[derive(Default)]
pub struct Counter {
    pub count: i32,
}

// This attribute indicates that all the following methods are to be treated as RPC
// These special methods are only executed server side
// The only difference with the similar method above is that the `self.count +=1` is done server side,
// and the `self` sent back to the client
#[rpc]
impl Counter {
    // The RPC methods MUST be async (at least for now)
    pub async fn remote_increment(&mut self) {
        self.count += 1;
	
        self.save().await;
    }
}

component! {
    Counter {
	button click: self.remote_increment().await {
	    self.count
	}
    }
}

comet::run!(Counter::default().create().await.unwrap());

数据库查询

定义新的数据库查询的最简单方法是使用宏 #[sql],它在内部使用 #[rpc]

你的所有模型都已经通过自动生成的diesel绑定增强,因此你可以使用熟悉的语法。未来将有一种方法可以提供原始SQL。

use comet::prelude::*;

#[model]
#[derive(Default, Debug)]
pub struct Todo {
    pub title: String,
    pub completed: bool,
}

#[sql]
impl Todo {
    // Use the watch macro to get back your data whenever the result set change in DB
    // Only valid for select statement for now
    #[watch]
    pub async fn db_get_all(limit: u16) -> Vec<Todo> {
	// The diesel schema has been generated for you
        use crate::schema::todos;

        // You don't have to actually execute the query, all the machinery
	// of creating a db connection and feeding it everywhere have been 
	// abstracted away so you can concentrate on what matters
        todos::table.select(todos::all_columns).limit(limit as i64)
    }
}

HTML视图

到目前为止,我们始终使用组件来管理我们的视图和逻辑。
每次你使用 component! 宏定义组件时,你都可以直接在宏内定义HTML片段。
在底层,我们调用 html! 宏,它在功能上要简单得多。

// You can define basic function that return an HTML
pub async fn my_view(my_arg: MyType) -> Html {
    html! {
        div {
            my_arg.my_property
        }
    }
}

// Then you can call it from a component, or another existing html view.
component! {
    SomeComponent {
        div {
            my_view(self.some_value).await
        }
    }
}

请注意,html! 宏目前不支持输入绑定(bind)或事件绑定(clickchange)。

完整的聊天示例

这是一个客户端/服务器完全反应式的聊天室

在示例文件夹中还有一个更复杂的多频道聊天

use comet::prelude::*;

#[model]
pub struct Message {
    pub sender: String,
    pub content: String,
}

#[sql]
impl Message {
    #[watch]
    pub async fn list_watch() -> Vec<Message> {
        use crate::schema::messages;
        messages::table.select(messages::all_columns)
    }
}

component! {
    Message {
        div {
            self.sender.to_owned() + ": " + &self.content
        }
    }
}

#[derive(Default)]
pub struct App {
    pub sender: String,
    pub content: String,
}

impl App {
    async fn send_message(&mut self) {
        let mut message = Message {
            id: -1,
            sender: self.sender.clone(),
            content: self.content.clone(),
        };

        self.content = "".into();

        message.save().await.unwrap();
    }
}

component! {
    App {
        div {
            Message::list_watch().await
            input bind: self.sender {}
            input bind: self.content {}
            button click: self.send_message().await {
                "Send"
            }
        }
    }
}

comet::run!(App::default());

待办事项列表

  • 函数组件

  • 允许HTML中存在迭代器

  • 有一个允许获取相应根DOM元素的 ComponentId

  • 找到一种全局组件间消息传递的方法

  • 对于非监视的rpc查询使用缓存(因为这会导致每次重绘时产生大量流量)

  • 找到一种方法来有一个全局状态

  • Postgres池和可重用连接

  • Result<T, Error> 实现 ToVirtualNode

  • 添加一个可扩展的错误系统

  • 将所有可重用的功能分别放在不同的crate中

    • Comet crate
      • 视图系统
        • HTML宏
        • 组件宏
      • 通过Websocket实现同构数据库模型
        • 生成基本模型查询的#[model]过程宏
        • 抽象的ws服务器/客户端

依赖项

~9–24MB
~360K SLoC