diff --git a/plugins/wasm-go/extensions/frontend-gray/README.md b/plugins/wasm-go/extensions/frontend-gray/README.md index eea2256fd..fd60b790f 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README.md +++ b/plugins/wasm-go/extensions/frontend-gray/README.md @@ -17,6 +17,7 @@ description: 前端灰度插件配置参考 | 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | |----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------| | `grayKey` | string | 非必填 | - | 用户ID的唯一标识,可以来自Cookie或者Header中,比如 userid,如果没有填写则使用`rules[].grayTagKey`和`rules[].grayTagValue`过滤灰度规则 | +| `useManifestAsEntry` | boolean | 非必填 | false | 是否使用manifest作为入口。当设置为true时,系统将使用manifest文件作为应用入口,适用于微前端架构。在这种模式下,系统会根据manifest文件的内容来加载不同版本的前端资源。 | | `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 | | `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` | | `storeMaxAge` | int | 非必填 | 60 * 60 * 24 * 365 | 网关设置Cookie最大存储时长:单位为秒,默认为1年 | diff --git a/plugins/wasm-go/extensions/frontend-gray/README_EN.md b/plugins/wasm-go/extensions/frontend-gray/README_EN.md index 76c73740d..dbdae00ed 100644 --- a/plugins/wasm-go/extensions/frontend-gray/README_EN.md +++ b/plugins/wasm-go/extensions/frontend-gray/README_EN.md @@ -15,6 +15,7 @@ Execution Priority: `1000` | Name | Data Type | Required | Default | Description | |------|-----------|----------|---------|-------------| | `grayKey` | string | Optional | - | Unique user identifier from Cookie/Header (e.g., userid). If empty, uses `rules[].grayTagKey` and `rules[].grayTagValue` to filter rules. | +| `useManifestAsEntry` | boolean | Optional | false | Whether to use manifest as entry point. When set to true, the system will use manifest file as application entry, suitable for micro-frontend architecture. In this mode, the system loads different versions of frontend resources based on manifest file content. | | `localStorageGrayKey` | string | Optional | - | When using JWT authentication, user ID comes from `localStorage`. Overrides `grayKey` if configured. | | `graySubKey` | string | Optional | - | Used when user info is in JSON format (e.g., `userInfo:{ userCode:"001" }`). In this example, `graySubKey` would be `userCode`. | | `storeMaxAge` | int | Optional | 31536000 | Max cookie storage duration in seconds (default: 1 year). | @@ -78,7 +79,7 @@ Execution Priority: `1000` | Name | Data Type | Required | Default | Description | |------|-----------|----------|---------|-------------| | `key` | string | Optional | `HIGRESS_CONSOLE_CONFIG` | Window global variable key. | -| `featureKey` | string | Optional | `FEATURE_STATUS` | Rule hit status (e.g., `{"beta-user":true}`). | +| `featureKey` | string | Optional | `FEATURE_STATUS` | Rule hit status (e.g., `{"beta-user":true,"inner-user":false}`). | | `value` | string | Optional | - | Custom global value. | | `enabled` | boolean | Optional | `false` | Enable global injection. | @@ -86,7 +87,7 @@ Execution Priority: `1000` | Name | Data Type | Required | Default | Description | |------|-----------|----------|---------|-------------| | `first` | string[] | Optional | - | Inject at body start. | -| `after` | string[] | Optional | - | Inject at body end. | +| `last` | string[] | Optional | - | Inject at body end. | ## Configuration Examples ### Basic Configuration (User-based) @@ -111,3 +112,165 @@ grayDeployments: - name: beta-user version: gray enabled: true + +``` + +The unique user identifier in the cookie is `userid`, and the current grayscale rule configures the `beta-user` rule. + +When the following conditions are met, the `version: gray` version will be used: +- `userid` in cookie equals `00000002` or `00000003` +- `level` in cookie equals `level3` or `level5` + +Otherwise, the `version: base` version will be used. + +### Percentage-based Grayscale +```yml +grayKey: userid +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true + weight: 80 + +``` +The total grayscale rule is 100%, with the grayscale version weighted at 80% and the baseline version at 20%. + +### User Information in JSON Format +```yml +grayKey: appInfo +graySubKey: userId +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +- name: beta-user + grayKeyValue: + - '00000002' + - '00000003' + grayTagKey: level + grayTagValue: + - level3 + - level5 +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true + +``` + +The cookie contains JSON data in `appInfo`, which includes the `userId` field as the unique identifier. +The current grayscale rule configures the `beta-user` rule. +When the following conditions are met, the `version: gray` version will be used: +- `userid` in cookie equals `00000002` or `00000003` +- `level` in cookie equals `level3` or `level5` + +Otherwise, the `version: base` version will be used. + +### User Information Stored in LocalStorage +Since the gateway plugin needs to identify users by unique identity information, and HTTP protocol can only transmit information in headers, a script can be injected into the homepage to set user information from LocalStorage to cookies if user information is stored in LocalStorage. + +``` +(function() { + var grayKey = '@@X_GRAY_KEY'; + var cookies = document.cookie.split('; ').filter(function(row) { + return row.indexOf(grayKey + '=') === 0; + }); + + try { + if (typeof localStorage !== 'undefined' && localStorage !== null) { + var storageValue = localStorage.getItem(grayKey); + var cookieValue = cookies.length > 0 ? decodeURIComponent(cookies[0].split('=')[1]) : null; + if (storageValue && storageValue.indexOf('=') < 0 && cookieValue && cookieValue !== storageValue) { + document.cookie = grayKey + '=' + encodeURIComponent(storageValue) + '; path=/;'; + window.location.reload(); + } + } + } catch (error) { + // xx + } +})(); +``` + +### Rewrite Configuration +> Generally used for CDN deployment scenarios +```yml +grayKey: userid +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +- name: beta-user + grayKeyValue: + - '00000002' + - '00000003' + grayTagKey: level + grayTagValue: + - level3 + - level5 +rewrite: + host: frontend-gray.oss-cn-shanghai-internal.aliyuncs.com + indexRouting: + /app1: '/mfe/app1/{version}/index.html' + /: '/mfe/app1/{version}/index.html', + fileRouting: + /: '/mfe/app1/{version}' + /app1/: '/mfe/app1/{version}' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true + +``` + +The `{version}` will be dynamically replaced with the actual version during runtime. + +#### indexRouting: Homepage Routing Configuration +Accessing `/app1`, `/app123`, `/app1/index.html`, `/app1/xxx`, `/xxxx` will all route to '/mfe/app1/{version}/index.html' + +#### fileRouting: File Routing Configuration +The following file mappings will be effective: +- `/js/a.js` => `/mfe/app1/v1.0.0/js/a.js` +- `/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js` +- `/app1/js/a.js` => `/mfe/app1/v1.0.0/js/a.js` +- `/app1/js/template/a.js` => `/mfe/app1/v1.0.0/js/template/a.js` + +### Injecting Code into HTML Homepage +```yml +grayKey: userid +rules: +- name: inner-user + grayKeyValue: + - '00000001' + - '00000005' +baseDeployment: + version: base +grayDeployments: + - name: beta-user + version: gray + enabled: true +injection: + head: + - + body: + first: + - + - + last: + - + - + +``` +Code can be injected into the HTML homepage through `injection`, either in the `head` tag or at the `first` and `last` positions of the `body` tag. diff --git a/plugins/wasm-go/extensions/frontend-gray/config/config.go b/plugins/wasm-go/extensions/frontend-gray/config/config.go index 6d7234265..a066dc772 100644 --- a/plugins/wasm-go/extensions/frontend-gray/config/config.go +++ b/plugins/wasm-go/extensions/frontend-gray/config/config.go @@ -60,6 +60,7 @@ type BodyInjection struct { type GrayConfig struct { StoreMaxAge int + UseManifestAsEntry bool GrayKey string LocalStorageGrayKey string GraySubKey string @@ -129,6 +130,7 @@ func convertToStringMap(result gjson.Result) map[string]string { func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) error { // 解析 GrayKey grayConfig.LocalStorageGrayKey = json.Get("localStorageGrayKey").String() + grayConfig.UseManifestAsEntry = json.Get("useManifestAsEntry").Bool() grayConfig.GrayKey = json.Get("grayKey").String() if grayConfig.LocalStorageGrayKey != "" { grayConfig.GrayKey = grayConfig.LocalStorageGrayKey diff --git a/plugins/wasm-go/extensions/frontend-gray/main.go b/plugins/wasm-go/extensions/frontend-gray/main.go index abf062e6a..874632fcd 100644 --- a/plugins/wasm-go/extensions/frontend-gray/main.go +++ b/plugins/wasm-go/extensions/frontend-gray/main.go @@ -129,23 +129,26 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig) ctx.DontReadResponseBody() return types.ActionContinue } - isIndexRequest, indexOk := ctx.GetContext(config.IsIndexRequest).(bool) - if indexOk && isIndexRequest { - // 首页请求强制不缓存 - proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") - ctx.DontReadResponseBody() - return types.ActionContinue + if !grayConfig.UseManifestAsEntry { + isIndexRequest, indexOk := ctx.GetContext(config.IsIndexRequest).(bool) + if indexOk && isIndexRequest { + // 首页请求强制不缓存 + proxywasm.ReplaceHttpResponseHeader("cache-control", "no-cache, no-store, max-age=0, must-revalidate") + ctx.DontReadResponseBody() + return types.ActionContinue + } + + isHtmlRequest, htmlOk := ctx.GetContext(config.IsHtmlRequest).(bool) + // response 不处理非首页的请求 + if !htmlOk || !isHtmlRequest { + ctx.DontReadResponseBody() + return types.ActionContinue + } else { + // 不会进去Streaming 的Body处理 + ctx.BufferResponseBody() + } } - isHtmlRequest, htmlOk := ctx.GetContext(config.IsHtmlRequest).(bool) - // response 不处理非首页的请求 - if !htmlOk || !isHtmlRequest { - ctx.DontReadResponseBody() - return types.ActionContinue - } else { - // 不会进去Streaming 的Body处理 - ctx.BufferResponseBody() - } // 处理HTML的首页 status, err := proxywasm.GetHttpResponseHeader(":status") if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" { diff --git a/plugins/wasm-go/extensions/frontend-gray/util/utils.go b/plugins/wasm-go/extensions/frontend-gray/util/utils.go index 031ec558b..9e722db5b 100644 --- a/plugins/wasm-go/extensions/frontend-gray/util/utils.go +++ b/plugins/wasm-go/extensions/frontend-gray/util/utils.go @@ -304,7 +304,10 @@ func GetConditionRules(rules []*config.GrayRule, grayKeyValue string, cookie str } func GetGrayWeightUniqueId(cookie string, uniqueGrayTag string) string { - uniqueId := GetCookieValue(cookie, uniqueGrayTag) + uniqueId, _ := proxywasm.GetHttpRequestHeader(uniqueGrayTag) + if uniqueId == "" { + uniqueId = GetCookieValue(cookie, uniqueGrayTag) + } if uniqueId == "" { uniqueId = strings.ReplaceAll(uuid.NewString(), "-", "") }