22 November 2017

Deploying an SPA as a Go Binary Using Webpack and Statik

It’s no secret that I’ve been doing a lot of Go work recently and dabbling with some Vue.js here and there. While there are many guides on how to build a single page web application in Vue, React, Ember, and serve it separately via S3 or NGINX, how would one build and deploy the application via Go? I’ve found what I think is a simple way of making this work, so bear with me. For reference, the source to this can be found in the following repo. This project assumes that you have a working Go environment properly configured and Node.js + npm installed.

Creating the project structure

First things first, let’s make our project. Create the project at mkdir -p $GOPATH/github.com/iheanyi/go-vue-statik. After creating this, let’s create a cmd folder with mkdir cmd. By the way, for managing our Go dependencies, we’ll be using dep, so make sure you have that installed. Similarly, for front-end dependency management, make sure that you have yarn installed as well.

Let’s create our Vue.js project. Make sure you have vue-cli installed by running npm install -g vue-cli. Then switch into the cmd directory by running cd cmd and the Vue.js project by running vue init sampleapp. Go through the project and enable things like Vue Router, Airbnb linting, ES6 modules, and unit tests, then run cd sampleapp && yarn in order to change into the project’s directory and install its dependencies. Also, this will be explained later, but let’s go ahead and build our project by running npm run build. This will build our project via Webpack. If all is well, you should be able to see a directory called dist after running ls -la.

Getting our files into Go

All right, so we got our front-end and it’s building properly. Let’s bundle this up in our Go project, yeah? You should still be in sampleapp project. So now, let’s do the go parts, shall we? For this part, we’re going to need to install Statik by rakyll. Do so by running go get github.com/rakyll/statik. Done? Cool. Ensure that statik is installed by running which statik, if you get some output, that’s a good thing. Let’s generate the statik directory by running statik -src=./dist. If all is fine, you should have a directory called statik in your sample app folder now. Here’s how your sampleapp directory should look like by this point.

.
├── README.md
├── build
├── config
├── dist
├── index.html
├── node_modules
├── package.json
├── src
├── static
├── statik
├── test
└── yarn.lock

Coolio. Okay, so you’re probably wondering, how do we even serve this file up? Well, don’t worry, we’re about to find out.

So in your sampleapp directory that you’ve been hanging out in, let’s create a file called main.go. Here’s what the structure of this file is going to look like.

package main

import (
    "log"
    "net/http"

    _ "github.com/iheanyi/go-vue-statik/cmd/sampleapp/statik"
    "github.com/rakyll/statik/fs"
)

func main() {
    statikFS, err := fs.New()
    if err != nil {
        log.Fatal(err)
    }

    staticHandler := http.FileServer(statikFS)
    // Serves up the index.html file regardless of the path.
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        r.URL.Path = "/"
        staticHandler.ServeHTTP(w, r)
    })
    http.Handle("/static/", staticHandler)
    http.ListenAndServe(":1337", nil)
}

Before we begin analyzing, ensure that you have all the relevant dependencies installed by running dep ensure. This will import and vendor all the dependencies your project needs. Now, onto analyzing.

First line of analysis.

_ "github.com/iheanyi/go-vue-statik/cmd/sampleapp/statik"

We’re importing the generated statik package. Nothing is being exported in this package, but there is some initialization in the statik/fs package being done here, primarily a call to fs.Register() that instantiates the binary data we’ll be calling.

Next lines of business.

statikFS, err := fs.New()
if err != nil {
        log.Fatal(err)
}

We’re instantiating a new instance of an http.FileSystem here, using the zip data that was defined to the call to fs.Register on import. Lit.

Now, into the final pieces of code.

staticHandler := http.FileServer(statikFS)
// Serves up the index.html file regardless of the path.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    r.URL.Path = "/"
    staticHandler.ServeHTTP(w, r)
})
http.Handle("/static/", staticHandler)
http.ListenAndServe(":1337", nil)

Breaking down what we’re doing here. We’re creating a FileServer from our filesystem that we just defined. The http.HandleFunc function essentially is saying, “For every single URL path, serve up the file server.” This will serve up the index.html page from our dist folder regardless of what path we’re at. Why would we want to do this? Well, since we’re using vue-router, we can handle routing client-side rather than server-side. How dope is that?

http.Handle("/static/", staticHandler) ensures that all of the files that were in the dist/static folder are served up correctly as well. It’s a wildcard match as well, so as long as files are being exported to that directory via Webpack, they’ll load just fine.

Let’s try it all together now? Run go install ./... and then run sampleapp locally. Alternatively, you can also run go run main.go. Then go to http://localhost:1337 in your browser. If things went properly, you should see the starter Vue.js app that says, “Welcome to your Vue.js App”. How awesome!

Cleaning this up

There’s some ways that this could be cleaned up. For one, Vue Router by default uses the hash mode for tracking history. Let’s change it’s mode to history. Open up src/router/index.js and add mode: history to the object input to the Router’s constructor. If all goes well, it should look like this now.

import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'

Vue.use(Router)

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

Rebuild this file by running npm run build and then running statik -src=./dist. Then, either recompile and run sampleapp and run go run main.go. Try navigating to a random URL in the browser now and notice how it doesn’t redirect anymore and still properly renders the Vue.js application.

Bonus Round: Go Generators

Open up the package.json file and modify the "build" command in scripts to be "build": "node build/build.js && statik -src=./dist". This way, when we run npm run build it will build the assets and compile them into Go. That way, when your binary is built, it will always be updated with the latest assets. Next, open up the main.go file and add the following line on Line 1: //go:generate npm run build. In the end, the file should look like this:

//go:generate npm run build
package main

import (
    "log"
    "net/http"

    _ "github.com/iheanyi/go-vue-statik/cmd/sampleapp/statik"
    "github.com/rakyll/statik/fs"
)

func main() {
    statikFS, err := fs.New()
    if err != nil {
        log.Fatal(err)
    }

    staticHandler := http.FileServer(statikFS)
    // Serves up the index.html file regardless of the path.
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        r.URL.Path = "/"
        staticHandler.ServeHTTP(w, r)
    })
    http.Handle("/static/", staticHandler)
    http.ListenAndServe(":1337", nil)
}

Now whenever we’re in sampleapp, we can run go generate in order to build our application. This can come in handy when you are working in a Go ecosystem with other services. Pretty cool, huh?

Future Possibilities

While this is a pretty simple example, this can be a really good productivity boost. Since we have things compiled to a binary, it can be deployed anywhere. Deploy it to your Kubernetes cluster, Heroku, or to your dedicated VPS instance (like a DigitalOcean droplet). It’s just a binary, it’ll run anywhere. Also, running it in a Docker container probably will be really easy. What I like about this approach is that I can develop my front-end web app using modern tooling like Webpack Dev Server, but when it comes to building and deploying our application, we can just deploy it like any other Go application. Pretty cool, right?

Additionally, if you have a service like an API service, you can use this same strategy to embed your front-end application within it and deploy it with your API, eliminating the need for things like CORS in production. This approach shouldn’t only be limited to Vue.js with Webpack. It should be applicable to any front-end build system, whether it’s Webpack, Broccoli, Rollup, etc. It’s should be fine regardless of the library/framework you’re using.

I’m interested in hearing alternative approaches to deploying front-end applications in Go applications, feel free to let me know on Twitter. I’m always a message away. Thanks for reading!

Did you enjoy reading this?

Feel free to let me know on Twitter. You can also subscribe to my newsletter for more of my writing.