<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 Go1.16 實現(xiàn)靜態(tài)文件的 HTTP Cache

          共 7626字,需瀏覽 16分鐘

           ·

          2021-01-16 20:07

          閱讀本文大概需要 15 分鐘。

          大家好,我是站長 polarisxu。

          之前寫過一篇文章:《提前試用將在 Go1.16 中發(fā)布的內(nèi)嵌靜態(tài)資源功能》,如果之前沒閱讀,建議繼續(xù)看本文前先閱讀下該文。

          現(xiàn)在 Go 1.16 Beta 已經(jīng)發(fā)布,離正式版發(fā)布不遠了,在 GitHub 發(fā)現(xiàn)了一個庫,它實現(xiàn)了 io/fs.FS 接口,它能夠計算文件的 SHA256 哈希值并附加到文件名中以允許進行 HTTP Cache:即控制靜態(tài)文件的版本。本文對其進行介紹并順帶講解一些涉及到的其他內(nèi)容。

          溫馨提示:本文內(nèi)容基于 Go 1.16 Beta,之前版本不支持!

          01 hashfs 包

          包地址:https://github.com/benbjohnson/hashfs,有效代碼函數(shù)不到 200。

          對于給定的一個文件,比如 scripts/main.js,hashfs.FS 文件系統(tǒng)處理后會生成一個帶 hash 的文件,類似 scripts/main-b633a..d628.js(中間有省略),客戶端請求該文件時,可以選擇讓客戶端緩存。hash 算法使用的是 SHA256。當(dāng)文件內(nèi)容發(fā)生變化時,hash 值也會變。

          該包默認提供對 net/http 的兼容。通過例子看看具體怎么使用。

          02 基于 net/http 的使用

          創(chuàng)建一個目錄,使用 module:

          $?mkdir?~/embed
          $?cd?~/embed
          $?go?mod?init?gtihub.com/polaris1119/embed

          為了基于同一個項目演示不同使用方式,創(chuàng)建如下目錄結(jié)構(gòu):

          ├──?cmd
          │???├──?std
          │???│???└──?main.go
          ├──?embed.go
          ├──?go.mod
          ├──?go.sum
          ├──?static
          │???└──?main.js?//?主要處理該文件的嵌入、hash
          ├──?template
          │???└──?index.html

          其中 embed.go 的作用在本文開頭文章提到過,內(nèi)容如下:

          package?embed

          import?(
          ?"embed"

          ?"github.com/benbjohnson/hashfs"
          )

          //go:embed?static
          var?embedFS?embed.FS

          //?帶?hash?功能的?fs.FS
          var?Fsys?=?hashfs.NewFS(embedFS)

          再說一句,因為 //go:embed 只能相對當(dāng)前源文件所在目錄,所以單獨創(chuàng)建這個文件以便和 static 在同一級目錄。

          index.html 和 main.js 的內(nèi)容很簡單。

          index.html:

          <html>
          ??<head>
          ????<title>測試?Embed?Hashtitle>
          ????<script?src="/assets/{{.mainjs}}">script>
          ??head>
          ??<body>
          ????<h1>測試?Embed?Hashh1>
          ????<hr>
          ????<div>
          ??????以下內(nèi)容來自 JS:
          ????div>
          ????<p?id="content"?style="color:?red;">p>
          ??body>
          html>

          該模板中有一個變量:mainjs。

          main.js:

          window.onload?=?function()?{
          ????document.querySelector('#content').innerHTML?=?"我是?JS?內(nèi)容";
          }

          如果一切正常,看到的頁面如下:

          在 cmd/std/main.go 中寫上如下代碼:

          package?main

          import?(
          ?"fmt"
          ?"html/template"
          ?"log"
          ?"net/http"

          ?"github.com/benbjohnson/hashfs"
          ?"github.com/polaris1119/embed"
          )

          func?main()?{
          ?http.Handle("/assets/",?http.StripPrefix("/assets/",?hashfs.FileServer(embed.Fsys)))

          ?http.HandleFunc("/",?func(w?http.ResponseWriter,?r?*http.Request)?{
          ??tpl,?err?:=?template.New("index.html").ParseFiles("template/index.html")
          ??if?err?!=?nil?{
          ???fmt.Fprint(w,?err.Error())
          ???return
          ??}

          ??err?=?tpl.Execute(w,?map[string]interface{}{
          ???"mainjs":?embed.Fsys.HashName("static/main.js"),
          ??})
          ??if?err?!=?nil?{
          ???fmt.Fprint(w,?err.Error())
          ???return
          ??}
          ?})

          ?log.Fatal(http.ListenAndServe(":8080",?nil))
          }
          • 特意為靜態(tài)資源加上 /assets/ 前綴,后文解釋;
          • hashfs.FileServer(embed.Fsys)) 是 hashfs 包對 net/http 的支持,即 hashfs.FileServer 是一個 http.Handler;
          • embed.Fsys.HashName("static/main.js") 將文件生成為帶 hash 的;

          執(zhí)行 go run ./cmd/std/main.go,打開瀏覽器訪問:http://localhost:8080 即可看到上面截圖的頁面,審查元素可以看到如下信息,緩存一年。(見代碼:https://github.com/benbjohnson/hashfs/blob/main/hashfs.go#L200)

          當(dāng)你再次刷新瀏覽器,看到 js 文件直接從緩存獲取的。

          當(dāng) main.js 的內(nèi)容發(fā)生變化,main-xxx.js 中的 hash 部分也會變化,你可以自行試驗。(注意,因為資源內(nèi)嵌了,修改了 js 的內(nèi)容,需要重新 go run)。

          03 關(guān)于服務(wù)靜態(tài)文件

          這塊有必要單獨拿出來說下,因為比較容易搞錯。比如上面的一行代碼改為這樣:

          http.Handle("/assets",?http.StripPrefix("/assets",?hashfs.FileServer(embed.Fsys)))

          再次運行結(jié)果就不對(沒有 “我是 JS 內(nèi)容”)。(注意禁用瀏覽器緩存,否則看不到效果)

          如果是 Echo 框架,則可以:

          e.Static("/assets",?".")

          Gin 框架,也可以:

          router.Static("/assets",?".")

          關(guān)于其中的細節(jié),大家有興趣可以查閱相關(guān)源碼。這里只要記住,服務(wù)目錄,末尾加上 /,(目錄嘛,應(yīng)該有 /),即:

          http.Handle("/assets/",?...)

          04 基于 Echo 的使用

          在 cmd 目錄下創(chuàng)建 echo/main.go 文件:

          package?main

          import?(
          ?"bytes"
          ?"fmt"
          ?"io"
          ?"mime"
          ?"net/http"
          ?"net/url"
          ?"os"
          ?"path"
          ?"strconv"
          ?"text/template"

          ?"github.com/benbjohnson/hashfs"
          ?"github.com/labstack/echo/v4"
          ?"github.com/polaris1119/embed"
          )

          func?main()?{
          ?e?:=?echo.New()

          ?e.GET("/assets/*",?func(ctx?echo.Context)?error?{
          ??filename,?err?:=?url.PathUnescape(ctx.Param("*"))
          ??if?err?!=?nil?{
          ???return?err
          ??}

          ??isHashed?:=?false
          ??if?base,?hash?:=?hashfs.ParseName(filename);?hash?!=?""?{
          ???if?embed.Fsys.HashName(base)?==?filename?{
          ????filename?=?base
          ????isHashed?=?true
          ???}
          ??}

          ??f,?err?:=?embed.Fsys.Open(filename)
          ??if?os.IsNotExist(err)?{
          ???return?echo.ErrNotFound
          ??}?else?if?err?!=?nil?{
          ???return?echo.ErrInternalServerError
          ??}
          ??defer?f.Close()

          ??//?Fetch?file?info.?Disallow?directories?from?being?displayed.
          ??fi,?err?:=?f.Stat()
          ??if?err?!=?nil?{
          ???return?echo.ErrInternalServerError
          ??}?else?if?fi.IsDir()?{
          ???return?echo.ErrForbidden
          ??}

          ??contentType?:=?"text/plain"
          ??//?Determine?content?type?based?on?file?extension.
          ??if?ext?:=?path.Ext(filename);?ext?!=?""?{
          ???contentType?=?mime.TypeByExtension(ext)
          ??}

          ??//?Cache?the?file?aggressively?if?the?file?contains?a?hash.
          ??if?isHashed?{
          ???ctx.Response().Header().Set("Cache-Control",?`public,?max-age=31536000`)
          ??}

          ??//?Set?content?length.
          ??ctx.Response().Header().Set("Content-Length",?strconv.FormatInt(fi.Size(),?10))

          ??//?Flush?header?and?write?content.
          ??buf?:=?new(bytes.Buffer)
          ??if?ctx.Request().Method?!=?"HEAD"?{
          ???io.Copy(buf,?f)
          ??}
          ??return?ctx.Blob(http.StatusOK,?contentType,?buf.Bytes())
          ?})

          ?e.GET("/",?func(ctx?echo.Context)?error?{
          ??tpl,?err?:=?template.New("index.html").ParseFiles("template/index.html")
          ??if?err?!=?nil?{
          ???return?err
          ??}

          ??var?buf?=?new(bytes.Buffer)
          ??err?=?tpl.Execute(buf,?map[string]interface{}{
          ???"mainjs":?embed.Fsys.HashName("static/main.js"),
          ??})
          ??if?err?!=?nil?{
          ???return?err
          ??}
          ??return?ctx.HTML(http.StatusOK,?buf.String())
          ?})

          ?e.Logger.Fatal(e.Start(":8080"))
          }
          • 服務(wù)靜態(tài)文件的代碼:e.GET("/assets/*", func(ctx echo.Context) error {,主要參照了 https://github.com/benbjohnson/hashfs/blob/main/hashfs.go#L162 的實現(xiàn);
          • 首頁的路由和 net/http 基本一樣,關(guān)注 mainjs 模板變量;

          簡單解釋下服務(wù)靜態(tài)文件的實現(xiàn)原理:

          • 獲取請求的路徑(* 部分);
          • 通過 hashfs.ParseName 解析出文件的 base 和 hash 兩部分;
          • 使用 fs.FS 打開文件,判斷文件類型、大小,并將內(nèi)容返回給客戶端,如果有緩存,設(shè)置 HTTP Cache;

          運行 go run ./cmd/echo/main.go,不出意外和 net/http 版本一樣的效果。

          05 基于 Gin 的使用

          其實知道了如何基于 Echo 框架使用,其他框架參照著實現(xiàn)即可。因為 Gin 框架用戶多,因此也實現(xiàn)下。

          在 cmd 目錄下創(chuàng)建文件:gin/main.go

          package?main

          import?(
          ?"bytes"
          ?"io"
          ?"mime"
          ?"net/http"
          ?"net/url"
          ?"os"
          ?"path"
          ?"strconv"
          ?"strings"

          ?"github.com/benbjohnson/hashfs"
          ?"github.com/gin-gonic/gin"
          ?"github.com/polaris1119/embed"
          )

          func?main()?{
          ?r?:=?gin.Default()

          ?r.GET("/assets/*filepath",?func(ctx?*gin.Context)?{
          ??filename,?err?:=?url.PathUnescape(ctx.Param("filepath"))
          ??if?err?!=?nil?{
          ???ctx.AbortWithError(http.StatusInternalServerError,?err)
          ???return
          ??}
          ??filename?=?strings.TrimPrefix(filename,?"/")

          ??isHashed?:=?false
          ??if?base,?hash?:=?hashfs.ParseName(filename);?hash?!=?""?{
          ???if?embed.Fsys.HashName(base)?==?filename?{
          ????filename?=?base
          ????isHashed?=?true
          ???}
          ??}

          ??f,?err?:=?embed.Fsys.Open(filename)
          ??if?os.IsNotExist(err)?{
          ???ctx.AbortWithError(http.StatusNotFound,?err)
          ???return
          ??}?else?if?err?!=?nil?{
          ???ctx.AbortWithError(http.StatusInternalServerError,?err)
          ???return
          ??}
          ??defer?f.Close()

          ??//?Fetch?file?info.?Disallow?directories?from?being?displayed.
          ??fi,?err?:=?f.Stat()
          ??if?err?!=?nil?{
          ???ctx.AbortWithError(http.StatusInternalServerError,?err)
          ???return
          ??}?else?if?fi.IsDir()?{
          ???ctx.AbortWithError(http.StatusForbidden,?err)
          ???return
          ??}

          ??contentType?:=?"text/plain"
          ??//?Determine?content?type?based?on?file?extension.
          ??if?ext?:=?path.Ext(filename);?ext?!=?""?{
          ???contentType?=?mime.TypeByExtension(ext)
          ??}

          ??//?Cache?the?file?aggressively?if?the?file?contains?a?hash.
          ??if?isHashed?{
          ???ctx.Writer.Header().Set("Cache-Control",?`public,?max-age=31536000`)
          ??}

          ??//?Set?content?length.
          ??ctx.Writer.Header().Set("Content-Length",?strconv.FormatInt(fi.Size(),?10))

          ??//?Flush?header?and?write?content.
          ??buf?:=?new(bytes.Buffer)
          ??if?ctx.Request.Method?!=?"HEAD"?{
          ???io.Copy(buf,?f)
          ??}
          ??ctx.Data(http.StatusOK,?contentType,?buf.Bytes())
          ?})

          ?r.LoadHTMLGlob("template/*")
          ?r.GET("/",?func(ctx?*gin.Context)?{
          ??ctx.HTML(http.StatusOK,?"index.html",?gin.H{
          ???"mainjs":?embed.Fsys.HashName("static/main.js"),
          ??})
          ?})
          ?r.Run(":8080")
          }

          服務(wù)靜態(tài)文件的內(nèi)容和 Echo 框架基本一樣,除了各自框架特有的。

          因為 Gin 框架提供了 LoadHTMLGlob,首頁路由的處理函數(shù)代碼很簡單。

          運行 go run ./cmd/gin/main.go,不出意外和 net/http 版本一樣的效果。

          06 總結(jié)

          舉一反三,在學(xué)習(xí)過程中可以讓你更好的掌握某個知識點。

          之前有讀者問到 module 如何使用 vendor(沒網(wǎng)情況下使用)。今天試驗這個就是用了 vendor。其實它的使用很簡單,在項目下執(zhí)行:go mod vendor 即可。不過需要注意的是,加入了新的依賴,就應(yīng)該執(zhí)行一次 go mod vendor。

          今天介紹的這個庫在這個時代用到的可能性不高,不過也有可能會用得到。更重要的是希望這篇文章可以作為一個小項目實踐下。希望你能從頭自己編碼實現(xiàn)。

          另外還留了一個問題給你:index.html 文件沒有內(nèi)嵌,請你自己完成。(提示:html/template 增加了對 io/fs.Fs 的支持)

          本項目完整代碼:https://github.com/polaris1119/embed。




          往期推薦


          歡迎關(guān)注我

          瀏覽 142
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品婷婷久久久久 | 欧美成人版 | 水蜜桃成视频人app | 国产v欧美v亚洲v精品v | 女人十八毛片一级A片蜜臀 |