How To Create A Users Search App For GitHub On WebAssembly

Hello! August 24, 2018, released version Go 1.11 with the experimental support of WebAssembly (Wasm). The technology is interesting and I immediately had a desire to experiment. To write “Hello World” is boring (and it, by the way, is in the documentation), especially the trend of the last summer of the article from the series “How to make users search on GitHub <insert your favorite JS-framework>”

Task: it is necessary to implement a search of users by GitHub with login and dynamic HTML creation. As previous publications show, this is elementary, but in our case, we will not use JavaScript. It should look like this:

Attention! This article does not call for throwing JS and rewriting all web applications on Go, but only shows the possibilities of working with the JS API from WASM.

Installation

First of all, it’s worth upgrading to the latest version of Go (at the time of writing 1.11)
Copy two files with HTML & JS support to your project:

cp $(go env GOROOT)/misc/wasm/wasm_exec.{html,js}

Hello World and the basic setting of the HTTP server I will skip, you can read the details on Go Wiki

Interaction with DOM

Implemented through the syscall/js package
The article will use the following functions and methods for managing JavaScript:

// type representing the value of JavaScript
js.Value
// returns a global JavaScript object, in the browser it's `window`
func Global () Value
// call the object method as a function
func (v Value) Call (m string, args ... interface {}) Value
// return the values of the object fields
func (v Value) Get (p string) Value
// sets the values of the object fields
func (v Value) Set (p string, x interface {}

All methods have analogs in JS and will become clearer in the course of the article.

Entry field

First, create a field in which the user will enter a login for the search, it will be a classic input C placeholder.
In the future, we will need to create another tag, so we will immediately write the HTML element constructor.

type Element struct {
tag string
params map[string]string
}

The structure of the HTML element contains the name of the tag (for example, input, div, etc.) and additional params parameters (for example placeholder, id, etc.)

The designer himself looks like this:

func (el *Element) createEl() js.Value {
e := js.Global().Get("document").Call("createElement", el.tag)
for attr, value := range el.params {
e.Set(attr, value)
}
return e
}

e: = js.Global (). Get (“document”). Call (“createElement”, el.tag) is an analog of var e = document.createElement (tag) in JS
e.Set (attr, value) analogue e.setAttribute (attr, value)
This method only creates items but does not add them to the page.

To add an element to a page, you need to determine the place of its insertion. In our case, this is a div with id = “box” (in the wasm_exec.html line <div id = “box”> </ div>)

type Box struct {
el js.Value
}
box := Box{
el: js.Global().Get("document").Call("getElementById", "box"),
}

In box.el, a link to the main container of our application is stored.
js.Global (). Get (“document”). Call (“getElementById”, “box”) in JS is document.getElementById (‘box’)

The method of creating the input-element itself:

func (b * Box) createInputBox () js.Value {
// Constructor
el: = Element {
tag: "input",
params: map [string] string {
"placeholder": "GitHub username",
},
}
// Create an item
input: = el.createEl ()
// Output to a page in a div with id = "box"
b.el.Call ("appendChild", input)
return input
}

This method returns a reference to the created item:

<input placeholder="GitHub username">

Output container

The results should be displayed on the page, let’s add div c id = “search_result” by analogy with the input

func (b *Box) createResultBox() js.Value {
el := Element{
tag: "div",
params: map[string]string{
"id": "search_result",
},
}
div := el.createEl()
b.el.Call("appendChild", div)
return div
}

A container is created and a reference to the item is returned:

<div id="search_result"></div>

It’s time to define the structure for our entire Web application:

type App struct {
inputBox js.Value
resultBox js.Value
}
a := App{
inputBox: box.createInputBox(),
resultBox: box.createResultBox(),
}

inputBox and resultBox — references to previously created elements <input placeholder = “GitHub username”> and <div id = “search_result”> </ div> respectively

Excellent! We added two elements to the page. Now the user can enter data into the input and look at the empty div, not bad yet, but for now, our application is not interactive. Let’s fix this.

Input event

We need to track when the user enters the login in the input and receive this data, for this we subscribe to the keyup event, to do this very simply.

func (a *App) userHandler() {
a.input.Call("addEventListener", "keyup", js.NewCallback(func(args []js.Value) {
e := args[0]
user := e.Get("target").Get("value").String()
println(user)
}))
}

e.Get(“target”).Get(“value”) — get the value of an input, analog event.target.value in JS, println (user) regular console.log (user)

Thus, we console all user actions by entering login in the input.
Now we have data with which we can generate a request to the GitHub API

Requests for the GitHub API

We will request information on registered users: get-request at https://api.github.com/users/:username
But first, we define the structure of the GitHub API response:

type Search struct {
Response Response
Result Result
}
type Response struct {
Status string
}`
`type Result struct {
Login string `json:"login"`
ID int `json:"id"`
Message string `json:"message"`
DocumentationURL string `json:"documentation_url"`
AvatarURL string `json:"avatar_url"`
Name string `json:"name"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
}

Response — contains the server response, for our application only Status Status string is needed — it will be required to display errors on the page.
Result — the body of the answer in abbreviated form, only the required fields.

The requests themselves are formed through the standard package net/http:

func (a *App) getUserCard(user string) {
resp, err := http.Get(ApiGitHub + "/users/" + user)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
var search Search
json.Unmarshal(b, &search.Result)
search.Response.Status = resp.Status
a.search <- search
}

Now that we have a method to get information about the user with the GitHub API, let’s modify the userHandler () and, in passing, expand the structure of the Web application App, adding the channel chan Search there to transfer data from the getUserCard ()

type App struct {
inputBox js.Value
resultBox js.Value
search chan Search
}
a := App{
inputBox: box.createInputBox(),
resultBox: box.createResultBox(),
search: make(chan Search),
}
func (a *App) userHandler() {
a.input.Call("addEventListener", "keyup", js.NewCallback(func(args []js.Value) {
e := args[0]
user := e.Get("target").Get("value").String()
go a.getUserCard(user)
}))
}

Templater

Perfectly! We got information about the user and we have a container for inserting. Now we need an HTML template and of course some simple template engine. In our application we will use the mustache — it’s a popular template with simple logic.
Installation: go get github.com/cbroglie/mustache

The HTML template user.mustache is located in the tmpl directory of our application and looks like this:

<div class="github-card user-card">
<div class="header User" />
<a class="avatar" href="https://github.com/{{Result.Login}}">
<img src="{{Result.AvatarURL}}&s=80" alt="{{Result.Name}}" />
</a>
<div class="content">
<h1>{{Result.Name}}</h1>
<ul class="status">
<li>
<a href="https://github.com/{{Result.Login}}?tab=repositories">
<strong>{{Result.PublicRepos}}</strong>Repos
</a>
</li>
<li>
<a href="https://gist.github.com/{{Result.Login}}">
<strong>{{Result.PublicGists}}</strong>Gists
</a>
</li>
<li>
<a href="https://github.com/{{Result.Login}}/followers">
<strong>{{Result.Followers}}</strong>Followers
</a>
</li>
</ul>
</div>
</div>

All styles are spelled out in web/style.css

The next step is to get the template as a string and throw it into our application. To do this, we again extend the structure of the App by adding the necessary fields there.

type App struct {
inputBox js.Value
resultBox js.Value
userTMPL string
errorTMPL string
search chan Search
}
a := App{
inputBox: box.createInputBox(),
resultBox: box.createResultBox(),
userTMPL: getTMPL("user.mustache"),
errorTMPL: getTMPL("error.mustache"),
search: make(chan Search),
}

userTMPL — template for displaying information about the user user.mustache. errorTMPL — error handling template error.mustache

To get a template from an application, use the usual Get-query:

func getTMPL(name string) string {
resp, err := http.Get("tmpl/" + name)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
return string(b)
}

The template is, the data is now trying to render the HTML representation:

func (a *App) listResults() {
var tmpl string
for {
search := <-a.search
switch search.Result.ID {
case 0:
// TMPL for Error page
tmpl = a.errorTMPL
default:
tmpl = a.userTMPL
}
data, _ := mustache.Render(tmpl, search)
// Output the resultBox to a page
a.resultBox.Set("innerHTML", data)
}
}

This is a goroutine that expects data from the <-a.search channel and renders HTML. Conditionally believe that if the data from the GitHub API has an ID-user search.Result.ID, then the result is correct, otherwise, return the error page.
data, _: = mustache.Render (tmpl, search) — renders the finished HTML and a.resultBox.Set (“innerHTML”, data) prints HTML to the page

Debounce

Working! But there is one problem — if you look at the console we will see that for each keystroke a request is sent to the GitHub API, in this situation we will quickly rest against the limits.

The solution is Debounce. This is a function that defers the call to another function for a specified time. That is, when the user presses the button, we must postpone the request to the GitHub API for X milliseconds, and if another event is triggered by pressing a button, the request is delayed by another X milliseconds.

Debounce in Go is implemented using channels. The working version was taken from the article debounce function for golang.

func debounce(interval time.Duration, input chan string, cb func(arg string)) {
var item string
timer := time.NewTimer(interval)
for {
select {
case item = <-input:
timer.Reset(interval)
case <-timer.C:
if item != "" {
cb(item)
}
}
}
}

Let’s rewrite the (a * App) userHandler () method with Debounce:

func (a *App) userHandler() {
spammyChan := make(chan string, 10)
go debounce(1000*time.Millisecond, spammyChan, func(arg string) {
// Get Data with github api
go a.getUserCard(arg)
})
a.inputBox.Call("addEventListener", "keyup", js.NewCallback(func(args []js.Value) {
e := args[0]
user := e.Get("target").Get("value").String()
spammyChan <- user
println(user)
}))
}

The keyup event always fires, but requests are sent only after 1000ms after the last event.

Polishing

And finally, we will improve our UX a little by adding the “Loading …” load indicator and cleaning the container in case of empty input:

func (a *App) loadingResults() {
a.resultBox.Set("innerHTML", "<b>Loading...</b>")
}
func (a *App) clearResults() {
a.resultBox.Set("innerHTML", "")
}

The final version of the method (a * App) userHandler () looks like this:

func (a *App) userHandler() {
spammyChan := make(chan string, 10)
go debounce(1000*time.Millisecond, spammyChan, func(arg string) {
// Get Data with github api
go a.getUserCard(arg)
})
a.inputBox.Call("addEventListener", "keyup", js.NewCallback(func(args []js.Value) {
// Placeholder "Loading..."
a.loadingResults()
e := args[0]
// Get the value of an element
user := e.Get("target").Get("value").String()
// Clear the results block
if user == "" {
a.clearResults()
}
spammyChan <- user
println(user)
}))
}

Done! Now we have a full search for users on GitHub without a single line on JS. In my opinion, it’s cool.

Conclusion

Write a Web application working with DOM on wasm possible, but is it worth it to do — the question. Firstly, it is not yet clear how to test the code; secondly, in some browsers it does not work stable (for example, in Chromium, it crashed wrong in FF with this better), thirdly, all work with DOM is done through the JS API, that should affect the performance (though measurements were not made, so everything is subjective)

By the way, most examples are working with graphics in canvas and excitement of heavy computing, most likely wasm was designed specifically for these tasks. Although … time will tell.

Build and run

We clone the repository:

cd work_dir
git clone https://github.com/maxchagin/gowasm-example ./gowasm-example
cd gowasm-example

Assembly:

GOARCH=wasm GOOS=js go build -o web/test.wasm main.go

Starting the server:

go run server.go

Checking:

http://localhost:8080/web/wasm_exec.html

Demo:

http://wasm.lovefrontend.ru/web/wasm_exec.html