It's alive! It runs!
It... doesn't do anything useful yet. Just like an average kitten. We have to wait until they grow up - but who doesn't like to watch progress?
Welcome to the MoonZoon Dev News!
MoonZoon is a Rust full-stack framework. If you want to read about new MZ features, architecture and interesting problems & solutions - Dev News is the right place.
There are two big news. I've written my first tweet ever! And also a couple MoonZoon lines of code - a build pipeline, live-reload, certificate generator and servers (GitHub PR).
Awesome Discord friends tested it on Fedora, Ubuntu, Windows and macOS with Chrome, Firefox and Safari. Live-reload works also on my older iPhone SE. Thanks @adsick
, @UberIntuiter
and @EvenWei
!
Follow these steps to try it by yourself.
When you run in examples/counter
the command
cargo run --manifest-path "../../crates/mzoon/Cargo.toml" start
# or in the future:
mzoon start
then:
-
MZoon (aka MoonZoon CLI) loads the project's
MoonZoon.toml
. It contains only configuration for file watchers atm:[watch] frontend = [ "frontend/Cargo.toml", "frontend/src", ] backend = [ "backend/Cargo.toml", "backend/src", ]
-
MZoon checks if wasm-pack exists and panics if doesn't.
- Note: MZoon will automatically install the required
wasm-pack
version defined inMoonZoon.toml
and check the compatible Rust version in the future.
- Note: MZoon will automatically install the required
-
MZoon generates a certificate for the
localhost
domain using rcgen. The result is two files -private.pem
andpublic.pem
- saved in thebackend/private
directory.- Note: Git ignores the
private
directory content. - Warning: I recommend to set the certificate's serial number explicitly to a unique value (you can use the current unix time). Otherwise Firefox may fail to load your app with the error code SEC_ERROR_REUSED_ISSUER_AND_SERIAL.
- Note: Git ignores the
-
wasm-pack
builds the frontend part. If you used the parameter-r / --release
together with the commandstart
, then it builds in the release mode and also optimizes the output (.wasm
file) for size. -
A unique
frontend_build_id
is generated and saved to theexamples/pkg/build_id
. The build id is added as a name suffix to some files in thepkg
directory. It's a cache busting mechanism becausepkg
files are served by the backend.- Note:
pkg
is generated bywasm-pack
and its content is ignored by Git.
- Note:
-
The frontend file watcher is set according to the paths in
MoonZoon.toml
. It sends an empty POST request to Moon (https://127.0.0.1:8443/api/reload
) on a file change.- Warning: Browsers treat unregistered self-signed certificates as invalid so we must allow the acceptance of such certificates before we fire the request:
reqwest::blocking::ClientBuilder::new() .danger_accept_invalid_certs(true)
- Warning: Browsers treat unregistered self-signed certificates as invalid so we must allow the acceptance of such certificates before we fire the request:
-
cargo run
builds and starts the backend. MZoons sets the backend file watcher and saves a generatedbackend_build_id
tobackend/private/build_id
.- Note: If you like async spaghetti, you won't be disappointed by looking at the related code. Why?
- We can't easily split
cargo run
to standalone "build" and "run" parts. We ideally need something like cargo run --no-build. - We need to handle "Ctrl+C signal".
- We need to somehow find out when the backend has been started or turned off (from the MZoon's point of view).
- (Don't worry, I'll refactor it later and probably rewrite with an async runtime.)
- We can't easily split
- Note: If you like async spaghetti, you won't be disappointed by looking at the related code. Why?
The backend part consists two Warp servers.
- The HTTPS one runs on the port
8443
. It uses generated certificates. - The HTTP one runs on the port
8080
and redirects to the HTTPS one.- Question: What's the best way of HTTP -> HTTPS redirection? I don't like the current code.
We need HTTPS server because otherwise browsers can't use HTTP/2. And we need HTTP/2 to eliminate SSE limitations. Also it's better to use HTTPS on the local machine to make the dev environment similar to the production one.
Both servers binds to 0.0.0.0
(instead of 127.0.0.1
) to make servers accessible outside of your development machine. It means you can connect to your dev servers with your phone on the address like https://192.168.0.1:8443
.
I assume some people will need to use custom dev domains, sub-domains, ports, etc. Let me know when it happens.
Tip: I recommend to test server performance through an IP address and not a domain (localhost
) because DNS resolving could be slow.
I've chosen Warp because I wanted a simpler server with HTTP/2 support. Also I have a relatively good experience with hyper (Warp's HTTP implementation) from writing a proxy server for my client.
When you go to https://127.0.0.1:8443
, the frontend
route is selected by Warp in the Moon. Moon responds with a generated HTML + Javascript code.
HTML and JS for app initialization aren't very interesting so let's focus on the live-reload code (Note: I know, the code needs refactor, but it should be good enough for explanation):
<script type="text/javascript">
{reconnecting_event_source}
var uri = location.protocol + '//' + location.host + '/sse';
var sse = new ReconnectingEventSource(uri);
...
sse.addEventListener("reload", function(msg) {
sse.close();
location.reload();
});
</script>
What's {reconnecting_event_source}
? And why I see ReconnectingEventSource
instead of EventSource?
Well, welcome to the world of browsers where nothing works as expected, specs are unreadable and jQuery and polyfills are still needed.
{reconnecting_event_source}
is a placeholder for the ReconnectingEventSource code. The library description:
This is a small wrapper library around the JavaScript EventSource API to ensure it maintains a connection to the server. Normally, EventSource will reconnect on its own, however there are some cases where it may not. This library ensures a reconnect always happens.
I've already found such "edge-case" - just run the app in Firefox and restart backend. Firefox permanently closes the connection. Chrome (and I hope other browsers) try to reconnect as expected. Question: Do you know a better solution?
Let's move forward and look at this snippet:
sse.addEventListener("reload", function(msg) {
sse.close();
location.reload();
});
It means we listen for messages with the event name reload
. Moon creates them in the POST /api/reload
endpoint this way:
Event::default().event("reload").data(""))
Warning: Notice the empty string in data("")
. It doesn't work without it.
We should call sse.close()
if we don't want to see an ugly error message in some console logs when the browser kills the connection on reload.
The last part, hidden under the ...
mark in the first code snippet, is:
var backendBuildId = null;
sse.addEventListener("backend_build_id", function(msg) {
var newBackendBuildId = msg.data;
if(backendBuildId === null) {
backendBuildId = newBackendBuildId;
} else if(backendBuildId !== newBackendBuildId) {
sse.close();
location.reload();
}
});
The only purpose of this code is to reload the frontend when the backend has been changed. The backend sends the message backend_build_id
automatically when the client connects to /sse
endpoint - i.e. when the SSE connection has been opened.
And that's all for today! Thank You for reading and I hope you are looking forward to the next episode.
Martin
P.S. We are waiting for you on Discord.