From 2eef27a0e10b3f56bce8c6ff61ecb3fea4a4bd49 Mon Sep 17 00:00:00 2001 From: Booker Date: Tue, 26 May 2026 20:09:38 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20H5=20URL=20?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=93=81=E7=89=8C=E5=8F=82=E6=95=B0=EF=BC=9B=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=BB=A5=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config/app_config.dart | 28 +-- lib/main.dart | 381 +------------------------------------ test/widget_test.dart | 22 +-- 3 files changed, 20 insertions(+), 411 deletions(-) diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index f9c9c1b..74358dd 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -21,39 +21,25 @@ class AppConfig { final host = environmentHosts.isNotEmpty ? environmentHosts.first : 'h5-im.imharry.work'; - return withFreshShellParams( - _normalizeHomeUrl(host), - appName: appName, - appLogo: appLogo, - ); + return withFreshShellParams(_normalizeHomeUrl(host)); } - static String withFreshShellParams( - String url, { - String? appName, - String? appLogo, - }) { + static String withFreshShellParams(String url) { final uri = Uri.parse(url); final queryParameters = Map.from(uri.queryParameters) ..['flutter_shell'] = '1' ..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString(); + queryParameters.remove('shell_app_name'); + queryParameters.remove('shell_app_logo'); final fragmentParameters = Uri.splitQueryString(uri.fragment); - - final trimmedName = (appName ?? AppConfig.appName).trim(); - if (trimmedName.isNotEmpty) { - queryParameters['shell_app_name'] = trimmedName; - } - - final trimmedLogo = (appLogo ?? AppConfig.appLogo).trim(); - if (trimmedLogo.isNotEmpty) { - fragmentParameters['shell_app_logo'] = trimmedLogo; - } + fragmentParameters.remove('shell_app_name'); + fragmentParameters.remove('shell_app_logo'); return uri .replace( queryParameters: queryParameters, fragment: fragmentParameters.isEmpty - ? null + ? '' : Uri(queryParameters: fragmentParameters).query, ) .toString(); diff --git a/lib/main.dart b/lib/main.dart index 180daa8..a77e474 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -16,8 +15,6 @@ const _shellBackground = Color(0xFFF8FBFF); const _shellAccent = Color(0xFF0089FF); const _shellSubText = Color(0xFF8E9AB0); const _resumeCoverDuration = Duration(milliseconds: 700); -const _urlOnlyShellMode = true; -const _urlOnlyWebViewUrl = 'https://h5-im.imharry.work/'; const _noCacheHeaders = { 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'Pragma': 'no-cache', @@ -27,87 +24,6 @@ const _shellBrandingChannel = MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); const _shellWebViewCacheChannel = MethodChannel('io.openim.flutter.im_webview_app/webview_cache'); -const _runtimeCachePurgeFallbackDuration = Duration(seconds: 2); -const _purgeWebRuntimeCacheScript = r''' -(() => { - if (window.__openimShellRuntimeCachePurgeStarted) { - return; - } - - window.__openimShellRuntimeCachePurgeStarted = true; - - const targetUrl = () => { - try { - const target = new URL(window.location.href); - target.searchParams.set('shell_cache_bust', Date.now().toString()); - target.searchParams.set('shell_runtime_cache_purged', '1'); - return target.toString(); - } catch (_) { - return window.location.href; - } - }; - - const notifyFlutter = (payload) => { - const channel = window.OpenIMFlutterShell; - if (channel && typeof channel.postMessage === 'function') { - channel.postMessage(JSON.stringify(payload)); - return true; - } - return false; - }; - - const purgeRuntimeCache = async () => { - const result = { - cacheStorageKeys: 0, - serviceWorkerRegistrations: 0, - }; - - try { - if ('caches' in window && caches.keys) { - const keys = await caches.keys(); - result.cacheStorageKeys = keys.length; - await Promise.all(keys.map((key) => caches.delete(key))); - } - } catch (_) {} - - try { - if ( - navigator.serviceWorker && - typeof navigator.serviceWorker.getRegistrations === 'function' - ) { - const registrations = await navigator.serviceWorker.getRegistrations(); - result.serviceWorkerRegistrations = registrations.length; - await Promise.all( - registrations.map((registration) => registration.unregister()) - ); - } - } catch (_) {} - - return result; - }; - - purgeRuntimeCache() - .then((result) => { - const payload = { - type: 'runtimeCachePurged', - url: targetUrl(), - result, - }; - if (!notifyFlutter(payload)) { - window.location.replace(payload.url); - } - }) - .catch(() => { - const payload = { - type: 'runtimeCachePurged', - url: targetUrl(), - }; - if (!notifyFlutter(payload)) { - window.location.replace(payload.url); - } - }); -})(); -'''; const _inspectH5SnapshotScript = r''' (() => { const toAbsoluteUrl = (value) => { @@ -265,10 +181,6 @@ Future main() async { systemNavigationBarIconBrightness: Brightness.dark, ), ); - if (_urlOnlyShellMode) { - runApp(const UrlOnlyWebViewApp()); - return; - } final shellBranding = await ShellBranding.load(); runApp(ImWebViewApp(shellBranding: shellBranding)); } @@ -306,121 +218,6 @@ class ShellBranding { static String _trim(String? value) => value?.trim() ?? ''; } -class UrlOnlyWebViewApp extends StatelessWidget { - const UrlOnlyWebViewApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: _urlOnlyWebViewUrl, - debugShowCheckedModeBanner: false, - theme: ThemeData(scaffoldBackgroundColor: Colors.white), - home: const UrlOnlyWebViewPage(), - ); - } -} - -class UrlOnlyWebViewPage extends StatefulWidget { - const UrlOnlyWebViewPage({super.key}); - - @override - State createState() => _UrlOnlyWebViewPageState(); -} - -class _UrlOnlyWebViewPageState extends State { - late final WebViewController _controller; - - int _progress = 0; - String? _loadError; - - @override - void initState() { - super.initState(); - _controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Colors.white) - ..setNavigationDelegate( - NavigationDelegate( - onProgress: (progress) { - if (mounted) { - setState(() => _progress = progress); - } - }, - onPageStarted: (_) { - if (mounted) { - setState(() { - _loadError = null; - _progress = 0; - }); - } - }, - onPageFinished: (url) { - debugPrint('[UrlOnlyWebView] finished: $url'); - if (mounted) { - setState(() => _progress = 100); - } - }, - onWebResourceError: (error) { - if (error.isForMainFrame ?? true) { - if (mounted) { - setState(() => _loadError = error.description); - } - } - }, - onNavigationRequest: _handleUrlOnlyNavigationRequest, - ), - ) - ..loadRequest(Uri.parse(_urlOnlyWebViewUrl)); - } - - Future _handleUrlOnlyNavigationRequest( - NavigationRequest request, - ) async { - final uri = Uri.tryParse(request.url); - if (uri == null) { - return NavigationDecision.prevent; - } - - const webSchemes = {'http', 'https', 'about', 'data'}; - if (webSchemes.contains(uri.scheme)) { - return NavigationDecision.navigate; - } - - try { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } catch (_) {} - return NavigationDecision.prevent; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - bottom: false, - child: Stack( - children: [ - WebViewWidget(controller: _controller), - if (_progress < 100) - LinearProgressIndicator( - value: _progress == 0 ? null : _progress / 100, - minHeight: 2, - ), - if (_loadError != null) - _ErrorPanel( - message: _loadError!, - onRetry: () { - setState(() => _loadError = null); - _controller.loadRequest(Uri.parse(_urlOnlyWebViewUrl)); - }, - ), - ], - ), - ), - ); - } -} - class ImWebViewApp extends StatelessWidget { const ImWebViewApp({ super.key, @@ -459,11 +256,7 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { int _progress = 0; String? _loadError; bool _showShellCover = true; - bool _runtimeCachePurgeStarted = false; - bool _runtimeCachePurgeReloaded = false; - String? _h5SnapshotDebugText; Timer? _shellCoverTimer; - Timer? _runtimeCachePurgeFallbackTimer; @override void initState() { @@ -476,7 +269,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { @override void dispose() { _shellCoverTimer?.cancel(); - _runtimeCachePurgeFallbackTimer?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -596,10 +388,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { if (type == 'openAppSettings') { unawaited(openAppSettings()); } - if (type == 'runtimeCachePurged' && payload != null) { - debugPrint('[H5Shell] runtime cache purged: ${jsonEncode(payload)}'); - unawaited(_reloadAfterRuntimeCachePurge(payload['url'] as String?)); - } if (type == 'requestMediaPermissions' && payload != null) { unawaited(_handleMediaPermissionBridgeRequest(payload)); } @@ -665,37 +453,11 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } String _freshHomeUrl() { - return AppConfig.homeUrl( - appName: widget.shellBranding.appName, - appLogo: widget.shellBranding.appLogo, - ); - } - - String _freshShellUrl(String? url) { - final value = url?.trim(); - if (value == null || value.isEmpty) { - return _freshHomeUrl(); - } - - try { - return AppConfig.withFreshShellParams( - value, - appName: widget.shellBranding.appName, - appLogo: widget.shellBranding.appLogo, - ); - } catch (_) { - return _freshHomeUrl(); - } + return AppConfig.homeUrl(); } Future _handlePageFinished() async { - final waitingForRuntimeCacheReload = - await _purgeWebRuntimeCacheAndReloadIfNeeded(); - if (waitingForRuntimeCacheReload) { - return; - } - - unawaited(_syncShellBranding()); + await _syncShellBranding(); unawaited(_injectShellMediaPermissionBridge()); unawaited(_logLoadedH5Snapshot()); if (mounted) { @@ -706,56 +468,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } } - Future _purgeWebRuntimeCacheAndReloadIfNeeded() async { - if (_runtimeCachePurgeStarted) { - return false; - } - - _runtimeCachePurgeStarted = true; - try { - await _controller.runJavaScript(_purgeWebRuntimeCacheScript); - } catch (_) { - _runtimeCachePurgeStarted = false; - return false; - } - - _runtimeCachePurgeFallbackTimer?.cancel(); - _runtimeCachePurgeFallbackTimer = Timer( - _runtimeCachePurgeFallbackDuration, - () { - if (!_runtimeCachePurgeReloaded) { - unawaited(_reloadAfterRuntimeCachePurge(null)); - } - }, - ); - return true; - } - - Future _reloadAfterRuntimeCachePurge(String? url) async { - if (_runtimeCachePurgeReloaded) { - return; - } - - _runtimeCachePurgeReloaded = true; - _runtimeCachePurgeFallbackTimer?.cancel(); - if (mounted) { - setState(() { - _loadError = null; - _progress = 0; - _showShellCover = true; - }); - } - - String? currentUrl; - if (url == null || url.trim().isEmpty) { - try { - currentUrl = await _controller.currentUrl(); - } catch (_) {} - } - - await _loadUrl(_freshShellUrl(url ?? currentUrl)); - } - Future _prepareWebViewForFreshLoad() async { await _configureAndroidNoCache(); try { @@ -815,61 +527,11 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { debugPrint( '[H5Shell] loaded H5 snapshot: $snapshot', ); - _updateH5SnapshotDebugText(snapshot); } catch (error) { debugPrint('[H5Shell] loaded H5 snapshot failed: $error'); } } - void _updateH5SnapshotDebugText(String snapshot) { - if (!kDebugMode || !mounted) { - return; - } - - setState(() { - _h5SnapshotDebugText = _formatH5SnapshotDebugText(snapshot); - }); - } - - String _formatH5SnapshotDebugText(String snapshot) { - try { - final decoded = jsonDecode(snapshot); - if (decoded is! Map) { - return snapshot; - } - - final href = (decoded['href'] as String? ?? '').trim(); - final title = (decoded['title'] as String? ?? '').trim(); - final bodyText = (decoded['bodyText'] as String? ?? '').trim(); - final scripts = decoded['scripts']; - final links = decoded['links']; - final assets = [ - if (scripts is List) - ...scripts.whereType().where((value) => value.isNotEmpty), - if (links is List) - ...links.whereType().where((value) => value.isNotEmpty), - ]; - final importantAssets = assets - .where( - (asset) => - asset.contains('/assets/index-') || - asset.contains('LoginBrandHeader') || - asset.contains('useLoginBranding'), - ) - .take(8) - .join('\n'); - - return [ - if (title.isNotEmpty) 'title: $title', - if (href.isNotEmpty) 'url: $href', - if (importantAssets.isNotEmpty) 'assets:\n$importantAssets', - if (bodyText.isNotEmpty) 'text: $bodyText', - ].join('\n'); - } catch (_) { - return snapshot; - } - } - String _decodeJavaScriptStringResult(Object? result) { if (result == null) { return ''; @@ -1038,15 +700,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { message: _loadError!, onRetry: () => unawaited(_loadHome()), ), - if (kDebugMode && _h5SnapshotDebugText != null) - Positioned( - left: 8, - right: 8, - bottom: 12, - child: IgnorePointer( - child: _H5SnapshotDebugPanel(text: _h5SnapshotDebugText!), - ), - ), ], ), ), @@ -1055,36 +708,6 @@ class _H5ShellPageState extends State with WidgetsBindingObserver { } } -class _H5SnapshotDebugPanel extends StatelessWidget { - const _H5SnapshotDebugPanel({required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.72), - borderRadius: BorderRadius.circular(8), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - text, - maxLines: 10, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - height: 1.25, - letterSpacing: 0, - ), - ), - ), - ); - } -} - class _ErrorPanel extends StatelessWidget { const _ErrorPanel({required this.message, required this.onRetry}); diff --git a/test/widget_test.dart b/test/widget_test.dart index de0dab6..f84afba 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -11,7 +11,7 @@ void main() { ); }); - test('loads the configured H5 root URL with shell branding', () { + test('loads the configured H5 root URL with shell launch params', () { const logo = 'data:image/png;base64,abc+/='; final uri = Uri.parse( @@ -22,29 +22,29 @@ void main() { expect(uri.host, 'h5-im.imharry.work'); expect(uri.path, '/'); expect(uri.queryParameters['flutter_shell'], '1'); - expect(uri.queryParameters['shell_app_name'], 'Shell Test'); + expect(uri.queryParameters.containsKey('shell_app_name'), isFalse); expect(uri.queryParameters.containsKey('shell_app_logo'), isFalse); expect(uri.queryParameters['shell_cache_bust'], isNotEmpty); expect(uri.toString(), isNot(contains('%25'))); - expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo); + expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'), + isFalse); }); - test('keeps shell branding when refreshing an H5 route URL', () { - const logo = 'data:image/png;base64,routeLogo+/='; - + test('refreshes an H5 route URL without adding branding to the URL', () { final uri = Uri.parse( AppConfig.withFreshShellParams( - 'https://h5-im.imharry.work/login?from=runtime', - appName: 'Shell Route', - appLogo: logo, + 'https://h5-im.imharry.work/login?from=runtime&shell_app_name=Old' + '#shell_app_logo=old', ), ); expect(uri.path, '/login'); expect(uri.queryParameters['from'], 'runtime'); expect(uri.queryParameters['flutter_shell'], '1'); - expect(uri.queryParameters['shell_app_name'], 'Shell Route'); + expect(uri.queryParameters.containsKey('shell_app_name'), isFalse); + expect(uri.queryParameters.containsKey('shell_app_logo'), isFalse); expect(uri.queryParameters['shell_cache_bust'], isNotEmpty); - expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo); + expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'), + isFalse); }); }