32
loading...
This website collects cookies to deliver better user experience
Chat example works! The most of this blog post will cover it - I hope it will be interesting even for people who hear about the MoonZoon for the first time.
The second chapter is dedicated to the MoonZoon Cloud - research & reasons.
I've updated my sponsors page and added "Sponsor" button to MZ repositories. More info in the MZ Cloud chapter.
frontend
, backend
and shared
.chat/shared/src/lib.rs
:use moonlight::serde_lite::{self, Deserialize, Serialize};
// ------ UpMsg ------
#[derive(Serialize, Deserialize, Debug)]
pub enum UpMsg {
SendMessage(Message),
}
// ------ DownMsg ------
#[derive(Serialize, Deserialize, Debug)]
pub enum DownMsg {
MessageReceived(Message),
}
// ------ Message ------
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Message {
pub username: String,
pub text: String,
}
shared
crate is to provide items needed for frontend-backend communication.UpMsg
is sent from the frontend to backend in a Fetch request.DownMsg
is sent from the backend to frontend in a Server-Send Event stream. serde_lite
instead of serde
to reduce the Wasm file size of the frontend app.moonlight
is a MoonZoon crate and it works as a "bridge" between the frontend (Zoon) and backend (Moon).chat/backend/src/main.rs
:use moon::*;
use shared::{DownMsg, UpMsg};
async fn frontend() -> Frontend {
Frontend::new().title("Chat example").append_to_head(
"
<style>
html {
background-color: black;
}
</style>",
)
}
async fn up_msg_handler(req: UpMsgRequest<UpMsg>) {
println!("{:?}", req);
let UpMsgRequest { up_msg, cor_id, .. } = req;
let UpMsg::SendMessage(message) = up_msg;
sessions::broadcast_down_msg(&DownMsg::MessageReceived(message), cor_id).await;
}
#[moon::main]
async fn main() -> std::io::Result<()> {
start(frontend, up_msg_handler, |_| {}).await
}
The function frontend
is invoked on the the web browser request (if the path doesn't start with _api
). The response is HTML that starts the Zoon (the frontend part).
The function up_msg_handler
handles message requests from the Zoon. Zoon sends in the UpMsgRequest
:
UpMsg
.CorId
(aka correlation id) generated for each request.SessionId
generated in the Zoon app before it connects to the Moon.Option<AuthToken>
containing String
defined in your Zoon app.sessions
are virtual actors managed by the Moon. Each SessionActor
represents a live connection between Zoon and Moon apps.DownMsg
to all connected Zoon apps by calling sessions::broadcast_down_msg
(demonstrated in the code snippet above).session
(e.g. to simulate a standard request-response mechanism):let UpMsgRequest { up_msg, cor_id, session_id, .. } = req;
let UpMsg::SendMessage(message) = up_msg;
sessions::by_session_id()
.get(session_id)
.unwrap()
.send_down_msg(&DownMsg::MessageReceived(message), cor_id).await;
by_session_id()
returns an actor index. Then we try to find the actor and calls its method send_down_msg
.All actor methods are asynchronous because the requested actor may live in another server or it doesn't live at all - then the Moon app has to start it and load its state into the main memory before it can process your call. And all those operations and the business logic processing take some time, so asynchronicity allows you to spend the time in better ways than just waiting.
Index API will change a bit during the future development to support server clusters (e.g. get
will be probably async
).
use moon::*;
use moon::actix_web::{get, Responder};
async fn frontend() -> Frontend {
Frontend::new().title("Actix example")
}
async fn up_msg_handler(_: UpMsgRequest<()>) {}
#[get("hello")]
async fn hello() -> impl Responder {
"Hello!"
}
#[moon::main]
async fn main() -> std::io::Result<()> {
start(frontend, up_msg_handler, |cfg|{
cfg.service(hello);
}).await
}
cfg
in the snippet is actix_web::web::ServiceConfig
chat/frontend/src/lib.rs
:use shared::{DownMsg, Message, UpMsg};
use zoon::{eprintln, *};
// ------ ------
// Statics
// ------ ------
// ...
// ------ ------
// Commands
// ------ ------
// ...
// ------ ------
// View
// ------ ------
// ...
// ------ ------
// Start
// ------ ------
// ...
#[static_ref]
fn username() -> &'static Mutable<String> {
Mutable::new("John".to_owned())
}
#[static_ref]
fn messages() -> &'static MutableVec<Message> {
MutableVec::new()
}
#[static_ref]
fn new_message_text() -> &'static Mutable<String> {
Mutable::new(String::new())
}
#[static_ref]
fn connection() -> &'static Connection<UpMsg, DownMsg> {
Connection::new(|DownMsg::MessageReceived(message), _| {
messages().lock_mut().push_cloned(message);
})
// .auth_token_getter(|| AuthToken::new("my_auth_token"))
}
Data stored in functions marked by the attribute #[static_ref]
are lazily initialized on the first call.
Read the excellent tutorial for Mutable
and signals in the futures_signals
crate.
A correlation id is automatically generated and sent to the Moon with each request.
A session id is automatically generated when the Connection
is created. Then it's sent with each UpMsg
.
fn set_username(name: String) {
username().set(name);
}
fn set_new_message_text(text: String) {
new_message_text().set(text);
}
fn send_message() {
Task::start(async {
connection()
.send_up_msg(UpMsg::SendMessage(Message {
username: username().get_cloned(),
text: new_message_text().take(),
}))
.await
.unwrap_or_else(|error| eprintln!("Failed to send message: {:?}", error))
});
}
Task::start
spawn the given Future
. (Note: Multithreading in Zoon apps isn't supported yet.)View
section:fn root() -> impl Element {
Column::new()
.s(Padding::new().all(30))
.s(Spacing::new(20))
.item(received_messages())
.item(new_message_panel())
.item(username_panel())
}
// ------ received_messages ------
// ...
// ------ new_message_panel ------
// ...
// ------ username_panel ------
// ...
When the root
function is invoked (note: it's invoked only once), all elements are immediately created and rendered to the browser DOM. (It means, for instance, methods Column::new()
or .item(..)
writes to DOM.)
s
is the abbreviation for style. All elements that implement the ability Styleable
have the method .s(...)
. The method accepts all items that implement the trait Style
.
Column
is a div
with CSS properties display: flex
and flex-direction: column
.
All built-in elements (Column
, Row
, etc.) have the HTML class similar to the element name, e.g. column
or row
.
// ------ received_messages ------
fn received_messages() -> impl Element {
Column::new().items_signal_vec(messages().signal_vec_cloned().map(received_message))
}
fn received_message(message: Message) -> impl Element {
Column::new()
.s(Padding::new().all(10))
.s(Spacing::new(6))
.item(
El::new()
.s(Font::new().bold().color(NamedColor::Gray10).size(17))
.child(message.username),
)
.item(
El::new()
.s(Font::new().color(NamedColor::Gray8).size(17))
.child(message.text),
)
}
Column::new().items_signal_vec(messages()..
means the Column
's items are synchronized with messages. I.e. when you add a new item to messages
, the new item is rendered in the Column
.
Spacing::new(6)
sets the CSS property gap
to 6px
.
NamedColor
is a very small and temporary enum of colors. It will be replaced probably with color pallets or a compile-time color generator later. In the meantime, you can create your own enums or other items that implement the trait Color
. See also the Color section in the MZ docs.
It's surprisingly difficult to set the font size correctly. See Size and Font Size sections in the MZ docs for more info. Font::size
just sets font-size
in px
until I resolve it properly.
El
is a simple div
. It must have one child (otherwise you get a compilation error).
// ------ new_message_panel ------
fn new_message_panel() -> impl Element {
Row::new().item(new_message_input()).item(send_button())
}
fn new_message_input() -> impl Element {
TextInput::new()
.s(Padding::new().x(10))
.s(Font::new().size(17))
.focus()
.on_change(set_new_message_text)
.label_hidden("New message text")
.placeholder(Placeholder::new("Message"))
.on_key_down(|event| event.if_key(Key::Enter, send_message))
.text_signal(new_message_text().signal_cloned())
}
fn send_button() -> impl Element {
let (hovered, hovered_signal) = Mutable::new_and_signal(false);
Button::new()
.s(Padding::new().all(10))
.s(Background::new()
.color_signal(hovered_signal.map_bool(|| NamedColor::Green5, || NamedColor::Green2)))
.s(Font::new().color(NamedColor::Gray10).size(20))
.on_hovered_change(move |is_hovered| hovered.set(is_hovered))
.on_press(send_message)
.label("Send")
}
Row
is a div
with the CSS property display: flex
.
TextInput
is an HTML input
. It has abilities:
Styleable
KeyboardEventAware
Focusable
Hoverable
You have to call either .label_hidden(..)
or .id(..)
to improve accessibility:
label_hidden
sets aria-label
to the given value.id
, then it's expected you create a Label
for the input. It's demonstrated in the following snippet.
// ------ username_panel ------
fn username_panel() -> impl Element {
let id = "username_input";
Row::new()
.item(username_input_label(id))
.item(username_input(id))
}
fn username_input_label(id: &str) -> impl Element {
Label::new()
.s(Font::new().color(NamedColor::Gray10))
.s(Padding::new().all(10))
.for_input(id)
.label("Username:")
}
fn username_input(id: &str) -> impl Element {
TextInput::new()
.s(Padding::new().x(10))
.id(id)
.on_change(set_username)
.placeholder(Placeholder::new("Joe"))
.text_signal(username().signal_cloned())
}
Label
is an HTML label
.#[wasm_bindgen(start)]
pub fn start() {
start_app("app", root);
connection();
}
start
is invoked automatically from the Javascript code.start_app
appends the element returned from the root
function to the element with the id app
.
None
instead of "app"
to mount directly to body
but it's not recommended.SessionActor
) and to improve its API during the process. I'm not sure yet if the right solution would be to allow to add a custom state directly to SessionActor
s or developers should create new custom actors like UserActor
and create/remove them when "associated" SessionActor
is created/removed. So I decided to choose the simplest approach for now (i.e. send username
in the Message
, basically keep the state in the Zoon app) and eventually revisit the chat example when actors API is stable enough."Up/DownMsg
communication works and for other cases you can use Actix directly. The last big missing Moon part is a virtual actor system. However distributable virtual actors are pretty useless without the platform where we can run them and without a storage where we can save their state. Also we would like to deploy simple apps without actors as soon as possible. We need the MoonZoon Cloud. mzoon
(MoonZoon CLI tool) commands.https://[your_app_name].mzoon.app
It's the missing piece to complete my journey to an ideal web development experience.
I need it for my MoonZoon apps.
I hope it will be the main source of income for the MoonZoon development. So if you want to speed up the MZ development, make it sustainable and get early access to the Cloud (once it's ready for it) and other benefits - please visit my Sponsors page.