說到往 Go
程式裡面打包進其他非 *.go
資源,在 Go1.16
之前有 go-bindata 第三方開源支援。
在 Go1.16
引入了新特性 embed
,不依賴第三方就能嵌入靜態資源許可權。
然而在專案中,以上兩種方案我都沒用,轉而自研了一套方案。
不想聽我bb心路歷程的,直接跳到 開整 環節看最終實現。
背景
在2021年初,我在參與的專案 go-mod-graph-chart 時,需要把前端資源整合到一個由Go語言構建的 命令列 程式中。
都說到這了,就介紹下 go-mod-graph-chart ,它是一個 將 go mod graph
命令輸出的文字視覺化 命令列 工具(類似於graphviz),在專案目錄下執行go mod graph
會輸出當前專案,所依賴的第三方包,以及第三方包又依賴了什麼包。但當你實際使用時,你會看到一堆的文字,
命令輸出結果如下:
$ go mod graph
go-learn github.com/antlabs/pcurl@v0.0.7
go-learn github.com/bxcodec/faker/v3@v3.6.0
go-learn github.com/go-sql-driver/mysql@v1.5.0
go-learn github.com/jinzhu/copier@v0.3.5
go-learn github.com/pingcap/parser@v0.0.0-20220118030345-6a854bcbd929
go-learn github.com/smartystreets/goconvey@v1.7.2
go-learn golang.org/x/text@v0.3.7
go-learn moul.io/http2curl@v1.0.0
github.com/antlabs/pcurl@v0.0.7 github.com/gin-contrib/gzip@v0.0.1
github.com/antlabs/pcurl@v0.0.7 github.com/gin-gonic/gin@v1.6.3
github.com/antlabs/pcurl@v0.0.7 github.com/guonaihong/clop@v0.0.9
github.com/antlabs/pcurl@v0.0.7 github.com/guonaihong/gout@v0.0.12
github.com/antlabs/pcurl@v0.0.7 github.com/stretchr/testify@v1.6.1
github.com/pingcap/parser@v0.0.0-20220118030345-6a854bcbd929 github.com/cznic/golex@v0.0.0-20181122101858-9c343928389c
github.com/pingcap/parser@v0.0.0-20220118030345-6a854bcbd929 github.com/cznic/mathutil@v0.0.0-20181122101859-297441e03548
...
而使用 gmchart 這個工具,將其視覺化為多級樹狀結構。
如下圖:
在分發這個命令列工具,如果還要帶著靜態資源,或是讓使用者先下個 graphviz
,體驗就很不好了。 於是我就想有什麼辦法,將靜態資源打包到 *.go
程式碼裡。
很不巧在2020年底時, Go1.16 embed
還未推出 ,但我要解決這個問題。go-bindata
在當時無疑是最受歡迎的方案,但會引入第三方依賴。這個時候,我程式碼潔癖上來了,之前我用gin做了http服務,後來發現專案只引入了 gin
,我把gin
換成內建的 http
服務 後,就變成無依賴的專案了。所以,我想繼續保持 no dependency 。我感覺這個功能應該不難,自己也能寫啊。
實現思路
前端打包,有一步是把所有的 *.js
檔案整合到一個 js
檔案。並把最終輸出的 js
檔名寫到 index.html
檔案裡作為入口js
。
Go
靜態資源打包,就是把其他型別的檔案序列化後,儲存到 Go程式碼
裡的靜態變數裡。
Golang
程式在對外提供http服務時,當收到靜態資源請求時,就會去讀取對應變數,輸出到http響應體中,並在 http
heder
中設定對應的 Content-Type
那麼如果想辦法干預下輸出流程,讓其寫 main.js
, index.html
檔案,改為將內容寫入到 go
程式碼的兩個變數,就可以實現 Go
打包靜態資源了。
package gostatic
var IndexHtml = `<!DOCTYPE html>^M
<html lang="en">^M
</html>
var MainJs = `echo "hello";`
var Favicon = `data:image/ico;base64,AAABAAEAmpsAAAEAIAT...`
開整
專案前端構建用到了 webpack,那就在這上面動動手腳了。
一個 gopher 想要去動 webpack?有點自不量力
於是開啟了webpack
的官網,轉了一圈,發現官方提供了plugin,通過自定義外掛,可以影響其構建流程。
這裡在plugin
,獲取到了構建結果,通過遍歷構建結果,獲取到了對於字串,以及檔名,然後我們又插入了一個新的構建結果 go_static.go
,這裡麵包含前面的 main.js
, index.html
檔案的內容。
pack-all-in-go-plugin.js
檔案內容如下:
class PackAllInGoPlugin {
apply(compiler) {
// emit is asynchronous hook, tapping into it using tapAsync, you can use tapPromise/tap(synchronous) as well
compiler.hooks.emit.tapAsync('PackAllInGoPlugin', (compilation, callback) => {
// Create a header string for the generated file:
var filelist = '';
var indexHtml, mainJs;
var goCode = `package godist
func GetFile(file string) string {
switch {
case \`index.html\` == file:
return IndexHtml
case \`main.js\` == file:
return MainJs
case \`favicon.ico\` == file:
return Favicon
default:
return ""
}
}
var IndexHtml = \`--index.html--\`
var MainJs = \`--main.js--\`
var Favicon = \`favicon.ico\``
// Loop through all compiled assets,
// adding a new line item for each filename.
for (var filename in compilation.assets) {
if ("main.js" == filename) {
let jsCode = compilation.assets[filename].source()
let jsCodeString = jsCode.slice();
jsCodeString = jsCodeString.replace(/\`/g, "\` + \"\`\" + \`")
goCode = goCode.replace('--main.js--', jsCodeString)
} else if ("index.html") {
let htmlCode = compilation.assets[filename].source()
goCode = goCode.replace('--index.html--', htmlCode)
}
}
// 將這個列表作為一個新的檔案資源,插入到 webpack 構建中:
compilation.assets['../godist/static.go'] = {
source: function() {
return goCode;
},
size: function() {
return goCode.length;
}
};
callback();
});
}
}
module.exports = PackAllInGoPlugin;
在 webpack
中引入
/*
* 引入自定義外掛
*/
const PackAllInGoPlugin = require('./plugin/pack-all-in-go-plugin');
...
config = {
pulbins: [
...
new PackAllInGoPlugin({options: true})
],
}
這一通設定後,每次執行 npm run build
就能把最新的靜態資源打包進 go_static.go 檔案內了。再執行 go build -o main main.go
go
程式碼和靜態資源就打包到一個可執行檔案內了。
對了!這個 webpack plugin 沒釋出到 npm ,你如果要用直接把原始碼抄過去就行了。中間遇到整合問題,可以看看 https://github.com/PaulXu-cn/... ,這個專案實際有在用。
最後
總的來說,這次是從前端構建這邊來解決了go
打包靜態資源問題,算是橫跨 Go
與 WebPack
,這方案算是比較小眾,基本屬於:
gopher
不想碰前端- 前端為什麼要給
go
寫webpack plugin
我也預見了,這方法也就少數人用用,想試試直接copy程式碼就行,這個小玩意,就不單獨開"坑"了。
好了,我是個愛折騰的 gohper
,這次填了一個我自己製造的坑。如果大家也喜歡搗鼓點不一樣的東西,歡迎一起交流。