Go の net/http で Vue.js / Angular 1 などの HTML5 history mode に対応する

type html5Handler struct {
    fs     http.FileSystem
    routes []staticRoute
}

// Look up path when Vue.js HTML5 history mode is enabled
// https://router.vuejs.org/ja/essentials/history-mode.html
func enableHTML5Mode(fs http.FileSystem, routes []staticRoute) http.FileSystem {
    return &html5Handler{fs, routes}
}

func (handler *html5Handler) Open(p string) (http.File, error) {
    file, err := handler.fs.Open(p)
    if err != nil {
        return searchHTML5Mode(handler, p, err)
    }
    return file, err
}

// * Try again after removing prefix if it has
// * Returns index.html otherwise
func searchHTML5Mode(handler *html5Handler, p string, err error) (http.File, error) {
    p = path.Clean(p)
    for _, route := range handler.routes {
        if strings.HasPrefix(p, route.basePath) {
            file, err := handler.fs.Open(strings.TrimPrefix(p, route.basePath))
            if err == nil {
                return file, nil
            }
        }
        if route.indexPattern.MatchString(p) {
            return handler.fs.Open("/index.html")
        }
    }
    return nil, err
}

http.Handle("/", http.FileServer(http.Dir("assets/site")))

とかやってたところを

routes := []staticRoute{
    staticRoute{indexPattern: regexp.MustCompile(`^/feeds/[0-9]+$`), basePath: `/feeds`},
}
http.Handle("/", http.FileServer(enableHTML5Mode(http.Dir("assets/site"), routes)))

に変える。

そうすると routes に定義したパス(この場合は /feeds/:id)に GET でアクセスした場合にも index.html が返り、 index.html の <script> タグや <img> タグ等で指定した静的ファイルも(basePath が取り除かれて再検索されるので)返るようになる。 もちろんもともと http.Dir() で指定してたパスの階層とマッチする場合は今まで通り問題なく静的ファイルが返される。

今回やったのは func (FileSystem) Open(p string) (http.File, error) で第2返り値の error が non-nil だった場合に指定された routes で再検索してるだけです。

ちなみに path.Clean(p)ディレクトリトラバーサル対策です。便利。

合わせて読みたい

www.kaoriya.net