Golang 非主流 打包靜態資源方案

小白要生髮 發表於 2022-07-18
Go

說到往 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/[email protected]
go-learn github.com/bxcodec/faker/[email protected]
go-learn github.com/go-sql-driver/[email protected]
go-learn github.com/jinzhu/[email protected]
go-learn github.com/pingcap/[email protected]
go-learn github.com/smartystreets/[email protected]
go-learn golang.org/x/[email protected]
go-learn moul.io/[email protected]
github.com/antlabs/[email protected] github.com/gin-contrib/[email protected]
github.com/antlabs/[email protected] github.com/gin-gonic/[email protected]
github.com/antlabs/[email protected] github.com/guonaihong/[email protected]
github.com/antlabs/[email protected] github.com/guonaihong/[email protected]
github.com/antlabs/[email protected] github.com/stretchr/[email protected]
github.com/pingcap/[email protected] github.com/cznic/[email protected]
github.com/pingcap/[email protected] github.com/cznic/[email protected]
...

而使用 gmchart 這個工具,將其視覺化為多級樹狀結構。

如下圖:

gmchart-show

在分發這個命令列工具,如果還要帶著靜態資源,或是讓使用者先下個 graphviz ,體驗就很不好了。 於是我就想有什麼辦法,將靜態資源打包到 *.go 程式碼裡。

很不巧在2020年底時, Go1.16 embed 還未推出 ,但我要解決這個問題。go-bindata 在當時無疑是最受歡迎的方案,但會引入第三方依賴。這個時候,我程式碼潔癖上來了,之前我用gin做了http服務,後來發現專案只引入了 gin,我把gin換成內建的 http 服務 後,就變成無依賴的專案了。所以,我想繼續保持 no dependency 。我感覺這個功能應該不難,自己也能寫啊。

git.go.mod

實現思路

前端打包,有一步是把所有的 *.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打包靜態資源問題,算是橫跨 GoWebPack,這方案算是比較小眾,基本屬於:

  1. gopher 不想碰前端
  2. 前端為什麼要給 gowebpack plugin

我也預見了,這方法也就少數人用用,想試試直接copy程式碼就行,這個小玩意,就不單獨開"坑"了。

好了,我是個愛折騰的 gohper,這次填了一個我自己製造的坑。如果大家也喜歡搗鼓點不一樣的東西,歡迎一起交流。

qrcode

參考