Implement the client
GopherJS can be used in a couple of different ways, and there’s a couple of different bindings to many popular JavaScript frameworks. The GopherJS wiki has some great resources.
I tried a couple of different ones and ended up using the VueJS bindings because it made it easy to prototype things quickly. I hear VueJS works well for many JS evangelisers out there, but I’ve only used it with small projects. It’ll do for this blog post, but in the future I want to try working with myitcv’s React bindings and the Vecty toolkit.
One of the biggest problems with GopherJS at the moment is it does not have deadcode elimination. This can be remedied by avoiding many standard library packages. Throughout this I’ve kept minimizing generated file size a priority, to show that it’s possible to implement fully featured frontend apps with GopherJS without compromising on file size.
To make things a little saner we’ll create a couple of folders again.
Lets add compiled
and html
to the client
folder:
$ cd client && tree -L 1 -d
.
|-- compiled
|-- html
`-- protos
Next we’ll define the HTML
of our page. Create index.html
in the html
folder.
It’s not supposed to be anything fancy, we just want a skeleton for our GopherJS code.
The full HTML can be found on
my github,
but it won’t make sense without looking at the GopherJS code!
Skipping out on some boilerplate, we’ll start with the Simple button:
<div id="app" v-cloak>
<h1>gRPC through gopherjs!</h1>
<p>
<h2>Simple</h2>
<button v-if="!simple_message" @click="Simple()">Send GET</button>
<div v-if="simple_message">
Message: {{ simple_message.msg }}, Num: {{ simple_message.num }}
</div>
</p>
</div>
<script src="index.js"></script>
This creates a button with the label Send GET
, that will, when clicked, call the
function Simple
. There’s also all kinds of references to things we haven’t seen yet
so lets move on to the GopherJS code. Again, you can skip ahead and see the full code
on my github. Create client.go
in the client
folder.
package main
//go:generate gopherjs build -m client.go -o html/index.js
//go:generate go-bindata -pkg compiled -nometadata -o compiled/client.go -prefix html ./html
//go:generate bash -c "rm html/*.js*"
import (
"github.com/gopherjs/gopherjs/js"
"github.com/johanbrandhorst/gopherjs-json"
"github.com/oskca/gopherjs-vue"
"honnef.co/go/js/xhr"
"github.com/johanbrandhorst/gopherjs-grpc-websocket/client/helpers"
"github.com/johanbrandhorst/gopherjs-grpc-websocket/client/protos/server"
)
type MyMessage struct {
*js.Object
Msg string `js:"msg"`
Num uint32 `js:"num"`
}
// Model is the state keeper of the app.
type Model struct {
*js.Object
SimpleMessage *MyMessage `js:"simple_message"`
}
func (m *Model) Simple() {
req := xhr.NewRequest("GET", "/api/v1/simple")
req.SetRequestHeader("Content-Type", "application/json")
// Wrap call in goroutine to use blocking code
go func() {
// Blocks until reply received
err := req.Send(nil)
if err != nil {
panic(err)
}
if req.Status != 200 {
panic(req.ResponseText)
}
rObj, err := json.Unmarshal(req.ResponseText)
if err != nil {
panic(err)
}
msg := &MyMessage{
Object: rObj,
}
m.SimpleMessage = msg
}()
}
func main() {
m := &Model{
Object: js.Global.Get("Object").New(),
}
// These must be set after the struct has been initialised
// so that the values can be mirrored into the internal JS Object.
m.SimpleMessage = nil
// Create the VueJS viewModel using a struct pointer
vue.New("#app", m)
}
There’s quite a bit to break down here, so we’ll start from the top.
GopherJS requires packages to pretend that they’re binaries,
so we must use package main
at the top.
Then we’ve got a couple of go generate
directives that will create and pack
the JS file so that we can easily serve it from the server later on.
//go:generate gopherjs build -m client.go -o html/index.js
//go:generate go-bindata -pkg compiled -nometadata -o compiled/client.go -prefix html ./html
//go:generate bash -c "rm html/*.js*"
The first one will compile client.go
file into html/index.js
. The -m
flag creates a minified JS output file.
The second one uses the excellent go-bindata
to generate a convenience package for serving static content from a
Go webserver. It means we can distribute
a single binary which contains both the webserver logic and the static content.
The -pkg
flag is the name of the package that is generated, the -prefix
flag
removes the specified path from the path in the package. We’ll see later how to use it.
The last one just removes the generated JS file. We do this because all the data is already in the package we generated in step two and the generated JS is just a huge unmanageable text file. Who likes looking at JS anyway ;)?
Then we’ve got the MyMessage
struct. It’s manually crafted to correspond
to the one defined in the protofile.
type MyMessage struct {
*js.Object
Msg string `js:"msg"`
Num uint32 `js:"num"`
}
The js
struct tag tells GopherJS what the variable should be called in the JS world,
and by extension, in the HTML
. So when we look back at the HTML
we
defined it should now be clearer how things work.
Next up we’ve got the Model
.
// Model is the state keeper of the app.
type Model struct {
*js.Object
SimpleMessage *MyMessage `js:"simple_message"`
}
Any Go structs that we want to use with JS need to embed the GopherJS *js.Object
type.
This is a quirk of GopherJS,
we’ll see more later about what it means.
The Model
is required by the VueJS
bindings, and is how we communicate between the Go
world and the HTML
. Anything we assign to properties on the Model
will be reflected
in the HTML
and all methods defined on the Model
are accessible from the HTML
.
<p>
<h2>Simple</h2>
<button v-if="!simple_message" @click="Simple()">Send GET</button>
<div v-if="simple_message">
Message: {{ simple_message.msg }}, Num: {{ simple_message.num }}
</div>
</p>
I’m a little unhappy about keeping logic in the markup, especially when it means using
JS logic, but it’ll do for this short introduction. So we show the button if simple_message
is unpopulated, otherwise we display the contents of the simple_message
.
Next up we’ve got the definition of the Simple
function.
func (m *Model) Simple() {
req := xhr.NewRequest("GET", "/api/v1/simple")
req.SetRequestHeader("Content-Type", "application/json")
// Wrap call in goroutine to use blocking code
go func() {
// Blocks until reply received
err := req.Send(nil)
if err != nil {
panic(err)
}
if req.Status != 200 {
panic(req.ResponseText)
}
rObj, err := json.Unmarshal(req.ResponseText)
if err != nil {
panic(err)
}
msg := &MyMessage{
Object: rObj,
}
m.SimpleMessage = msg
}()
}
We’ll use the excellent xhr package for
interacting with the API the server exposes. Using the xhr
library means we can
avoid importing net/http
which adds 3MB to the generated JS file size.
Another quirk of GopherJS is that blocking code must be wrapped in a goroutine, this is apparently because JS cannot contain blocking code. You can find more information about this quirk in the GopherJS README.
As you can probably see, we’re using panic
quite liberally in this code,
compared to how it would be used in real Go code. A panic
in GopherJS
will dump the stack to the browser console, which is fine for
something simple like this, but in a real app you might want to capture the
error produced and assign it to something in the Model
to make it visible
to the user.
Next we use a simple json helper library
to create a GopherJS *js.Object
from the JSON
string the server responds with. Because
we know the JSON
is of the type MyMessage
, we can use the *js.Object
returned to intialise a new MyMessage
from the object,
and use that to update the Model
. Simple!
Lastly we’ve got the main
boilerplate:
func main() {
m := &Model{
Object: js.Global.Get("Object").New(),
}
// These must be set after the struct has been initialised
// so that the values can be mirrored into the internal JS Object.
m.SimpleMessage = nil
// Create the VueJS viewModel using a struct pointer
vue.New("#app", m)
}
Whenever we create a GopherJS struct, a new *js.Object
needs to be assigned to it.
Any properties on the GopherJS structs need to be initialized after the object
has been assigned to it. Note that we must explicitly initialize properties on
the Model
, in order for the value to be reflected in the underlying
*js.Object
. This is just another quirk of GopherJS.
We use the VueJS bindings
to create the app. The first parameter is the identifier
of the element in the HTML
which we want to populate the app with.
We’ve got just enough here for a minimal GopherJS frontend to a grpc-gateway proxied Go gRPC server. But we’re still missing the really interesting stuff! Lets implement the unary streaming function as well:
<p>
<h2>Unary</h2>
<button v-if="unary_messages.length == 0" @click="Unary()">Send GET</button>
<div v-if="unary_messages">
<div v-for="msg in unary_messages">
Message: {{ msg.msg }}, Num: {{ msg.num }}
</div>
</div>
</p>
This is very similar to the simple one, but we iterate over the messages as they come in. Next we need to add something to the model, to display the unary messages.
type Model struct {
...
UnaryMessages []*MyMessage `js:"unary_messages"`
}
We’ll need to initialize the new struct member in the main
function as well.
func main() {
...
m.UnaryMessages = []*MyMessage{}
...
}
Now we can implement the Unary
streaming function.
func getStreamMessage(msg string) *MyMessage {
rObj, err := json.Unmarshal(msg)
if err != nil {
panic(err)
}
// The actual message is wrapped in a "result" key,
// and there might be an error returned as well.
// See https://github.com/grpc-ecosystem/grpc-gateway/blob/b75dbe36289963caa453a924bd92ddf68c3f2a62/runtime/handler.go#L163
aux := &struct {
*js.Object
msg *MyMessage `js:"result"`
}{
Object: rObj,
}
// The most reliable way I've found to check whether
// an error was returned.
if rObj.Get("error").Bool() {
panic(msg)
}
return aux.msg
}
func (m *Model) Unary() {
req := xhr.NewRequest("GET", "/api/v1/unary")
req.SetRequestHeader("cache-control", "no-cache")
req.SetRequestHeader("Content-Type", "application/json")
bytesRead := 0
req.AddEventListener("readystatechange", false, func(_ *js.Object) {
switch req.ReadyState {
case xhr.Loading:
// This whole dance is because the XHR ResponseText
// will contain all the messages, and we just want to read
// anything we havent already read
resp := req.ResponseText[bytesRead:]
bytesRead += len(resp)
m.UnaryMessages = append(m.UnaryMessages, getStreamMessage(resp))
}
})
// Wrap call in goroutine to use blocking code
go func() {
// Blocks until reply received
err := req.Send(nil)
if err != nil {
panic(err)
}
if req.Status != 200 {
panic(req.ResponseText)
}
}()
}
While I’m quite happy with the overall simplicity of this, unfortunately it’s
very much JS in Go clothing. I haven’t found a better way to handle periodically
updating requests. Again, the xhr
library makes this much nicer. Unfortunately
the grotesque error check inside the handler is, as far as I can tell, unavoidable.
Hopefully most of the other stuff should be fairly self-explanatory with the comments.
That leaves us with the bidi-websocket streaming functions left to implement. I wanted this to show off the capabilities of the websocket protocol, so it’s a little more elaborate than it probably needed to be. This’ll be a bit of a code dump but I think most of this should be reasonable easy to understand, given what we’ve talked about so far.
<p>
<h2>Bidi</h2>
<div v-if="!ws_conn">
<button @click="Connect()">Connect to Websocket</button>
</div>
<form v-if="ws_conn" v-on:submit.prevent>
<input type="text" v-model="input_message"></input>
<button @click="Send()">Send Websocket message</button>
<button @click="Close()">Close Websocket</button>
</form>
<div v-if="bidi_messages">
<div v-for="msg in bidi_messages">
Message: {{ msg.msg }}, Num: {{ msg.num }}
</div>
</div>
</p>
type Model struct {
...
InputMessage string `js:"input_message"`
BidiMessages []*MyMessage `js:"bidi_messages"`
ConnOpen bool `js:"ws_conn"`
}
func main() {
...
m.BidiMessages = []*MyMessage{}
m.InputMessage = ""
m.ConnOpen = false
...
}
// GetWSBaseURL constructs the base URL for WebSocket calls
// Copied from
// https://github.com/gopherjs/websocket/blob/edfe1438a4184bea0b3f9e35fd77969061676d9c/test/test/index.go
func GetWSBaseURL() string {
document := js.Global.Get("window").Get("document")
location := document.Get("location")
wsProtocol := "ws"
if location.Get("protocol").String() == "https:" {
wsProtocol = "wss"
}
return wsProtocol + "://" + location.Get("hostname").String() + ":" + location.Get("port").String()
}
func (m *Model) Connect() {
// Wrap call in goroutine to use blocking code
go func() {
// Blocks until connection is established
var err error
WSConn, err = websocket.Dial(GetWSBaseURL() + "/api/v1/bidi")
if err != nil {
panic(err)
}
m.ConnOpen = true
}()
}
func (m *Model) Close() {
err := WSConn.Close()
if err != nil {
panic(err)
}
m.ConnOpen = false
m.InputMessage = ""
m.BidiMessages = []*MyMessage{}
}
func (m *Model) Send() {
msg := &MyMessage{
Object: js.Global.Get("Object").New(),
}
msg.Msg = m.InputMessage
s, err := json.Marshal(msg.Object)
if err != nil {
panic(err)
}
_, err = WSConn.Write([]byte(s))
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
// Wrap call in goroutine to use blocking code
go func() {
// Blocks until a WebSocket frame is received
n, err := WSConn.Read(buf)
if err != nil {
panic(err)
}
m.BidiMessages = append(m.BidiMessages, getStreamMessage(string(buf[:n])))
}()
}
In this, we have separate functions for connecting to the WebSocket,
sending and reading a message, and closing the WebSocket. We use another
excellent GopherJS package, the websocket
wrapper for working with the browsers native websockets. The Send
function
takes the text from the form input, marshals it to JSON and sends it on the websocket.
It then reads until it gets a reply. Obviously this can be designed differently
if we need different behaviour.
Now that we’ve finished the client, we need to put everything together.