支持 Minify AutoReversion 等
This commit is contained in:
parent
951a9a7397
commit
308def589d
14
go.mod
14
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
|
||||
|
||||
205
service.go
205
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, "<html")) {
|
||||
if strings.HasPrefix(contentType, "text/html") || (contentType == "" && strings.Contains(outStr[0:int(math.Min(float64(len(outStr)), 100))], "<html")) {
|
||||
if strings.Contains(outStr, "let _watchWS = null") {
|
||||
return nil, false
|
||||
}
|
||||
@ -355,6 +378,166 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
if inWatch {
|
||||
// watch模式下,不自动压缩,方便调试定位脚本
|
||||
serviceConfig.AutoMinify = false
|
||||
}
|
||||
|
||||
// 处理自动版本号和自动压缩(非watch模式)
|
||||
if serviceConfig.AutoReversion || serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||
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:int(math.Min(float64(len(outStr)), 100))], "<html")) {
|
||||
outChanged := false
|
||||
newOutStr := resourceRegex.ReplaceAllStringFunc(outStr, func(match string) string {
|
||||
subs := resourceRegex.FindStringSubmatch(match)
|
||||
attr := subs[1] // src/href...
|
||||
eq := subs[2]
|
||||
quote1 := subs[3]
|
||||
webPath := subs[4] // /res/app.js
|
||||
ext := "." + subs[5] // .js
|
||||
suffix := subs[6] // ?v=123456
|
||||
quote2 := subs[7]
|
||||
if strings.Contains(webPath, "://") || strings.HasPrefix(webPath, "//") || strings.HasPrefix(webPath, "data:") {
|
||||
return match
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(webPath, "/") {
|
||||
if strings.HasSuffix(request.URL.Path, "/") {
|
||||
webPath = request.URL.Path + webPath
|
||||
} else {
|
||||
webPath = path.Join(path.Dir(request.URL.Path), webPath)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取磁盘物理路径
|
||||
localPath := s.GetStaticPath(webPath, request.Host)
|
||||
if localPath == "" {
|
||||
return match
|
||||
}
|
||||
|
||||
changed := false
|
||||
|
||||
// 自动查找翻译文件
|
||||
if serviceConfig.AutoTranslate {
|
||||
language := ""
|
||||
if cookieLanguage, err := request.Cookie("language"); err == nil {
|
||||
language = cookieLanguage.Value
|
||||
} else if uaLanguage := request.Header.Get("Accept-Language"); uaLanguage != "" {
|
||||
language = uaLanguage
|
||||
} else {
|
||||
language = "en-US"
|
||||
}
|
||||
translationPath := strings.TrimSuffix(localPath, ext) + "." + language + ext
|
||||
if u.FileExists(translationPath) {
|
||||
localPath = translationPath
|
||||
webPath = strings.TrimSuffix(webPath, ext) + "." + language + ext
|
||||
changed = true
|
||||
} else {
|
||||
language2 := strings.Split(language, "-")[0]
|
||||
translationPath = strings.TrimSuffix(localPath, ext) + "." + language2 + ext
|
||||
if u.FileExists(translationPath) {
|
||||
localPath = translationPath
|
||||
webPath = strings.TrimSuffix(webPath, ext) + "." + language2 + ext
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if serviceConfig.AutoMinify && !strings.Contains(webPath, ".min.") && !strings.Contains(webPath, "-min.") && (ext == ".js" || ext == ".css" || ext == ".svg" || ext == ".json") {
|
||||
localMinPath := strings.TrimSuffix(localPath, ext) + ".min" + ext
|
||||
if err := Minify(localPath, localMinPath); err == nil {
|
||||
webPath = strings.TrimSuffix(webPath, ext) + ".min" + ext
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if serviceConfig.AutoReversion {
|
||||
if srcStat := u.GetFileInfo(localPath); srcStat != nil {
|
||||
ver := uint64(srcStat.ModTime.Unix())
|
||||
verStr := string(u.HashInt(u.EncodeInt(ver)))
|
||||
if suffix == "" {
|
||||
suffix = fmt.Sprintf("?v=%s", verStr)
|
||||
} else if suffix[0] == '?' {
|
||||
suffix = fmt.Sprintf("?v=%s&%s", verStr, suffix[1:])
|
||||
} else {
|
||||
suffix = fmt.Sprintf("?v=%s%s", verStr, suffix)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
// 拼接新路径,保留原有属性名
|
||||
outChanged = true
|
||||
return fmt.Sprintf(`%s%s%s%s%s%s`, attr, eq, quote1, webPath, suffix, quote2)
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if outChanged {
|
||||
return newOutStr, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
})
|
||||
}
|
||||
|
||||
// 处理html自动压缩(非watch模式)
|
||||
if (inWatch && serviceConfig.AutoReversion) || serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||
s.SetStaticRewriter(func(filename string, request *s.Request, response *s.Response, logger *log.Logger) string {
|
||||
changed := false
|
||||
isHTML := strings.HasSuffix(filename, ".html") || strings.HasSuffix(filename, ".htm")
|
||||
if (inWatch && serviceConfig.AutoReversion) && isHTML {
|
||||
// 开发模式下自动处理应用版本时,移除If-Modified-Since头,禁用html缓存
|
||||
request.Header.Del("If-Modified-Since")
|
||||
}
|
||||
if serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||
if isHTML && !strings.Contains(filename, ".min.") && !strings.Contains(filename, "-min.") {
|
||||
ext := filepath.Ext(filename)
|
||||
|
||||
if serviceConfig.AutoTranslate {
|
||||
language := ""
|
||||
if cookieLanguage, err := request.Cookie("language"); err == nil {
|
||||
language = cookieLanguage.Value
|
||||
} else if uaLanguage := request.Header.Get("Accept-Language"); uaLanguage != "" {
|
||||
language = uaLanguage
|
||||
} else {
|
||||
language = "en-US"
|
||||
}
|
||||
translationPath := strings.TrimSuffix(filename, ext) + "." + language + ext
|
||||
if u.FileExists(translationPath) {
|
||||
filename = translationPath
|
||||
changed = true
|
||||
} else {
|
||||
language2 := strings.Split(language, "-")[0]
|
||||
translationPath = strings.TrimSuffix(filename, ext) + "." + language2 + ext
|
||||
if u.FileExists(translationPath) {
|
||||
filename = translationPath
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if serviceConfig.AutoMinify {
|
||||
minFilename := strings.TrimSuffix(filename, ext) + ".min" + ext
|
||||
if err := Minify(filename, minFilename); err == nil {
|
||||
filename = minFilename
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
return filename
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
server = s.AsyncStart()
|
||||
waitChan = make(chan bool, 1)
|
||||
@ -716,6 +899,19 @@ func init() {
|
||||
// }
|
||||
// return nil
|
||||
// },
|
||||
"minify": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
args := gojs.MakeArgs(&argsIn, vm).Check(2)
|
||||
if err := Minify(args.Str(0), args.Str(1)); err == nil {
|
||||
} else {
|
||||
vm.SetData("_lastError", err.Error())
|
||||
gojs.GetLogger(vm).Error(err.Error())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
"getStaticPath": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
args := gojs.MakeArgs(&argsIn, vm).Check(2)
|
||||
return vm.ToValue(s.GetStaticPath(args.Str(0), args.Str(1)))
|
||||
},
|
||||
"setTplReplaces": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
||||
replaces := args.Map(0)
|
||||
@ -790,6 +986,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fnList) > 0 {
|
||||
tpl = tpl.Funcs(fnList)
|
||||
}
|
||||
|
||||
@ -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<string, LimiterConfig> // 限流器配置,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<string, string> // 代理配置,key为[host][path],value为代理的目标应用或URL
|
||||
|
||||
99
web.go
Normal file
99
web.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user