SPA に特化した http.FileServer

SPA の紹介に特化した http.FileServer (http.Handler) を作っていたので紹介します。

Single Page Application (以下 SPA) の特徴の1つとして、 クライアント側でルーティングを行うことがあります。 これは端的に言うと /foo/ というパスに SPA を配置した場合、 Web サーバーは /foo/bar/foo/bar/quux/ など /foo/ 以下のパスに対しては /foo/ と同じコンテンツを返さなければならないことを意味します。

先日 tyru さんが Go の net/http で Vue.js / Angular 1 などの HTML5 history mode に対応する において、それに対応する方法を記事にしていました。

実は私も似たようなことをするけど設計思想が少し違うパッケージ koron/go-spafs をちょっと前に作っていました。 どのように違っているかというと、 golang のコードとしてパスの読み替えルール(正規表現)を記述するのではなく、 普通の http.FileServer と同じようにルートとなるディレクトリだけを指定して、 SPA として振る舞う箇所はそのディレクトリの中身で記述するようになっています。

具体的に説明しましょう。 前提として /usr/local/www をルートディレクトリ (以下ルート) として Webファイルサーバで公開するとします。 その場合のコードは以下のようになります。

package main

import (
	"net/http"

	spafs "github.com/koron/go-spafs"
)

func main() {
	fs := spafs.FileServer(http.Dir("/usr/local/www"))
	err := http.ListenAndServe(":8080", fs)
	if err != nil {
		panic(err)
	}
}

ここでルートに 以下のようにファイルを配置したと仮定しましょう。

/usr/local/www/
    foo/
        index.html
        main.js
        style.css
        aaa/
            index.html
    other/
        content.html
    robot.txt

この時 /foo/ に対するリクエストには foo/index.html を返します。 また SPA でルーティングされる /foo/bar/foo/bar/quux/ に対しても foo/index.html を返します。 一方で /foo/ の外である /other/content.html/robot.txt へのリクエストに対しては、そのまま対応するファイルを返します。

さらに /foo/ の中でもファイルが実在するパス、 /foo/main.js/foo/style.css については存在しているファイルを返します。

さらに実在しているファイル名が index.html だった場合、 そのパス以下は独立した SPA として振る舞います。 上の例では /foo/aaa/ 以下のパス (/foo/aaa/bbb/foo/aaa/bbb/ccc/) に対しては、 いずれも foo/aaa/index.html を返します。

つまり go-spafs ではリクエストされたパスに対応するリソースを http.FileSystem に問い合わせ、 見つかればそれをそのまま返し、 なければ親となるパスの index.html を ルートまで遡って探して見つかったものを返す (なければ当然404エラーとなる) という単純な動作をしています。

パスの存在確認にディスクI/Oが比較的多めに走るので本番運用には向かない可能性がありますが、 開発用としてはパスの組み換えや SPA の追加などが容易にできるので便利に使えます。 以上 go-spafs の紹介でした。