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 次下载
58KB
1K SLoC
Comet
响应式同构 Rust 网络框架。
索引
简介
开发中,这还是一个早期的简单原型。不要期待任何功能正常工作,预期会有很多功能经常崩溃。
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
快速浏览
轻松定义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
字段
目前这仅限于 input
和 select
字段
每个绑定应该是唯一的,即每个绑定使用不同的变量,否则你会遇到冲突
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
)或事件绑定(click
、change
)。
完整的聊天示例
这是一个客户端/服务器完全反应式的聊天室
在示例文件夹中还有一个更复杂的多频道聊天
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服务器/客户端
- 自动生成协议的宏
- 数据库的响应式/监听部分 reactive-postgres-rs
- 视图系统
- Comet crate
依赖项
~9–24MB
~360K SLoC