Everyone is talking about Rust lately, and it seems to be delivering what other C/C++ killers didn't. Safe, high-performance system software.
But with the dawn of WebAssembly (WASM), Rust can also dive into the frontend realm. Since Rust has a minimal runtime, it's one of the few programming languages that can actually deliver sane package sizes for WASM based web frontends.
The creators of the Yew framework try to tackle that problem. Is Yew smaller than JavaScript-based frameworks? No! Is it faster? Also No! But it's easier? Well... No.
So why am I looking into this?!
Well, I guess in the hope that a Yew developer chimes in and tells me that I did it wrong! :D
Also, because I want to learn a bit more Rust and know mostly stuff about frontend development, so I can at least re-use some of my skills while getting the hang of it.
I talked so much about backend stuff on "Fullstack Frontend", I thought it was time to write some frontend content again.
The Bootstrap Starter
In this article, I will try to build the Bootstrap starter template with Yew. So no interactions, just setting the development environment up and pump some HTML into it.
Prerequisites
I assume you have Rust and Cargo installed.
Setting Up the Environment
Install wasm-pack, a WebAssembly build-tool for Rust.
$ cargo install wasm-pack
Creating the Rust Project
Use Cargo to create a new project.
$ cargo new --lib bootstrap-starter
$ cd bootstrap-starter
Configuring the Packages
Cargo automatically downloads packages you add as dependencies to the Cargo.toml
.
[package]
name = "bootstrap-starter"
version = "0.1.0"
authors = ["John Doe <dev@example.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2.67"
yew = "0.17"
We need two dependencies here. wasm-bindgen
is used to link up the WASM code with JavaScript and, well, the Yew framework itself.
Creating the HTML File
WASM apps can't stand alone in the browser; they have to be loaded via JavaScript, which in turn has to be embedded into HTML.
So let's create a static HTML file static/index.html
with this content:
<!doctype html>
<title>Bootstrap Starter</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<style type="text/css">
body { padding-top: 5rem }
.starter-template {
padding: 3rem 1.5rem;
text-align: center;
}
</style>
<script type="module">
import init from "./wasm.js";
init();
</script>
WASM-Pack will create a static/wasm.js
file that our JavaScript can load later. I also added some CSS and Bootstrap to make the example look at least a bit interesting.
Creating the Components
I used Bootstrap in this example, the starter template, be exact.
I tried to split it into two components, nothing fancy. No interaction, just minimal Yew components.
Let's replace the content of your src/lib.rs
file with the following content:
#![recursion_limit="1000"]
mod content;
mod navigation;
use content::Content;
use navigation::Navigation;
use wasm_bindgen::prelude::*;
use yew::prelude::*;
struct Model {}
impl Component for Model {
type Message = ();
type Properties = ();
fn create(_: Self::Properties, _link: ComponentLink<Self>) -> Self {
Self {}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<Navigation />
<Content />
</div>
}
}
}
#[wasm_bindgen(start)]
pub fn run_app() {
App::<Model>::new().mount_to_body();
}
This is what came up as I tried to remove as much code from the Yew examples as possible while still getting it to compile.
Rust doesn't have classes, and functional components are still in the making, so a struct with a trait implementation is as close as Yew currently gets to React class components.
If you come from a React background, you'll notice the view
method that uses an html!
macro, allowing JSX like syntax. The other methods are create
, which is the constructor of the component. update
, which handles interactions via messages as Redux would, I guess? And finally, change
which handles new props that come from the parent component. They all don't return anything interesting because I didn't implement any interactions, but the Component
trait doesn't come with default implementations for them, so I had to do it myself.
The first line tells the compiler not to blow up when the html!
macro's HTML is nested too deep.
The mod
statements import the other components, which we will create next. The use
statements make some things a bit easier to... well... use, haha. After use content::Content;
we can use Content inside the html!
macro like it wasn't defined in another file.
The use
statements for wasm_bindgen
and yew
make some other code available to run the app at the end of the file. A bit like the render
function of ReactDOM, just with the extra step of calling out of WASM into JavaScript.
For the Content
component, you have to create a new file src/content.rs
with the following code:
use yew::prelude::*;
pub struct Content {}
impl Component for Content {
type Message = ();
type Properties = ();
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Content {}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<main class="container">
<div class="starter-template">
<h1>{"Bootstrap starter template"}</h1>
<p class="lead">
{"Use this document as a way to quickly start any new project."}
<br/>
{"All you get is this text and a mostly barebones HTML document."}
</p>
</div>
</main>
}
}
}
Nothing too spectacular here. As with JSX, you need to close your tags properly. But different from JSX, you have to wrap your strings into curly braces and quotation marks.
The navigation component isn't much different. Create a new file src/navigation.rs
and fill this code in:
use yew::prelude::*;
pub struct Navigation {}
impl Component for Navigation {
type Message = ();
type Properties = ();
fn create(_props: Self::Properties, _link: ComponentLink<Self>) -> Self {
Navigation {}
}
fn update(&mut self, _msg: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="#">{"Navbar"}</a>
<button class="navbar-toggler">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#">
{"Home "}<span class="sr-only">{"(current)"}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://example.com">{"Link"}</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">{"Disabled"}</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search" />
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">{"Search"}</button>
</form>
</div>
</nav>
}
}
}
Again, nothing special here. The input
tag has to be closed properly, and the text-nodes have to be inside curly braces and quotation-marks.
Build the App
If you run the following command, you should end up with more files inside the static
directory besides the index.html
.
$ wasm-pack build --target web --out-name wasm --out-dir ./static
These files contain the actual Rust code compiled to WASM and many wasm-pack/wasm-bindgen helpers to link up the WASM with JavaScript.
I ended up with about 140KB, which is quite steep compared to JavaScript, but I didn't optimize anything here, just plain out of the box compilation.
On the other hand, it's still much smaller than what other languages that compile to WASM currently achieve, especially those with giant runtimes.
Running the Example
If you point an HTTP server to the static
directory, you will see nothing out of the ordinary. But if you look into the dev-tools' network tab, you will see what files are actually loaded and how big they are.
I used the miniserve crate that was proposed on the Yew guide. It's installed with:
$ cargo +nightly install miniserve
But it could be that you have to install the nightly toolchain before.
$ rustup toolchain install nightly
After that, you can run miniserve and look at your fresh WASM frontend:
$ miniserve ./static --index index.html
Summary
Yew is an interesting approach to front-end development. Rust's type-system is really nice and can prevent many JavaScript-related errors.
Also, the html!
macro is a nice touch and certainly better integrated into Rust than JSX is into JavaScript (because it isn't really).
I didn't build anything complex here, so I can't say anything about the rest of the framework, just that the size isn't perfect, and the benchmarks I saw didn't look very good either. But I guess this will change when WASM gets better integration with the browser in the future.