Wasm Builders 🧱

Cover image for Host and Serve a Lit SPA with WebAssembly (and Capsule)
Philippe Charrière
Philippe Charrière

Posted on • Updated on

Host and Serve a Lit SPA with WebAssembly (and Capsule)

Sept 4th, 2022 - update following release 0.1.9

One of the significant advantages of WASM outside the browser is that a wasm application consumes much fewer resources than a more classic application in Java, Node.js, Ruby, Php, ...

By the way, my pet project is to develop my website with WASM and Capsule and self-host on an RPI3A+ ... But it's another topic for another blog post.

So I decided that with each new release of Capsule (my little http server for wasm nano services), I would code a "useful" example going beyond the simple "hello world". The stated goal of this blog post is to demonstrate that "wasm outside the browser" (aka WASI) is a relevant solution for your future applications.

If you don't know Capsule, you can read the introduction blog post Capsule, my personal wasm multi-tools knife

So let's see how to "fit" a Lit web app into a wasm module (Lit is a simple library for building fast, lightweight web components.).

The Single Page Application

My Single Page Application comprises three files and uses the Lit library. I will use a convenient possibility of Go (and TinyGo): it is possible to bundle resources in a running Go program thanks to the embed package and then access the resources.

The three files of the SPA are in the resources directory

.
β”œβ”€β”€ go.mod
β”œβ”€β”€ index.go 
β”œβ”€β”€ resources
β”‚  β”œβ”€β”€ index.html 
β”‚  β”œβ”€β”€ index.js
β”‚  └── styles.js
Enter fullscreen mode Exit fullscreen mode

I want to merge the three files into a single html file. Then embed the html resource in the wasm module to be able to serve it with Capsule.

The easiest way to merge the SPA files into one is to use Parcel

So, create in a directory, another directory resources with three files. We are defining the content of the files in the next section.

Content of the three files

index.html:

<html>
  <head>
    <meta charset="utf-8">
    <title>Wasm is fantastic 😍</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <script type="module">
        import value from "./index.js"; 1️⃣
      </script>
  </head>

  <body>
    <main-app></main-app> 2️⃣
  </body>

</html>
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣: this notation allows Parcel to use and embed the JavaScript file.
  • 2️⃣: this is a Lit component

With Lit, we can define the CSS styles in JavaScript
(https://lit.dev/docs/components/styles/#sharing-styles), then it easier to share styles between various web components.

styles.js:

import {css} from 'lit-element';

export const myStyles = css`
.container { 
  min-height: 100vh; 
  display: flex; 
  justify-content: center; 
  align-items: center; 
  text-align: center; 
}
.title { 
  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 
  display: block; 
  font-weight: 300; 
  font-size: 100px; 
  color: #35495e; 
  letter-spacing: 1px; 
}
.subtitle { 
  font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; 
  font-weight: 300; 
  font-size: 42px; 
  color: #526488; 
  word-spacing: 5px; 
  padding-bottom: 15px; 
}
`;
Enter fullscreen mode Exit fullscreen mode

This SPA is very simplistic; I defined all the web components in only one file, index.js.

index.js:

import { LitElement, html, css} from 'lit-element';
import { myStyles as myStyles } from './styles.js';

class HelloWorld extends LitElement {
  static styles = [myStyles]

  render() {
    return html`
    <h1 class="title">πŸ‘‹ Hello World 🌍</h1>
    `;
  }
}

customElements.define('hello-world', HelloWorld);

class ServedByCapsule extends LitElement {
  static styles = [myStyles]

  render() {
    return html`
    <h2 class="subtitle">Served with πŸ’œ by Capsule πŸ’Š</h2>
    `;
  }
}

customElements.define('served-by-capsule', ServedByCapsule);

class LittleMessage extends LitElement {
  static styles = [myStyles]

  render() {
    return html`
    <h2 class="subtitle"> {{message}} </h2> 1️⃣
    `;
  }
}

customElements.define('little-message', LittleMessage);

class MainApp extends LitElement { 2️⃣
  static styles = [myStyles]

  render() {
    return html`
    <section class="container">
      <div>
        <hello-world></hello-world>
        <served-by-capsule></served-by-capsule>
        <little-message></little-message>
      </div>
    </section>    
    `;
  }
}

customElements.define('main-app', MainApp);
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣: this is not related to Lit. I will use it later to substitute {{message}} with the value of an environment variable.
  • 2️⃣: MainApp is the main component of the application, it will be mounted in the html page thanks to the tag <main-app></main-app>

Bundle the three files into a single one

As I said at the beginning of the blog post, the easiest way to do this is using Parcel.

In the resources directory, create a package.json file.

package.json:

{
  "scripts": {
    "start": "parcel index.html",
    "build": "parcel build index.html"
  },
  "devDependencies": {
    "parcel": "^2.7.0"
  },
  "dependencies": {
    "lit-element": "^3.2.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

And then, run this command: npm install to install both Parcel and Lit.

For developing the web application and see the changes (the web application is served and reloaded at every change), you can use the command npm start.

To bundle all the files into a single file, use the following command:

cd resources
npm run build
Enter fullscreen mode Exit fullscreen mode

The command will create a dist directory with the bundle (index.html).

.
β”œβ”€β”€ resources
β”‚  β”œβ”€β”€ dist
|  |   └── index.html 
β”‚  β”œβ”€β”€ index.html 
β”‚  β”œβ”€β”€ index.js
β”‚  └── styles.js
Enter fullscreen mode Exit fullscreen mode

It's time to create the wasm capsule module.

The Wasm Capsule Module

Write the wasm function

To generate the module, we need two files at the root of the project directory: go.mod and index.go

.
β”œβ”€β”€ go.mod
β”œβ”€β”€ index.go 
β”œβ”€β”€ resources
β”‚  β”œβ”€β”€ dist
|  |   └── index.html 
β”‚  β”œβ”€β”€ index.html 
β”‚  β”œβ”€β”€ index.js
β”‚  └── styles.js
Enter fullscreen mode Exit fullscreen mode

go mod:

module lit-demo

go 1.18
Enter fullscreen mode Exit fullscreen mode

Use go mod init lit-demo to generate it (and use go mod tidy to download all the dependencies).

index.go:

package main

import (
  _ "embed"
  "strings"
  hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
)

var (
  //go:embed resources/dist/index.html 1️⃣
  html []byte
)

func main() {
  hf.SetHandleHttp(Handle)
}

func Handle(hf.Request) (resp hf.Response, errResp error) {

  // Read the environment variable
  message, err := hf.GetEnv("MESSAGE")

  // Set the response format
  headers := map[string]string{
    "Content-Type": "text/html; charset=utf-8",
  }

  htmlPage 
    := strings.Replace( 2️⃣
      string(html), 
      "{{message}}", 
      message, 
      -1) 

  return hf.Response{Body: htmlPage, Headers: headers}, err
}
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣: explain to the TinyGo compiler where is the embedded resource and put the content of index.html in a bytes' array.
  • 2️⃣: replace {{message}} with the content of the MESSAGE environment variable.

Build and serve the SPA

First, to build the wasm module, use the below command:

tinygo build -o index.wasm  -target wasi ./index.go
Enter fullscreen mode Exit fullscreen mode

Then, you need to install the last version of Capsule:

CAPSULE_VERSION="0.1.9"
OS="linux"
ARCH="amd64"
wget https://github.com/bots-garden/capsule/releases/\
download/${CAPSULE_VERSION}/\
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz

sudo tar -zxf \
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz \
--directory /usr/local/bin

rm capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz
Enter fullscreen mode Exit fullscreen mode

And finally, run the below command to serve the SPA:

MESSAGE="I πŸ’™ Lit-Element" capsule \
-wasm=./index.wasm \
-mode=http \
-httpPort=8080
Enter fullscreen mode Exit fullscreen mode

and open http://localhost:8080 with your browser:

my amazing spa

πŸŽ‰ As you can see, it's pretty easy to embed a Single Page Application in a wasm module. You can use the same technique with Sat from Suborbital or Spin from Fermyon.All the magic parts are done thanks to Parcel and the GoLang embed package.

Next time (with the next release of Capsule) we will see how to use Redis as a service discovery with Capsule nano services.

πŸ‘‹ Have a nice day πŸ™‚

Latest comments (0)