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, 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!