golang serve SPA

最近有前端開發的需求,選用了 react-redux-starter-kit 來進行二次開發,省去一些想要使用 React, redux, redux-router 的基本配置,這樣速度會快一點

因為 react-redux-starter-kit 也使用 webpack 進行程式碼的打包, 所以最後的產出預設在 dist 資料夾中,所以部署時只需要一個簡單的 host server 即可

1
2
3
4
5
6
7
8
9
10
11
12
# dist example
$ l
total 752
-rw-r--r-- 1 cage staff 3317 Aug 4 00:22 1.counter.fa53ea42bc9ff9de19bd.js
-rw-r--r-- 1 cage staff 144953 Aug 4 00:22 app.5d0f2ab61ef7dd5daac5.js
-rw-r--r-- 1 cage staff 2619 Aug 4 00:22 app.97a1751c9624097874a4b54cb93fa067.css
-rw-r--r-- 1 cage staff 173 Aug 4 00:33 app.go
-rw-r--r-- 1 cage staff 24838 Aug 4 00:22 favicon.ico
-rw-r--r-- 1 cage staff 103 Aug 4 00:22 humans.txt
-rw-r--r-- 1 cage staff 604 Aug 4 00:22 index.html
-rw-r--r-- 1 cage staff 24 Aug 4 00:22 robots.txt
-rw-r--r-- 1 cage staff 183224 Aug 4 00:22 vendor.9012d9d99074521f418e.js

考慮效能的問題, 最後打算使用 golang 來當作 host server, golang 內建的 net/http 可以輕鬆的使用 http.FileServer(http.Dir("./")) 來 host 整個靜態目錄

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"log"
"net/http"
)
func main() {
log.Println("Listening port 3000...")
log.Fatal(http.ListenAndServe(":3000", http.FileServer(http.Dir("./"))))
}

但是上述的作法基本上是可以動的, 不過如果前端自己有使用到 redux-router 時, golang 並不會將請求導至前端的 router 而是直接得到 golang 404 而不會進到前端 redux-router 訂定的 router (如果有使用 redux-router 對 Notfound 進行處理)

1
http://localhost:3000/dfa

golang 404 page

所以我們使用了 golang echo 的 web framework, 監聽所有的請求並直接導至 index.html 的前端靜態檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main
import (
"flag"
"fmt"
"net/http"
"os"
"strings"
"github.com/labstack/echo"
"github.com/labstack/echo/engine/standard"
mw "github.com/labstack/echo/middleware"
)
const (
wwwRoot = "./"
)
var (
httpPort = flag.Int("http", 3000, "http port number")
)
func Init() *echo.Echo {
e := echo.New()
e.Debug()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Any("/*", echo.HandlerFunc(func(c echo.Context) (err error) {
r := c.Request().(*standard.Request).Request
w := c.Response().(*standard.Response).ResponseWriter
requestPath := r.URL.Path
fileSystemPath := wwwRoot + r.URL.Path
endURIPath := strings.Split(requestPath, "/")[len(strings.Split(requestPath, "/"))-1]
splitPath := strings.Split(endURIPath, ".")
splitLength := len(splitPath)
if splitLength > 1 && splitPath[splitLength-1] != "go" {
f, error := os.Stat(fileSystemPath)
if error == nil && !f.IsDir() {
http.ServeFile(w, r, fileSystemPath)
return
}
}
http.ServeFile(w, r, wwwRoot+"index.html")
return
}))
return e
}
func main() {
flag.Parse()
server := Init()
server.Run(standard.New(fmt.Sprintf(`:%d`, *httpPort)))
}
1
http://localhost:3000/dfa

redux-router 404 page

run SPA as with docker image

處理完 golang 對 single page application 的支援, 進一步簡化部署的流程, 可以將整個 single page application 連同 app.go 利用 golang build 的方式編譯成執行檔, 再將 golang 執行檔透過 Dockerfile 編譯成 docker image, 這樣一來就可以很容易的部署在任何可以執行 container 的環境

flow

上述一系列的流程我們可以使用 npm run scripts 的方式把它完全串起來來達到一鍵建立 docker image

1
2
3
4
5
6
7
8
9
10
FROM alpine:3.3
MAINTAINER cage.chung <cage.chung@gmail.com>
WORKDIR /go
ADD . /go/
EXPOSE 3000
CMD ["./counter"]

Dockerfile 檔案中我們需要指定 docker image 啟動時直接執行我們透過 GOOS=linux GOARCH=amd64 go build -o counter 編譯出來的執行檔 counter*

執行自訂義 npm scripts $ npm run docker 成功執行後會自動建立 docker image

1
2
3
4
# list docker image
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
username/counter v0.1.0 1771ddbe0a98 4 seconds ago 14.67 MB

執行 docker image

1
2
3
4
5
6
7
8
# run docker image
$ docker run -d --name counter -p 3000:3000 username/counter:v0.1.0
f8394fec624a4e3b989f7ce48857f64178e39aa8a5195f39e2d0d5a6572ee55c
# docker ps
$ dps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f8394fec624a username/counter:v0.1.0 "./counter" 2 seconds ago Up 1 seconds 0.0.0.0:3000->3000/tcp counter

repo

Demo example cage1016/golang-serve-spa

1
2
3
4
5
6
7
8
9
10
11
# clone repo
$ git clone git@github.com:cage1016/golang-serve-spa.git
# npm install package
$ npm install
# 一鍵建立 docker image
$ npm docker
# docker run
$ docker run -d --name counter -p 3000:3000 username/counter:v0.1.0