go http框架下的静态资源代理实现(压缩,缓存验证自定义)

发布于:2024-05-03 ⋅ 阅读:(28) ⋅ 点赞:(0)

之前在这一篇文章里说了我的第一版静态资源代理,后面我又完善了一下:
上一种方案的问题:

  1. 首页未加入自定义代理中
  2. 依赖了gin框架的file()方法
  3. 反复访问本地文件,访问文件系统是很消耗性能的

所以本次我做了改进,思路是:

  1. 鉴于网站的静态资源占用空间很小,所以我将所有文件加载到内存中,以此摆脱反复访问文件系统,直接访问内存的性能高很多
  2. 由于文件都在内存中,也就不需要依赖gin的file()方法去读取文件,直接用write()方法放回二进制数据
  3. 依赖http的缓存机制,增加自定义的文件修改判断(根据文件的md5码或最后修改时间),配合客户端实现缓存机制

缺点也有:

  1. 需要占用一定的内存,其实一般不多,就10M以内
  2. 静态资源的更新无法及时更新到内存中(可以通过定时任务或写一个接口手工更新)

照着以上思路,可以在其他语言其他框架中实现,因为对框架没有依赖,都是使用的一些基本功能

第一步,将静态资源加载到内存

首先,实现一个静态资源管理文件:

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值:

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)
}

自定义代理就更灵活,是否返回压缩文件,是否使用缓存,文件是否修改的校验方式,都可以自己实现。

最后注册一个路由:

route.GET("/update_static", global.UpdateStaticFile)

这样,当静态资源更新时,调用一便接口便可以实现更新


网站公告

今日签到

点亮在社区的每一天
去签到