3个版本
0.1.6 | 2023年2月20日 |
---|---|
0.1.5 | 2023年2月14日 |
0.1.4 | 2023年2月3日 |
#3 in #comet
11KB
328 行
Comet
响应式同构Rust Web框架。
索引
简介
正在开发中,这还是一个早期原始原型。不要期待任何东西能正常工作,要期待经常出现故障。
Comet是一个使用Rust + Wasm构建的Web框架 <3。它从MeteorJS、Seed-rs、Yew等中获得灵感。
这个crate旨在成为一个包含所有功能的综合性同构响应式框架。
- You keep saying 'Isomorphic', but why ?
在这个上下文中,同构意味着你只为客户端和服务器编写一个程序。
一个crate。一个。两者都。
这意味着我们大量依赖宏和代码生成,所有这些优点和缺点都可能带来,但它允许实现许多功能,接近零样板代码,并在不同方面提供一些生活质量上的提升。
- Ok, and how is it reactive then ?
它在许多意义上都是响应式的,首先是通过其组件
系统,该系统将小块逻辑封装到HTML模板系统中,并且可以直接将你的struct的方法绑定到JS事件上,触发仅更改的组件的渲染。还有一个在PostgreSQL
数据库之上的响应式层,允许在一段时间内监视某些查询是否发生变化,并通过WebSocket向所有监视这些更改的客户端发送推送通知,在需要时触发渲染。
访问示例文件夹。
功能
- 同构客户端/服务器
- 响应式视图
- 虚拟DOM
- 客户端缓存
- 带有PostgreSQL的响应式数据库
- 每次你的struct发生变化时自动生成数据库(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.6"
这个新生成的项目包含了你开始所需的一切。你的旅程从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"
}
}
}
}
}
将变量绑定到响应事件的字段
目前仅限于和
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]
中涉及的struct必须可以从根模块(即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中使用迭代器
-
有一个ComponentId,允许获取相应的根dom元素
-
找到一种实现全局组件间消息传递的方法
-
使用缓存来处理非观察的rpc查询(因为这会导致每次重绘时的大量流量)
-
找到一种实现全局状态的方法
-
Postgres池和可重用连接
-
为Result
实现ToVirtualNode -
添加可扩展的错误系统
-
将所有可重用功能分别放在不同的crate中
- Comet crate
- 视图系统
- HTML宏
- 组件宏
- 通过WebSocket实现的同构数据库模型
- #[model]过程宏,用于生成基本模型查询
- 抽象的ws服务器/客户端
- 自动原型宏
- 数据库的响应式/监听部分 reactive-postgres-rs
- 视图系统
- Comet crate
依赖项
~0-9MB
~86K SLoC