之前在这一篇文章里说了我的第一版静态资源代理,后面我又完善了一下:
上一种方案的问题:
- 首页未加入自定义代理中
- 依赖了gin框架的file()方法
- 反复访问本地文件,访问文件系统是很消耗性能的
所以本次我做了改进,思路是:
- 鉴于网站的静态资源占用空间很小,所以我将所有文件加载到内存中,以此摆脱反复访问文件系统,直接访问内存的性能高很多
- 由于文件都在内存中,也就不需要依赖gin的file()方法去读取文件,直接用write()方法放回二进制数据
- 依赖http的缓存机制,增加自定义的文件修改判断(根据文件的md5码或最后修改时间),配合客户端实现缓存机制
缺点也有:
- 需要占用一定的内存,其实一般不多,就10M以内
- 静态资源的更新无法及时更新到内存中(可以通过定时任务或写一个接口手工更新)
照着以上思路,可以在其他语言其他框架中实现,因为对框架没有依赖,都是使用的一些基本功能
第一步,将静态资源加载到内存
首先,实现一个静态资源管理文件:
go
package global
import (
"crypto/md5"
"encoding/hex"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
type StaticFile struct {
File []byte
Md5 string
}
var WebFlieMap = make(map[string]*StaticFile) // 加载前端静态文件到内存,以便快速读取
/**
* @description: 加载静态资源到内存
* @param {map[string]*StaticFile} fileMap
* @return {*}
*/
func LoadWeb(fileMap map[string]*StaticFile) error {
// 遍历目录
err := filepath.Walk("./web", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 忽略目录本身,只处理文件
if !info.IsDir() {
// 移除根目录路径
relativePath := strings.ReplaceAll(strings.TrimPrefix(path, "web"), "\\", "/")
// 读取文件内容
var thisFile StaticFile
thisFile.File, err = os.ReadFile(path)
sum := md5.Sum(thisFile.File)
thisFile.Md5 = hex.EncodeToString(sum[:])
if err != nil {
return err
}
// 将文件路径和内容存入map
fileMap[relativePath] = &thisFile
}
return nil
})
if err != nil {
return err
}
return nil
}
/**
* @description: 更新静态文件的接口
* @param {*gin.Context} c
* @return {*}
*/
func UpdateStaticFile(c *gin.Context) {
WebFlieMapNew := make(map[string]*StaticFile) // 先用一个新的map,防止并发问题
if err := LoadWeb(WebFlieMapNew); err != nil {
thislog := Log{}
thislog.Error("更新静态文件失败", err)
c.Data(200, "text/html", []byte(err.Error()))
return
}
WebFlieMap = WebFlieMapNew // 然后直接覆盖原来的map
c.Data(200, "text/html", []byte("更新成功"))
}
记得在项目启动时,需要调一遍LoadWeb()
然后就是代理了,http的path,就是map的key值:
go
package proxy
import (
"path"
g "src/global"
"strings"
"github.com/gin-gonic/gin"
)
/**
* @description: 静态资源的代理,包含了判断是否支持压缩,是否启用缓存
* @param {*gin.Context} c
* @return {*}
*/
func ProxyStatic(c *gin.Context) {
var file string
urlPath := c.Request.URL.Path
// 路径以/结尾,或者直接以一个目录结尾的,默认其请求的是index.html
if strings.HasSuffix(urlPath, "/") || !strings.Contains(urlPath, ".") {
file = path.Join(urlPath, "index.html")
c.Header("Content-Type", `text/html; charset=utf-8`) // 因为后续可能返回它的压缩文件,必须得手工设置类型,否则c.File获取的是压缩文件的类型
} else { // 其他则是带具体文件名的请求,当前我的文件类型里只有.js、.css、.jpg和.json,如果还有其他,需在此补充
file = urlPath
if strings.HasSuffix(urlPath, ".js") {
c.Header("Content-Type", `application/javascript; charset=utf-8`) // 因为后续可能返回它的压缩文件,必须得手工设置类型,否则c.File获取的是压缩文件的类型
} else if strings.HasSuffix(urlPath, ".css") {
c.Header("Content-Type", `text/css; charset=utf-8`) // 因为后续可能返回它的压缩文件,必须得手工设置类型,否则c.File获取的是压缩文件的类型
} else if strings.HasSuffix(urlPath, ".jpg") {
c.Header("Content-Type", `image/jpeg`)
} else if strings.HasSuffix(urlPath, ".json") {
c.Header("Content-Type", `application/json`)
}
}
// 判断文件是否存在,如果不存在则返回404.html
_, ok := g.WebFlieMap[file]
if !ok {
c.Header("Content-Type", `text/html; charset=utf-8`) // 因为后续可能返回它的压缩文件,必须得手工设置类型,否则c.File获取的是压缩文件的类型
file = `/404.html`
} else if c.Request.Header.Get("If-None-Match") == g.WebFlieMap[file].Md5 { // 检查客户端是否启用缓存,如果启用则检查文件是否修改
writeNotModified(c)
return
}
c.Header("Etag", g.WebFlieMap[file].Md5)
// 下面是判断请求是否支持压缩,如果支持,先判断是否有对应的压缩文件,有则返回压缩文件,无则返回原文件
acceptEncoding := c.Request.Header.Get("Accept-Encoding")
if strings.Contains(acceptEncoding, "br") {
_, ok = g.WebFlieMap[file+".br"]
if ok {
c.Header("Content-Encoding", "br")
file = file + ".br"
}
} else if strings.Contains(acceptEncoding, "gzip") {
_, ok = g.WebFlieMap[file+".gz"]
if ok {
c.Header("Content-Encoding", "gzip")
file = file + ".gz"
}
}
c.Writer.Write(g.WebFlieMap[file].File)
}
/**
* @description: 文件未修改的响应,删除掉不需要的header
* @param {*gin.Context} c
* @return {*}
*/
func writeNotModified(c *gin.Context) {
delete(c.Writer.Header(), "Content-Type")
c.Status(304)
}
自定义代理就更灵活,是否返回压缩文件,是否使用缓存,文件是否修改的校验方式,都可以自己实现。
最后注册一个路由:
go
route.GET("/update_static", global.UpdateStaticFile)
这样,当静态资源更新时,调用一便接口便可以实现更新