From d3550411b170b6992429e551beb930911473d530 Mon Sep 17 00:00:00 2001 From: Booker Date: Wed, 3 Jun 2026 12:17:36 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20H5=20=E7=BA=BF?= =?UTF-8?q?=E8=B7=AF=E9=85=8D=E7=BD=AE=E9=80=BB=E8=BE=91=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=BB=98=E8=AE=A4=E7=BA=BF=E8=B7=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=92=8C=E7=9B=B8=E5=85=B3=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + lib/config/app_config.dart | 96 ++++++++++++++++++++++++++++++++------ test/widget_test.dart | 38 +++++++++++++++ 3 files changed, 123 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 87736d7..2e7a572 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Flutter WebView 套壳 App,启动后会请求 `/client_config/query` 获取 H5 线路配置,并加载第一条可用线路。 +默认情况下,非本地 Web 访问会使用当前服务地址的 origin 作为 H5 线路和接口域名,例如访问 `https://app.example.com/app/` 时会请求 `https://app.example.com/client_config/query`。本地访问(如 `localhost`、`127.0.0.1`)继续使用固定兜底域名 `https://h5-test.imharry.work/`。 + ## H5 线路切换 线路切换在 Flutter 套壳层完成。远程配置不可用时,会先使用启动兜底线路;也可以在打包时覆盖启动线路或配置接口地址: diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 320af2f..9c8d3e9 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -42,8 +42,7 @@ class AppConfig { static List get h5Lines { final configuredUrls = _dartDefinedH5LineUrls.trim().isEmpty - ? _normalizedUniqueUrls(_environmentHosts[currentEnvironment] ?? - const [_bootstrapH5LineUrl]) + ? _normalizedUniqueUrls(defaultH5LineUrlsForBaseUri(Uri.base)) : _normalizedUniqueUrls(_dartDefinedH5LineUrls.split(',')); final urls = configuredUrls.isEmpty ? _normalizedUniqueUrls(const [_bootstrapH5LineUrl]) @@ -71,18 +70,26 @@ class AppConfig { return const []; } - final homeUri = lines.first.uri; + return clientConfigQueryUrisForHomeUri(lines.first.uri); + } + + static List defaultH5LineUrlsForBaseUri(Uri baseUri) { + final runtimeHomeUrl = _runtimeHomeUrlFromBaseUri(baseUri); + if (runtimeHomeUrl != null) { + return [runtimeHomeUrl]; + } + + final environmentHosts = _environmentHosts[currentEnvironment]; + if (environmentHosts == null || environmentHosts.isEmpty) { + return const [_bootstrapH5LineUrl]; + } + return environmentHosts; + } + + static List clientConfigQueryUrisForHomeUri(Uri homeUri) { return [ - homeUri.replace( - path: '/client_config/query', - queryParameters: const {}, - fragment: '', - ), - homeUri.replace( - path: '/api/user/client_config/query', - queryParameters: const {}, - fragment: '', - ), + _originUriWithPath(homeUri, '/client_config/query'), + _originUriWithPath(homeUri, '/api/user/client_config/query'), ]; } @@ -191,6 +198,69 @@ class AppConfig { return uri.replace(path: path).toString(); } + static String? _runtimeHomeUrlFromBaseUri(Uri baseUri) { + if (_isLocalRuntimeUri(baseUri)) { + return null; + } + + return _originUriWithPath(baseUri, '/').toString(); + } + + static Uri _originUriWithPath(Uri uri, String path) { + final normalizedPath = path.startsWith('/') ? path : '/$path'; + return Uri.parse('${uri.scheme}://${uri.authority}$normalizedPath'); + } + + static bool _isLocalRuntimeUri(Uri uri) { + if (uri.scheme != 'http' && uri.scheme != 'https') { + return true; + } + + final host = _normalizeHostForCompare(uri.host); + if (host.isEmpty) { + return true; + } + + if (host == 'localhost' || host.endsWith('.localhost') || host == '::1') { + return true; + } + + final ipv4Parts = _tryParseIpv4Address(host); + if (ipv4Parts == null) { + return false; + } + + return ipv4Parts[0] == 127 || ipv4Parts.every((part) => part == 0); + } + + static String _normalizeHostForCompare(String host) { + final normalized = host.trim().toLowerCase(); + if (normalized.endsWith('.')) { + return normalized.substring(0, normalized.length - 1); + } + return normalized; + } + + static List? _tryParseIpv4Address(String host) { + final parts = host.split('.'); + if (parts.length != 4) { + return null; + } + + final octets = []; + for (final part in parts) { + if (part.isEmpty || !RegExp(r'^\d+$').hasMatch(part)) { + return null; + } + final value = int.tryParse(part); + if (value == null || value < 0 || value > 255) { + return null; + } + octets.add(value); + } + return octets; + } + static String _replaceWildcardHost( String value, { String Function()? wildcardFactory, diff --git a/test/widget_test.dart b/test/widget_test.dart index eda1033..710bdd5 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -20,6 +20,30 @@ void main() { expect(Uri.parse(lines.first.url).path, '/'); }); + test('uses current non-local service origin as default H5 line', () { + final urls = AppConfig.defaultH5LineUrlsForBaseUri( + Uri.parse('https://app.example.com/shell/index.html?from=local#hash'), + ); + + expect(urls, ['https://app.example.com/']); + }); + + test('preserves current service port when building default H5 line', () { + final urls = AppConfig.defaultH5LineUrlsForBaseUri( + Uri.parse('https://chat.example.com:8443/app/'), + ); + + expect(urls, ['https://chat.example.com:8443/']); + }); + + test('keeps fixed bootstrap H5 line for local browser access', () { + final urls = AppConfig.defaultH5LineUrlsForBaseUri( + Uri.parse('http://localhost:3000/'), + ); + + expect(urls, ['https://h5-test.imharry.work/']); + }); + test('uses fixed client config query parameters', () { expect(AppConfig.clientConfigQueryPayload, { 'device': 'h5', @@ -31,6 +55,20 @@ void main() { ); }); + test('builds client config query APIs from current service origin', () { + final uris = AppConfig.clientConfigQueryUrisForHomeUri( + Uri.parse('https://app.example.com/app/index.html?from=local#hash'), + ); + + expect( + uris.map((uri) => uri.toString()), + [ + 'https://app.example.com/client_config/query', + 'https://app.example.com/api/user/client_config/query', + ], + ); + }); + test('splits client config resourceUrl into indexed H5 lines', () { final lines = AppConfig.h5LinesFromResourceUrl( 'line-one.example\nhttps://line-two.example/app\n\nline-three.example',