diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5483545..0321f80 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,7 +23,7 @@ body: attributes: label: Version / 版本 description: What version of our software are you running? - placeholder: v0.1.1 + placeholder: v0.1.2 validations: required: true - type: textarea diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d17d4..4dc5ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v0.1.2 + +* Optimize: compress frontend assets. + +* 优化: 压缩前端资源,加快加载速度。 + + + ## v0.1.1 * Add: text file editor. diff --git a/server/handler/bridge.go b/server/handler/bridge.go index 7d9600c..1f84123 100644 --- a/server/handler/bridge.go +++ b/server/handler/bridge.go @@ -100,16 +100,16 @@ func bridgePush(ctx *gin.Context) { buf := make([]byte, 2<<14) srcConn.SetReadDeadline(common.Now.Add(5 * time.Second)) n, err := bridge.src.Request.Body.Read(buf) + if n == 0 { + break + } if err != nil { eof = err == io.EOF if !eof { break } } - if n == 0 { - break - } - dstConn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + dstConn.SetWriteDeadline(common.Now.Add(10 * time.Second)) _, err = bridge.dst.Writer.Write(buf[:n]) if eof || err != nil { break @@ -154,16 +154,16 @@ func bridgePull(ctx *gin.Context) { buf := make([]byte, 2<<14) srcConn.SetReadDeadline(common.Now.Add(5 * time.Second)) n, err := bridge.src.Request.Body.Read(buf) + if n == 0 { + break + } if err != nil { eof = err == io.EOF if !eof { break } } - if n == 0 { - break - } - dstConn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + dstConn.SetWriteDeadline(common.Now.Add(10 * time.Second)) _, err = bridge.dst.Writer.Write(buf[:n]) if eof || err != nil { break diff --git a/server/handler/generate.go b/server/handler/generate.go index 602056c..982328e 100644 --- a/server/handler/generate.go +++ b/server/handler/generate.go @@ -43,7 +43,7 @@ func checkClient(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusBadRequest, modules.Packet{Code: -1, Msg: `${i18n|invalidParameter}`}) return } - _, err := os.Open(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch)) + _, err := os.Stat(fmt.Sprintf(config.BuiltPath, form.OS, form.Arch)) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, modules.Packet{Code: 1, Msg: `${i18n|osOrArchNotPrebuilt}`}) return diff --git a/server/main.go b/server/main.go index fa735b6..0b923c6 100644 --- a/server/main.go +++ b/server/main.go @@ -11,9 +11,12 @@ import ( "encoding/hex" "fmt" "github.com/rakyll/statik/fs" + "io" "net" "os" "os/signal" + "path" + "strings" "syscall" "time" @@ -70,7 +73,9 @@ func main() { handler.InitRouter(app.Group(`/api`)) app.Any(`/ws`, wsHandshake) app.NoRoute(handler.AuthHandler, func(ctx *gin.Context) { - http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request) + if !serveGzip(ctx, webFS) && !checkCache(ctx, webFS) { + http.FileServer(webFS).ServeHTTP(ctx.Writer, ctx.Request) + } }) } @@ -306,3 +311,81 @@ func authCheck() gin.HandlerFunc { lastRequest = now } } + +func serveGzip(ctx *gin.Context, statikFS http.FileSystem) bool { + headers := ctx.Request.Header + filename := path.Clean(ctx.Request.RequestURI) + if !strings.Contains(headers.Get(`Accept-Encoding`), `gzip`) { + return false + } + if strings.Contains(headers.Get(`Connection`), `Upgrade`) { + return false + } + if strings.Contains(headers.Get(`Accept`), `text/event-stream`) { + return false + } + + file, err := statikFS.Open(filename + `.gz`) + if err != nil { + return false + } + + file.Seek(0, io.SeekStart) + conn, ok := ctx.Request.Context().Value(`Conn`).(net.Conn) + if !ok { + file.Close() + return false + } + + etag := fmt.Sprintf(`"%x-%s"`, []byte(filename), config.COMMIT) + if headers.Get(`If-None-Match`) == etag { + ctx.Status(http.StatusNotModified) + return true + } + ctx.Header(`Cache-Control`, `max-age=604800`) + ctx.Header(`ETag`, etag) + ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) + + ctx.Writer.Header().Del(`Content-Length`) + ctx.Header(`Content-Encoding`, `gzip`) + ctx.Header(`Vary`, `Accept-Encoding`) + ctx.Status(http.StatusOK) + + for { + eof := false + buf := make([]byte, 2<<14) + n, err := file.Read(buf) + if n == 0 { + break + } + if err != nil { + eof = err == io.EOF + if !eof { + break + } + } + conn.SetWriteDeadline(common.Now.Add(10 * time.Second)) + _, err = ctx.Writer.Write(buf[:n]) + if eof || err != nil { + break + } + } + conn.SetWriteDeadline(time.Time{}) + file.Close() + ctx.Done() + return true +} + +func checkCache(ctx *gin.Context, _ http.FileSystem) bool { + filename := path.Clean(ctx.Request.RequestURI) + + etag := fmt.Sprintf(`"%x-%s"`, []byte(filename), config.COMMIT) + if ctx.Request.Header.Get(`If-None-Match`) == etag { + ctx.Status(http.StatusNotModified) + return true + } + ctx.Header(`ETag`, etag) + ctx.Header(`Cache-Control`, `max-age=604800`) + ctx.Header(`Expires`, common.Now.Add(7*24*time.Hour).Format(`Mon, 02 Jan 2006 15:04:05 GMT`)) + return false +} diff --git a/web/package-lock.json b/web/package-lock.json index af51ec8..97f9190 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -40,6 +40,7 @@ "antd-dayjs-webpack-plugin": "^1.0.6", "babel-loader": "^8.2.4", "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^10.0.0", "copy-webpack-plugin": "^10.2.4", "css-loader": "^6.7.1", "html-webpack-plugin": "^5.5.0", @@ -3189,6 +3190,79 @@ "node": ">= 0.8.0" } }, + "node_modules/compression-webpack-plugin": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz", + "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression-webpack-plugin/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/compression-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/compression-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/compression-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -10686,6 +10760,57 @@ } } }, + "compression-webpack-plugin": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-10.0.0.tgz", + "integrity": "sha512-wLXLIBwpul/ALcm7Aj+69X0pYT3BYt6DdPn3qrgBIh9YejV9Bju9ShhlAsjujLyWMo6SAweFIWaUoFmXZNuNrg==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, "compute-scroll-into-view": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", diff --git a/web/package.json b/web/package.json index 536a5a5..b3bf9bf 100644 --- a/web/package.json +++ b/web/package.json @@ -40,6 +40,7 @@ "antd-dayjs-webpack-plugin": "^1.0.6", "babel-loader": "^8.2.4", "clean-webpack-plugin": "^4.0.0", + "compression-webpack-plugin": "^10.0.0", "copy-webpack-plugin": "^10.2.4", "css-loader": "^6.7.1", "html-webpack-plugin": "^5.5.0", diff --git a/web/webpack.config.js b/web/webpack.config.js index a5732ab..6ae86c6 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -1,9 +1,10 @@ -const path = require('path'); -const TerserPlugin = require('terser-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const {CleanWebpackPlugin} = require('clean-webpack-plugin'); -const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin'); +const path = require("path"); +const TerserPlugin = require("terser-webpack-plugin"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const {CleanWebpackPlugin} = require("clean-webpack-plugin"); +const AntdDayjsWebpackPlugin = require("antd-dayjs-webpack-plugin"); +const CompressionPlugin = require("compression-webpack-plugin"); module.exports = (env, args) => { let mode = args.mode; @@ -12,7 +13,7 @@ module.exports = (env, args) => { output: { publicPath: mode === 'development' ? undefined : './', path: path.resolve(__dirname, 'dist'), - filename: '[name].js' //'[name].[contenthash:7].js' + filename: '[name].[contenthash:7].js' }, devtool: mode === 'development' ? 'eval-source-map' : false, module: { @@ -70,6 +71,15 @@ module.exports = (env, args) => { from: path.resolve(__dirname, 'public/ext-modelist.js'), } ] + }), + new CompressionPlugin({ + test: /\.js$|\.css$|\.html$/, + filename: "[file].gz", + algorithm: "gzip", + threshold: 256 * 1024, + compressionOptions: { + level: 9 + } }) ], optimization: { @@ -77,6 +87,7 @@ module.exports = (env, args) => { minimizer: [ new TerserPlugin({ test: /\.js(\?.*)?$/i, + parallel: true, extractComments: false, terserOptions: { compress: { @@ -87,22 +98,38 @@ module.exports = (env, args) => { } }) ], - runtimeChunk: 'single', + runtimeChunk: 'multiple', splitChunks: { chunks: 'initial', cacheGroups: { - runtime: { - name: 'runtime', - test: (module) => { - return /axios|react|redux|antd|ant-design/.test(module.context); - }, + react: { + test: /react|redux|react-router/i, + priority: -1, + chunks: 'all', + reuseExistingChunk: true + }, + common: { + test: /axios|i18next|crypto-js|dayjs/i, + priority: -2, + chunks: 'all', + reuseExistingChunk: true + }, + antd: { + test: /antd|ant-design/i, + priority: -3, + chunks: 'all', + reuseExistingChunk: true + }, + addon: { + test: /xterm|react-ace|ace-builds/i, + priority: -4, chunks: 'initial', - priority: 10, reuseExistingChunk: true }, vendor: { - test: /[\\/]node_modules[\\/]/, - name: 'vendors', + test: /[\\/]node_modules[\\/]/i, + priority: -5, + chunks: 'initial', reuseExistingChunk: true } }