l12a

白ウサギを追え

Go で作る SPA 用バックエンド

Go で作る Web アプリケーションバックエンドを作っていきます。
フロントエンドは Vue.js や React を用いた SPA にすることを想定したとき、バックエンドは以下のような要件が求められることも多いでしょう。

  • SPA 側でルーティングを制御する都合上、バックエンドはルートを問わず単一の index.html を返すように
  • 特定のディレクトリをフロントエンドアセットの配置場所として設定
  • フロントエンドが利用する API を提供できるように

これらを Go の WAF である Gin を使って実現していきます。
また以下は前提条件として割愛します。

  • Go のインストール
  • プロジェクトの初期化
  • Gin などの依存パッケージのインストール

ソースコードの構成

最終的には以下のようなツリーを構成します。

.
├── main.go
├── pkg
│   ├── main.go
│   ├── handler
│   │   ├── index.go
│   │   ├── foo.go
│   │   └── error.go
│   └── router
│       └── main.go
├── html
│   └── index.html
└── static
    └── test.js

ハンドラ

はじめにハンドラを3つ用意します。

index.html をレスポンスするためのハンドラ

pkg/handler/index.go

package handler

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func Index(c *gin.Context) {
    c.HTML(http.StatusOK, "index.html", gin.H{})
}

API として JSON をレスポンスするためのハンドラ

ダミーですので、簡単にレスポンスを書いておきます。

pkg/handler/foo.go

package handler

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func GetFoo(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "foo": "this is foo",
    })
}

存在しない API を呼び出されたときのエラーハンドラ

なくてもいい場合がほとんどだと思いますが、一応エラーをレスポンスできるようにしておきます。
pkg/handler/error.go

package handler

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func ApiNotfound(c *gin.Context) {
    c.String(http.StatusOK, "404")
}

ルーター

次は router です。本記事で取り扱う内容のメインはこちらです。 各行にコメントで解説しました。

package router

import (
    "strings"

    "github.com/gin-gonic/gin"

    "github.com/linlymatsumura/go-study/pkg/handler" // 各々のプロジェクトによります。
)

func Build() *gin.Engine {
    router := gin.New()

    // handler.Index で返却するための静的な html ファイルをディレクトリ指定で読み込みます。
    router.LoadHTMLGlob("html/*.html")
 
    // 特定のディレクトリをフロントエンドアセットの配置場所として公開するための設定をします。
    // static 配下のファイル及びディレクトリが参照できるようになります。
    router.Static("static", "static")
 
    // ドメイン直にアクセスされた場合は、handler.Index へ。
    router.GET("/", handler.Index)
 
    // routerで管理しているルートに以外にアクセスされた場合は router.NoRoute に入ります。
    router.NoRoute(func(c *gin.Context) {
        path := c.Request.URL.Path
     
        // そのときのルートが /api で始まるルートのときは...
        if strings.HasPrefix(path, "/api") {
            // error 用のハンドラへ振り分けます。
            handler.ApiNotfound(c)
            return
        }
     
        // それ以外のルートはすべて handler.Index へ。
        // これで、ルートを問わず共通の index.html を返却できるようになりました。
        handler.Index(c)
    })

    // API は共通のパスを持ちますので router.Group でまとめられます。
    apiGroup := router.Group("/api")
 
    // 各 API のハンドラへ。
    apiGroup.GET("/foo", handler.GetFoo)

    return router
}

エントリポイント

main.go 及び pkg/main.go で以下のようにルーターを初期化すれば、完成です。

main.go

package main

import (
    _ "github.com/linlymatsumura/go-study/pkg" // 各々のプロジェクトによります。
)

func main() {
}

pkg/main.go

package app

import (
     "github.com/linlymatsumura/go-study/pkg/router" // 各々のプロジェクトによります。
)

func init() {
    r := router.Build()
    r.Run()
}

終わり

基本的な SPA 用バックエンドを Go で作ることができました。