簡體   English   中英

在 Go 程序中捆綁靜態資源的最佳方式是什么?

[英]What's the best way to bundle static resources in a Go program?

我正在使用 Go 開發一個小型 Web 應用程序,該應用程序旨在用作開發人員機器上的工具,以幫助調試他們的應用程序/Web 服務。 該程序的界面是一個網頁,不僅包括 HTML,還包括一些 JavaScript(用於功能)、圖像和 CSS(用於樣式)。 我打算開源這個應用程序,所以用戶應該能夠運行一個 Makefile,所有的資源都會去他們需要去的地方。 但是,我還希望能夠簡單地分發具有盡可能少的文件/依賴項的可執行文件。 有沒有一種將 HTML/CSS/JS 與可執行文件捆綁在一起的好方法,這樣用戶只需下載並擔心一個文件?


現在,在我的應用程序中,提供靜態文件看起來有點像這樣:

// called via http.ListenAndServe
func switchboard(w http.ResponseWriter, r *http.Request) {

    // snipped dynamic routing...

    // look for static resource
    uri := r.URL.RequestURI()
    if fp, err := os.Open("static" + uri); err == nil {
        defer fp.Close()
        staticHandler(w, r, fp)
        return
    }

    // snipped blackhole route
}
    

所以這很簡單:如果請求的文件存在於我的靜態目錄中,則調用處理程序,它只是打開文件並嘗試在服務之前設置一個好的Content-Type 我的想法是沒有理由這需要基於真實的文件系統:如果有已編譯的資源,我可以簡單地通過請求 URI 索引它們並照此提供它們。

讓我知道是否沒有好的方法可以做到這一點,或者我試圖這樣做是在吠叫錯誤的樹。 我只是認為最終用戶會希望管理盡可能少的文件。

如果有比更合適的標簽,請隨時添加它們或讓我知道。

從 Go 1.16 開始,go 工具支持將靜態文件直接嵌入到可執行二進制文件中。

您必須導入embed包,並使用//go:embed指令來標記要嵌入哪些文件以及要將它們存儲到哪個變量中。

hello.txt文件嵌入可執行文件的 3 種方法:

import "embed"

//go:embed hello.txt
var s string
print(s)

//go:embed hello.txt
var b []byte
print(string(b))

//go:embed hello.txt
var f embed.FS
data, _ := f.ReadFile("hello.txt")
print(string(data))

對變量使用embed.FS類型,您甚至可以將多個文件包含到一個變量中,該變量將提供一個簡單的文件系統接口:

// content holds our static web server content.
//go:embed image/* template/*
//go:embed html/index.html
var content embed.FS

net/http支持使用http.FS()embed.FS的值提供文件,如下所示:

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(content))))

模板包還可以使用text/template.ParseFS()html/template.ParseFS()函數和text/template.Template.ParseFS()html/template.Template.ParseFS()方法解析模板:

template.ParseFS(content, "*.tmpl")

以下答案列出了您的舊選項(在 Go 1.16 之前)。


嵌入文本文件

如果我們談論的是文本文件,它們可以很容易地嵌入到源代碼本身中。 只需使用反引號來聲明string文字,如下所示:

const html = `
<html>
<body>Example embedded HTML content.</body>
</html>
`

// Sending it:
w.Write([]byte(html))  // w is an io.Writer

優化提示:

由於大多數時候您只需將資源寫入io.Writer ,您還可以存儲[]byte轉換的結果:

var html = []byte(`
<html><body>Example...</body></html>
`)

// Sending it:
w.Write(html)  // w is an io.Writer

唯一需要注意的是原始字符串文字不能包含反引號字符 (`)。 原始字符串文字不能包含序列(與解釋的字符串文字不同),因此如果要嵌入的文本確實包含反引號,則必須打破原始字符串文字並將反引號連接為解釋字符串文字,如下例所示:

var html = `<p>This is a back quote followed by a dot: ` + "`" + `.</p>`

性能不受影響,因為這些連接將由編譯器執行。

嵌入二進制文件

存儲為字節片

對於二進制文件(例如圖像),最緊湊(關於生成的本機二進制文件)和最有效的方法是將文件的內容作為源代碼中的[]byte 這可以由go-bindata等第三方工具/庫生成。

如果您不想為此使用第 3 方庫,這里有一個簡單的代碼片段,它讀取二進制文件,並輸出聲明[]byte類型變量的 Go 源代碼,該變量將使用文件:

imgdata, err := ioutil.ReadFile("someimage.png")
if err != nil {
    panic(err)
}

fmt.Print("var imgdata = []byte{")
for i, v := range imgdata {
    if i > 0 {
        fmt.Print(", ")
    }
    fmt.Print(v)
}
fmt.Println("}")

如果文件包含從 0 到 16 的字節的示例輸出(在Go Playground上嘗試):

var imgdata = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}

存儲為 base64 string

如果文件不是“太大”(大多數圖像/圖標都符合條件),那么還有其他可行的選項。 您可以將文件的內容轉換為 Base64 string並將其存儲在源代碼中。 在應用程序啟動時( func init() )或需要時,您可以將其解碼為原始[]byte內容。 Go 在encoding/base64包中對 Base64 編碼有很好的支持。

將(二進制)文件轉換為 base64 string非常簡單:

data, err := ioutil.ReadFile("someimage.png")
if err != nil {
    panic(err)
}
fmt.Println(base64.StdEncoding.EncodeToString(data))

將結果 base64 字符串存儲在您的源代碼中,例如作為const

解碼它只是一個函數調用:

const imgBase64 = "<insert base64 string here>"

data, err := base64.StdEncoding.DecodeString(imgBase64) // data is of type []byte

存儲為帶引號的string

比存儲為 base64 更有效,但在源代碼中可能更長的是存儲二進制數據的帶引號的字符串文字。 我們可以使用strconv.Quote()函數獲取任何字符串的引號形式:

data, err := ioutil.ReadFile("someimage.png")
if err != nil {
    panic(err)
}
fmt.Println(strconv.Quote(string(data))

對於包含從 0 到 64 的值的二進制數據,這是輸出的樣子(在Go Playground上試試):

"\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?"

(請注意, strconv.Quote()會在其前面附加一個引號。)

您可以在源代碼中直接使用這個帶引號的字符串,例如:

const imgdata = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?"

即用即用,無需解碼; 取消引用是由 Go 編譯器在編譯時完成的。

如果您需要它,您也可以將其存儲為字節片:

var imgdata = []byte("\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?")

go-bindata 包看起來可能是您感興趣的。

https://github.com/go-bindata/go-bindata

它將允許您將任何靜態文件轉換為可嵌入代碼中的函數調用,並在調用時返回文件內容的字節切片。

捆綁 React 應用程序

例如,您有一個來自 react 的構建輸出,如下所示:

build/favicon.ico
build/index.html
build/asset-manifest.json
build/static/css/**
build/static/js/**
build/manifest.json

當你像這樣使用go:embed時,它會將內容作為http://localhost:port/build/index.html提供,這不是我們想要的(意外的/build )。

//go:embed build/*
var static embed.FS

// ...
http.Handle("/", http.FileServer(http.FS(static)))

事實上,我們需要再采取一步,通過使用fs.Sub使其按預期工作:

package main

import (
    "embed"
    "io/fs"
    "log"
    "net/http"
)

//go:embed build/*
var static embed.FS

func main() {
    contentStatic, _ := fs.Sub(static, "build")
    http.Handle("/", http.FileServer(http.FS(contentStatic)))
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

現在, http://localhost:8080應該按預期為您的 Web 應用程序提供服務。

歸功於阿米特米塔爾

注意: go:embed需要 go 1.16 或更高版本。

還有一些奇特的方式——我使用maven 插件來構建 GoLang 項目,它允許使用JCP 預處理器將二進制塊和文本文件嵌入到源代碼中。 在這種情況下,代碼如下所示( 可以在此處找到一些示例

var imageArray = []uint8{/*$binfile("./image.png","uint8[]")$*/}

作為另一個答案中提到的go-bindata的流行替代方案, mjibson/esc還嵌入了任意文件,但特別方便地處理目錄樹。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM