refactor: 静态资源缓存破坏改为文件级哈希,重启不刷新未修改文件的缓存
- 启动时计算每个 JS/CSS 的 SHA-1 前 8 位作为版本号 - 文件不变则哈希不变,浏览器继续用缓存,重启零压力 - 替换全局 AssetVersion 为 assetV 模板函数,按文件路径查询哈希 - 移除 BuildPageData 中的全局版本号注入
This commit is contained in:
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"metazone.cc/metalab/internal/common"
|
||||||
"metazone.cc/metalab/internal/config"
|
"metazone.cc/metalab/internal/config"
|
||||||
"metazone.cc/metalab/internal/model"
|
"metazone.cc/metalab/internal/model"
|
||||||
"metazone.cc/metalab/internal/router"
|
"metazone.cc/metalab/internal/router"
|
||||||
@ -48,6 +49,9 @@ func main() {
|
|||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 计算静态资源哈希(用于缓存破坏,文件不变哈希不变)
|
||||||
|
common.ComputeAssetHashes("templates/MetaLab-2026")
|
||||||
|
|
||||||
// 模板加载(前端主题 + 管理后台)
|
// 模板加载(前端主题 + 管理后台)
|
||||||
tmpl, err := theme.LoadTemplates(
|
tmpl, err := theme.LoadTemplates(
|
||||||
theme.TemplateRoot{Dir: "templates/MetaLab-2026/html", Prefix: ""},
|
theme.TemplateRoot{Dir: "templates/MetaLab-2026/html", Prefix: ""},
|
||||||
|
|||||||
@ -1,23 +1,105 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"metazone.cc/metalab/internal/model"
|
"metazone.cc/metalab/internal/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AssetVersion 静态资源版本号(服务启动时生成,重启即刷新缓存)
|
// assetHashes 静态资源文件哈希表(URL 路径 → 短哈希,文件不变哈希不变)
|
||||||
var AssetVersion string
|
var (
|
||||||
|
assetHashes map[string]string
|
||||||
|
assetOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
// ComputeAssetHashes 遍历 static 目录,计算每个文件的 SHA-1 前 8 位作为版本号
|
||||||
AssetVersion = strconv.FormatInt(time.Now().Unix(), 36)
|
func ComputeAssetHashes(templateDir string) {
|
||||||
|
assetHashes = make(map[string]string)
|
||||||
|
staticDir := filepath.Join(templateDir, "static")
|
||||||
|
filepath.WalkDir(staticDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 只处理 .js 和 .css
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if ext != ".js" && ext != ".css" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h := sha1.Sum(data)
|
||||||
|
shortHash := hex.EncodeToString(h[:])[:8]
|
||||||
|
// 生成 URL 路径:/static/...
|
||||||
|
rel, _ := filepath.Rel(filepath.Dir(templateDir), path)
|
||||||
|
urlPath := "/" + filepath.ToSlash(rel)
|
||||||
|
assetHashes[urlPath] = shortHash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// 也处理 shared/static 目录
|
||||||
|
sharedDir := filepath.Join(filepath.Dir(templateDir), "shared", "static")
|
||||||
|
filepath.WalkDir(sharedDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if ext != ".js" && ext != ".css" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h := sha1.Sum(data)
|
||||||
|
shortHash := hex.EncodeToString(h[:])[:8]
|
||||||
|
rel, _ := filepath.Rel(filepath.Dir(templateDir), path)
|
||||||
|
urlPath := "/" + filepath.ToSlash(rel)
|
||||||
|
assetHashes[urlPath] = shortHash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
// 也处理 admin/static 目录
|
||||||
|
adminDir := filepath.Join(filepath.Dir(templateDir), "admin", "static")
|
||||||
|
filepath.WalkDir(adminDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil || d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if ext != ".js" && ext != ".css" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h := sha1.Sum(data)
|
||||||
|
shortHash := hex.EncodeToString(h[:])[:8]
|
||||||
|
rel, _ := filepath.Rel(filepath.Dir(templateDir), path)
|
||||||
|
urlPath := "/" + filepath.ToSlash(rel)
|
||||||
|
assetHashes[urlPath] = shortHash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
log.Printf("[AssetHashes] computed %d file hashes", len(assetHashes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetV 返回指定静态资源的哈希版本号(模板函数)
|
||||||
|
func AssetV(path string) string {
|
||||||
|
if h, ok := assetHashes[path]; ok {
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
return "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// mdImageRe 匹配 Markdown 图片语法 
|
// mdImageRe 匹配 Markdown 图片语法 
|
||||||
@ -57,8 +139,6 @@ func BuildPageData(c *gin.Context, extra gin.H) gin.H {
|
|||||||
if isMaintenance, exists := c.Get("is_maintenance"); exists {
|
if isMaintenance, exists := c.Get("is_maintenance"); exists {
|
||||||
data["IsMaintenance"] = isMaintenance
|
data["IsMaintenance"] = isMaintenance
|
||||||
}
|
}
|
||||||
// 注入静态资源版本号
|
|
||||||
data["V"] = AssetVersion
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"metazone.cc/metalab/internal/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateRoot 模板根目录配置
|
// TemplateRoot 模板根目录配置
|
||||||
@ -21,6 +23,7 @@ func LoadTemplates(roots ...TemplateRoot) (*template.Template, error) {
|
|||||||
"add": func(a, b int) int { return a + b },
|
"add": func(a, b int) int { return a + b },
|
||||||
"subtract": func(a, b int) int { return a - b },
|
"subtract": func(a, b int) int { return a - b },
|
||||||
"formatTTL": formatTTL,
|
"formatTTL": formatTTL,
|
||||||
|
"assetV": common.AssetV,
|
||||||
}
|
}
|
||||||
t := template.New("").Funcs(funcMap)
|
t := template.New("").Funcs(funcMap)
|
||||||
for _, root := range roots {
|
for _, root := range roots {
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script src="/static/js/login.js?v={{.V}}"></script>
|
<script src="/static/js/login.js?v={{assetV "/static/js/login.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -89,7 +89,7 @@
|
|||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script src="/static/js/register.js?v={{.V}}"></script>
|
<script src="/static/js/register.js?v={{assetV "/static/js/register.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -37,6 +37,6 @@
|
|||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -13,9 +13,9 @@
|
|||||||
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
|
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
<title>{{.Title}} - MetaLab</title>
|
<title>{{.Title}} - MetaLab</title>
|
||||||
<link rel="stylesheet" href="/static/css/common.css?v={{.V}}">
|
<link rel="stylesheet" href="/static/css/common.css?v={{assetV "/static/css/common.css"}}">
|
||||||
{{if .ExtraCSS}}
|
{{if .ExtraCSS}}
|
||||||
<link rel="stylesheet" href="{{.ExtraCSS}}?v={{.V}}">
|
<link rel="stylesheet" href="{{.ExtraCSS}}?v={{assetV .ExtraCSS}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
<script src="/shared/static/js/utils.js"></script>
|
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -202,6 +202,6 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -9,6 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -53,6 +53,6 @@
|
|||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -62,9 +62,9 @@
|
|||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||||||
<script src="/static/vditor/dist/index.min.js"></script>
|
<script src="/static/vditor/dist/index.min.js?v={{assetV "/static/vditor/dist/index.min.js"}}"></script>
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script type="module" src="/static/js/editor.js?v={{.V}}"></script>
|
<script type="module" src="/static/js/editor.js?v={{assetV "/static/js/editor.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -48,10 +48,10 @@
|
|||||||
{{/* MD 内容安全传递给 JS */}}
|
{{/* MD 内容安全传递给 JS */}}
|
||||||
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>
|
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||||||
<script src="/static/vditor/dist/method.min.js"></script>
|
<script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}"></script>
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script src="/static/js/shortcode.js?v={{.V}}"></script>
|
<script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var el = document.getElementById('postContent');
|
var el = document.getElementById('postContent');
|
||||||
|
|||||||
@ -253,7 +253,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|||||||
@ -88,6 +88,6 @@
|
|||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
|
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -64,9 +64,9 @@
|
|||||||
|
|
||||||
{{template "layout/footer.html" .}}
|
{{template "layout/footer.html" .}}
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||||||
<script src="/static/vditor/dist/index.min.js"></script>
|
<script src="/static/vditor/dist/index.min.js?v={{assetV "/static/vditor/dist/index.min.js"}}"></script>
|
||||||
<script src="/static/js/common.js?v={{.V}}"></script>
|
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}"></script>
|
||||||
<script src="/static/js/studio-editor.js?v={{.V}}"></script>
|
<script src="/static/js/studio-editor.js?v={{assetV "/static/js/studio-editor.js"}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
|
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
|
||||||
<title>{{.Title}} - MetaLab 管理面板</title>
|
<title>{{.Title}} - MetaLab 管理面板</title>
|
||||||
<link rel="stylesheet" href="/admin/static/css/common.css">
|
<link rel="stylesheet" href="/admin/static/css/common.css?v={{assetV "/admin/static/css/common.css"}}">
|
||||||
{{if .ExtraCSS}}
|
{{if .ExtraCSS}}
|
||||||
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
<link rel="stylesheet" href="{{.ExtraCSS}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="/shared/static/js/utils.js?v={{.V}}"></script>
|
<script src="/shared/static/js/utils.js?v={{assetV "/shared/static/js/utils.js"}}"></script>
|
||||||
<script src="/admin/static/js/common.js?v={{.V}}"></script>
|
<script src="/admin/static/js/common.js?v={{assetV "/admin/static/js/common.js"}}"></script>
|
||||||
{{if .ExtraJS}}
|
{{if .ExtraJS}}
|
||||||
<script src="{{.ExtraJS}}"></script>
|
<script src="{{.ExtraJS}}"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@ -82,5 +82,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<script src="/admin/static/js/posts.js?v={{.V}}"></script>
|
<script src="/admin/static/js/posts.js?v={{assetV "/admin/static/js/posts.js"}}"></script>
|
||||||
{{template "admin/layout/footer.html" .}}
|
{{template "admin/layout/footer.html" .}}
|
||||||
|
|||||||
Reference in New Issue
Block a user