From 308def589d7c79d1a63819e183fca8a3f0fd6a55 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Thu, 7 May 2026 21:11:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20Minify=20AutoReversion=20?= =?UTF-8?q?=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 14 ++-- service.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++-- service.ts | 6 ++ web.go | 99 ++++++++++++++++++++++++++ 4 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 web.go diff --git a/go.mod b/go.mod index 3df2629..f190bd3 100644 --- a/go.mod +++ b/go.mod @@ -16,9 +16,10 @@ require ( github.com/ssgo/httpclient v1.7.9 github.com/ssgo/log v1.7.11 github.com/ssgo/redis v1.7.8 - github.com/ssgo/s v1.7.26 + github.com/ssgo/s v1.7.30 github.com/ssgo/standard v1.7.8 github.com/ssgo/u v1.7.24 + github.com/tdewolff/minify/v2 v2.24.11 ) require ( @@ -32,15 +33,16 @@ require ( github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/gomodule/redigo v1.9.3 // indirect github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect - github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect + github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/shoenig/go-m1cpu v0.2.1 // indirect github.com/ssgo/tool v0.4.29 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tdewolff/parse/v2 v2.8.11 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/ysmood/fetchup v0.2.3 // indirect github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/got v0.40.0 // indirect @@ -53,3 +55,5 @@ require ( golang.org/x/text v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/ssgo/s => ../../ssgo/s diff --git a/service.go b/service.go index 44c9175..54d377f 100644 --- a/service.go +++ b/service.go @@ -6,7 +6,9 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" + "path" "path/filepath" "reflect" "regexp" @@ -77,6 +79,9 @@ type Config struct { LimiterRedis string HotLoad int TplSafePaths []string + AutoReversion bool + AutoMinify bool + AutoTranslate bool Proxy map[string]string Rewrite map[string]string @@ -89,6 +94,19 @@ var onStop goja.Callable var limiters = map[string]*s.Limiter{} var configed = false +var strTplRegexp = regexp.MustCompile(`%(\w+)%`) + +func MakeByVar(text string, vars map[string]any) string { + text = strTplRegexp.ReplaceAllStringFunc(text, func(match string) string { + key := match[1 : len(match)-1] + if value, ok := vars[key]; ok { + return u.String(value) + } + return match + }) + return text +} + func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { configed = true s.InitConfig() @@ -96,7 +114,7 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { s.SetWorkPath(u.String(startPath)) } // 处理配置 - serviceConfig = Config{"Session", "Device", "Client", "id", "name", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", 0, []string{}, map[string]string{}, map[string]string{}, map[string]string{}} + serviceConfig = Config{"Session", "Device", "Client", "id", "name", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", 0, []string{}, false, false, false, map[string]string{}, map[string]string{}, map[string]string{}} if errs := config.LoadConfig("service", &serviceConfig); len(errs) > 0 { // panic(vm.NewGoError(errs[0])) vm.SetData("_lastError", errs[0]) @@ -104,7 +122,6 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { return } config.LoadConfig("service", &discover.Config) - // var auth goja.Callable if opt != nil { u.Convert(opt.O.Export(), &serviceConfig) u.Convert(opt.O.Export(), &s.Config) @@ -247,8 +264,13 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { } }, nil, nil) } + + // 处理多语言 + // LoadLanguage("language.yml") } +var resourceRegex = regexp.MustCompile(`(?i)(src|href|data|\w+)(\s*[=:]\s*)(["']?)([^"'\s>]+\.(js|css|json|svg|\w+))([^"'\s>]*)(["']?)`) + func init() { s.Config.KeepKeyCase = true s.DontStartLogAuto = true @@ -283,7 +305,8 @@ func init() { } // 处理Watch - if inWatch := vm.GetData("inWatch"); inWatch != nil && inWatch.(bool) { + inWatch := u.Bool(vm.GetData("inWatch")) + if inWatch { onWatchConn := map[string]*websocket.Conn{} onWatchLock := sync.Mutex{} vm.SetData("onWatch", func(filename string) { @@ -308,7 +331,7 @@ func init() { s.SetOutFilter(func(in map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) { contentType := response.Header().Get("Content-Type") outStr := u.String(out) - if strings.HasPrefix(contentType, "text/html") || (contentType == "" && strings.Contains(outStr, " 0 { tpl = tpl.Funcs(fnList) } diff --git a/service.ts b/service.ts index e26e45b..91b9ba6 100644 --- a/service.ts +++ b/service.ts @@ -25,6 +25,8 @@ export default { // setTplFunc, setTplReplaces, tpl, + minify, + getStaticPath, } function config(config?: Config): void { } @@ -56,6 +58,8 @@ function id(size?: number): string { return '' } // function setTplFunc(fnList: Object): void { } function setTplReplaces(replaces: Object): void { } function tpl(file: string, data: Object, fnList?: Object): string { return '' } +function minify(from: string, to: string): string { return '' } +function getStaticPath(requestPath: string, host?: string): string { return '' } export interface Config { // github.com/ssgo/s 的配置参数 @@ -122,6 +126,8 @@ export interface Config { limiters: Map // 限流器配置,from 为数据来源,例如:ip、user、device、header.User-Agent、in.phone 等(in表示从请求参数中获取),time为时间间隔,单位ms,times为 时间间隔内允许访问的次数 hotLoad: number // 热加载配置,单位s,默认值:0,0表示不检测热加载 tplSafePaths: string[] // 模板文件的安全路径,默认不开启安全检查 + autoReversion: boolean // 是否自动为html中引用的本地.js .css .json .svg 等文件添加版本号,默认不开启 + autoMinify: boolean // 是否自动压缩html中引用的本地.js .css .json .svg 等文件,默认不开启 // gateway 的配置参数 proxy: Map // 代理配置,key为[host][path],value为代理的目标应用或URL diff --git a/web.go b/web.go new file mode 100644 index 0000000..8eaae4e --- /dev/null +++ b/web.go @@ -0,0 +1,99 @@ +package service + +import ( + "errors" + "os" + "path/filepath" + "regexp" + "strings" + "syscall" + + "github.com/ssgo/u" + "github.com/tdewolff/minify/v2" + "github.com/tdewolff/minify/v2/css" + "github.com/tdewolff/minify/v2/html" + "github.com/tdewolff/minify/v2/js" + "github.com/tdewolff/minify/v2/json" + "github.com/tdewolff/minify/v2/svg" + "github.com/tdewolff/minify/v2/xml" +) + +var m *minify.M + +func init() { + m = minify.New() + m.AddFunc("text/html", html.Minify) + m.AddFunc("text/css", css.Minify) + m.AddFunc("image/svg+xml", svg.Minify) + m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify) + m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify) + m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify) +} + +var reJSBacktickContent = regexp.MustCompile("(?s)`([^`]*)`") +var reHTMLBetweenTags = regexp.MustCompile(`>\s+<`) + +func Minify(from, to string) error { + srcStat := u.GetFileInfo(from) + if srcStat == nil { + return errors.New("file not found: " + from) + } + dstStat := u.GetFileInfo(to) + if dstStat != nil && !srcStat.ModTime.After(dstStat.ModTime) { + return nil + } + + tmpTo := filepath.Join(filepath.Dir(to), "."+filepath.Base(to)) + dst, err := os.OpenFile(tmpTo, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer os.Remove(tmpTo) + defer dst.Close() + + if syscall.Flock(int(dst.Fd()), syscall.LOCK_EX) == nil { + defer syscall.Flock(int(dst.Fd()), syscall.LOCK_UN) + } + + dstStat = u.GetFileInfo(to) + if dstStat != nil && !srcStat.ModTime.After(dstStat.ModTime) { + return nil + } + + input := u.ReadFileN(from) + processedInput := reJSBacktickContent.ReplaceAllStringFunc(input, func(s string) string { + return "`" + reHTMLBetweenTags.ReplaceAllString(strings.TrimSpace(s[1:len(s)-1]), "><") + "`" + }) + + minifiedOutput, err := m.String(getMimeType(from), processedInput) + if err != nil { + return err + } + + if _, err = dst.WriteString(minifiedOutput); err != nil { + return err + } + if err = dst.Close(); err == nil { + err = os.Rename(tmpTo, to) + } + return err +} + +func getMimeType(filename string) string { + switch filepath.Ext(filename) { + case ".html", ".htm": + return "text/html" + case ".css": + return "text/css" + case ".js": + return "application/javascript" + case ".json": + return "application/json" + case ".svg": + return "image/svg+xml" + case ".xml": + return "text/xml" + default: + return "text/plain" + } +}