rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Go Tips: WebAssembly and Vugu
Dec 20, 2020

I’ve been learning Rust and WebAssembly in my free time for a few months already, nothing really crazy to be honest, just reading some docs, articles and blogs, watching some videos and attending a few live seminars to understand more. Rust and WebAssembly are closely related to each other and in practice I should be learning Rust first, but the truth is that most of my time has been dedicated to explore WebAssembly more deeply, specifically learning about the progress already made in Go as well as some ways to interact with WebAssembly using different packages and whatnot.

There are a lot of good articles and videos that cover really interesting technical things about how Go and WebAssembly work:

Funny enough during GopherCon 2020 there was a presentation called Go is Not Just on Your Server, it’s in Your Browser: The Story of and Introduction to Vugu (Go+WebAssembly UI library) by Brad Peabody, that covered Vugu A modern UI library for Go+WebAssembly that really piqued my interested.

And funny enough (again!) I’ve been working, just recently, on an internal tool that happened to need some frontend code, the perfect excuse to start using Vugu for real.

I started about two weeks ago and I already released a working version of this internal tool that happens to be interacting with our backend APIs, everything works nicely to be honest, and the cool thing is that everything is written in Go.

However, because of the lack of experience using WebAssembly and Vugu I initially had some problems, let me elaborate more about how I solved those next. By the way all the code is available on Gitlab, feel fee to explore it and run it locally.

Custom template for rendering Vugu Pages

The official documentation mentions Full-HTML Mode support but I haven’t been able to figure it out, in the end my solution was to render this template through the server that is delivering all the assets:

templ := fmt.Sprintf(/* here define the HTML template to be used */)

h := simplehttp.New(wd, false)
h.PageHandler = &simplehttp.PageHandler{
	Template:         template.Must(template.New("_page_").Parse(templ)),
	TemplateDataFunc: simplehttp.DefaultTemplateDataFunc,
}

Please refer to the final example, specifically the definition of the template as well as how this is being used.

wasm main arguments via Javascript.

A tiny bit related to the problem above. If we need to pass in information to the final wasm artifact we should be using something like:

WebAssembly.instantiateStreaming(fetch("/main.wasm"), go.importObject).then((result) => {
    go.argv = ["main.wasm", "-parameter", "%s", "-parameter2", "something"];
    go.run(result.instance);
});

For example, in cases we need to indicate configuration details that happen to be only available via environment variables through the original server.

/health check using simplehttp

If you use simplehttp (via github.com/vugu/vugu/simplehttp) to build the server that delivers the assets, then you may need a way to define a health-like endpoint for allowing your autoscaling configuration to work, this is easily solved by implementing a wrapper for PageHandler, something like:

health := func(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/health" {
			// do some actual health checks
			w.WriteHeader(http.StatusOK)
			return
		}

		h.ServeHTTP(w, r)
	})
}

h := simplehttp.New(wd, false)

h.PageHandler = health(&simplehttp.PageHandler{/* actual configuration */})

Render smaller wasm files

To date, the final compiled wasm files are a bit large, to reduce their filesize you need a combination of two things:

  1. To use brotli to compress the final wasm file and,
  2. To use gzipped.FileServer (via github.com/lpar/gzipped/v2) to deliver that compressed asset with the corresponding headers if the client supports them.

Something like the following should work:

h.StaticHandler = gzipped.FileServer(gzipped.Dir(wd))

Don’t forget about http.StripPrefix in case your requested path if different than the local one.

For context, the final files look like this:

4.6M Dec 20 14:35 main.wasm
980K Dec 20 14:35 main.wasm.br

Downloading a file.

Finally, this one is interesting because it involves passing data from Go to Javascript via the standard library. In the linked example, this is triggered when clicking the “Download file!” button:

func (r *Root) HandleDownloadFile(e vugu.DOMEvent) {
	var output bytes.Buffer

	// literal copy/paste from the encoding/csv godoc

	records := [][]string{
		{"first_name", "last_name", "username"},
		{"Rob", "Pike", "rob"},
		{"Ken", "Thompson", "ken"},
		{"Robert", "Griesemer", "gri"},
	}

	w := csv.NewWriter(&output)

	for _, record := range records {
		if err := w.Write(record); err != nil {
			log.Fatalln("error writing record to csv:", err)
		}
	}

	w.Flush()

	if err := w.Error(); err != nil {
		log.Fatal(err)
	}

	// important part

	dst := js.Global().Get("Uint8Array").New(len(output.Bytes()))
	_ = js.CopyBytesToJS(dst, output.Bytes())
	js.Global().Call("downloadFile", dst, "text/csv", "usernames.csv")
}

The really important bit is the last 3 lines of the method

dst := js.Global().Get("Uint8Array").New(len(output.Bytes()))
_ = js.CopyBytesToJS(dst, output.Bytes())
js.Global().Call("downloadFile", dst, "text/csv", "usernames.csv")
  • Uint8Array is used because it represents an array of 8-bit unsigned integers, and recall byte is an alias to uint8 therefore making them equivalent.
  • downloadFile() is called to pass in the data from Go to original javascript function.

This allows passing in data from Go to Javascript which in the end triggers a download on the frontend side.

Conclusion

I’m really enjoying Vugu (and WebAssembly), I understand the well known limitations, like users require a modern browser, but somehow if you have control of who your users are then using Vugu for building web applications is a great excuse.


Back to posts