使用Golang构建单页应用(SPA)服务的最佳实践

使用Golang构建单页应用(SPA)服务的最佳实践 Azure_Bit_Gopher.png

在 Go 中提供单页应用程序服务

提供基于非哈希路由的单页应用程序服务可能有点棘手。让我们看看如何在 Go 中实现这一点,同时又不影响性能。

1 回复

更多关于使用Golang构建单页应用(SPA)服务的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中为单页应用(SPA)提供静态文件服务时,需要特别注意路由处理。以下是几种最佳实践方案:

1. 基础SPA服务实现

package main

import (
    "net/http"
    "os"
    "path/filepath"
)

func spaHandler(publicDir string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 尝试提供请求的文件
        path := filepath.Join(publicDir, r.URL.Path)
        
        _, err := os.Stat(path)
        if os.IsNotExist(err) {
            // 文件不存在,回退到index.html
            http.ServeFile(w, r, filepath.Join(publicDir, "index.html"))
            return
        }
        
        http.ServeFile(w, r, path)
    }
}

func main() {
    fs := http.FileServer(http.Dir("./dist"))
    http.Handle("/", spaHandler("./dist"))
    
    http.ListenAndServe(":8080", nil)
}

2. 使用标准库的优雅方案

package main

import (
    "net/http"
    "strings"
)

type spaHandler struct {
    staticPath string
    indexPath  string
}

func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    path := h.staticPath + r.URL.Path
    
    // 检查文件是否存在
    if !strings.Contains(r.URL.Path, ".") {
        // 可能是前端路由,返回index.html
        http.ServeFile(w, r, h.staticPath+h.indexPath)
        return
    }
    
    // 尝试提供静态文件
    http.ServeFile(w, r, path)
}

func main() {
    spa := spaHandler{
        staticPath: "./dist",
        indexPath:  "index.html",
    }
    
    http.Handle("/", spa)
    http.ListenAndServe(":8080", nil)
}

3. 支持API路由的完整示例

package main

import (
    "net/http"
    "path/filepath"
    "strings"
)

func main() {
    // API路由
    http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"message": "API response"}`))
    })
    
    // SPA静态文件服务
    fs := http.FileServer(http.Dir("./dist"))
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 如果是API请求,不处理
        if strings.HasPrefix(r.URL.Path, "/api/") {
            return
        }
        
        // 检查文件是否存在
        filePath := filepath.Join("./dist", r.URL.Path)
        if _, err := http.Dir("./dist").Open(r.URL.Path); err != nil {
            // 文件不存在,返回index.html
            http.ServeFile(w, r, "./dist/index.html")
            return
        }
        
        fs.ServeHTTP(w, r)
    }))
    
    http.ListenAndServe(":8080", nil)
}

4. 使用gorilla/mux的增强方案

package main

import (
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    
    // API路由
    api := r.PathPrefix("/api").Subrouter()
    api.HandleFunc("/users", getUsers).Methods("GET")
    api.HandleFunc("/users", createUser).Methods("POST")
    
    // SPA静态文件
    spa := spaHandler{staticPath: "dist", indexPath: "index.html"}
    r.PathPrefix("/").Handler(spa)
    
    http.ListenAndServe(":8080", r)
}

func getUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`[{"id": 1, "name": "John"}]`))
}

func createUser(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)
}

5. 带缓存的性能优化版本

package main

import (
    "net/http"
    "time"
)

func cacheControlMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 对静态资源设置缓存
        if r.URL.Path == "/" || !containsDot(r.URL.Path) {
            w.Header().Set("Cache-Control", "no-cache")
        } else {
            w.Header().Set("Cache-Control", "public, max-age=31536000")
        }
        next.ServeHTTP(w, r)
    })
}

func containsDot(path string) bool {
    for i := 0; i < len(path); i++ {
        if path[i] == '.' && i > 0 {
            return true
        }
    }
    return false
}

func main() {
    fs := http.FileServer(http.Dir("./dist"))
    
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 处理前端路由
        if !containsDot(r.URL.Path) && r.URL.Path != "/" {
            http.ServeFile(w, r, "./dist/index.html")
            return
        }
        fs.ServeHTTP(w, r)
    })
    
    http.Handle("/", cacheControlMiddleware(handler))
    
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      nil,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
    }
    
    srv.ListenAndServe()
}

这些实现方案都确保了:

  1. 静态文件正确服务
  2. 前端路由回退到index.html
  3. API路由与静态文件路由分离
  4. 性能优化(缓存控制)
  5. 支持HTML5 History模式的路由

选择哪种方案取决于具体需求,基础项目可以使用标准库方案,复杂项目推荐使用gorilla/mux或chi等路由库。

回到顶部