using Avalonia.Controls; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Avalonia_PC.Views { /// /// 主窗口,承载 WebView2 控件并管理前后端 Bridge 通信。 /// public partial class MainWindow : Window { /// /// 自定义协议方案名称。 /// private const string AppScheme = "app"; /// /// 在线模式下的前端启动 URL。 /// private const string? OnlineStartupUrl = "http://localhost:51240"; //private const string? OnlineStartupUrl = null; /// /// 离线模式下的前端本地文件路径,为空则使用在线模式。 /// private const string? LocalStartupPath = null; private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; /// /// WebView2 原生控件实例。 /// private NativeWebView? _webView; /// /// 标记 WebView 事件是否已绑定。 /// private bool _eventsAttached; /// /// WebView 适配器对象。 /// private object? _webViewAdapter; /// /// 本地 HTTP 服务器实例(离线模式)。 /// private HttpListener? _localHttpServer; /// /// 本地 HTTP 服务器的取消令牌源。 /// private CancellationTokenSource? _localHttpServerCts; /// /// 本地 HTTP 服务器的基础 URL。 /// private string? _localHttpBaseUrl; /// /// 本地 HTTP 服务器的根目录路径。 /// private string? _localHttpRoot; #region 生命周期与 WebView 事件 /// /// 初始化窗口并注册生命周期事件。 /// public MainWindow(IServiceProvider services) { _services = services; InitializeComponent(); Opened += OnOpened; Closed += OnClosed; RegisterRoutes(); } /// /// 窗口打开后初始化 WebView、挂载事件并加载入口页面。 /// private async void OnOpened(object? sender, EventArgs e) { if (_eventsAttached) { return; } _webView = this.FindControl("WebView"); if (_webView is null) { return; } _eventsAttached = true; _webView.NavigationCompleted += OnNavigationCompleted; _webView.WebMessageReceived += OnWebMessageReceived; _webView.AdapterCreated += OnAdapterCreated; await LoadInitialContentAsync(); } /// /// WebView 适配器创建后缓存实例,用于后续打开开发者工具。 /// private void OnAdapterCreated(object? sender, WebViewAdapterEventArgs e) { _webViewAdapter = e.GetType().GetProperty("Adapter")?.GetValue(e); } /// /// 窗口关闭时解绑事件并释放本地资源。 /// private void OnClosed(object? sender, EventArgs e) { if (_webView is not null) { _webView.NavigationCompleted -= OnNavigationCompleted; _webView.WebMessageReceived -= OnWebMessageReceived; _webView.AdapterCreated -= OnAdapterCreated; } _webViewAdapter = null; StopLocalHttpServer(); } /// /// 页面导航完成后注入 JS 桥接脚本。 /// private async void OnNavigationCompleted(object? sender, WebViewNavigationCompletedEventArgs e) { await InjectBridgeScriptAsync(); } #endregion #region 前端桥接与页面加载 /// /// 接收前端消息并进行分发(打开调试工具 / 处理 app 请求)。 /// private async void OnWebMessageReceived(object? sender, WebMessageReceivedEventArgs e) { var messageJson = e.Body; if (string.IsNullOrWhiteSpace(messageJson)) { return; } AppResponse? response = null; try { using var document = JsonDocument.Parse(messageJson); var root = document.RootElement; if (!root.TryGetProperty("kind", out var kindProperty)) { return; } var kind = kindProperty.GetString(); if (string.Equals(kind, "app-open-devtools", StringComparison.OrdinalIgnoreCase)) { TryOpenDevTools(); return; } if (!string.Equals(kind, "app-request", StringComparison.OrdinalIgnoreCase)) { return; } response = await HandleAppRequestAsync(root); } catch (Exception ex) { response = new AppResponse { Kind = "app-response", Id = TryGetRequestId(messageJson), StatusCode = 500, StatusMessage = "Internal Server Error", Body = JsonSerializer.Serialize(new { success = false, error = ex.Message }), Headers = CreateJsonHeaders(), }; } if (_webView is not null && response is not null) { var responseJson = JsonSerializer.Serialize(response, BridgeJsonSerializerOptions); var responseJsonLiteral = JsonSerializer.Serialize(responseJson); await _webView.InvokeScript($"window.__dispatchAppResponse({responseJsonLiteral})"); } } /// /// 加载初始页面:优先在线地址,其次本地路径(通过本地 HTTP 服务托管)。 /// private async Task LoadInitialContentAsync() { if (_webView is null) { return; } var onlineUrl = GetConfiguredOnlineStartupUrl(); if (onlineUrl is not null) { StopLocalHttpServer(); _webView.Source = onlineUrl; return; } var localHtmlPath = GetConfiguredLocalStartupPath(); if (string.IsNullOrWhiteSpace(localHtmlPath) || !File.Exists(localHtmlPath)) { return; } var localRoot = Path.GetDirectoryName(localHtmlPath); if (string.IsNullOrWhiteSpace(localRoot)) { return; } await EnsureLocalHttpServerStartedAsync(localRoot); if (string.IsNullOrWhiteSpace(_localHttpBaseUrl)) { _webView.Source = new Uri(localHtmlPath); return; } _webView.Source = new Uri(new Uri(_localHttpBaseUrl), Path.GetFileName(localHtmlPath)); } /// /// 向页面注入桥接脚本,接管 app:// 请求并回传到 C# 处理。 /// private async Task InjectBridgeScriptAsync() { if (_webView is null) { return; } await _webView.InvokeScript(BridgeScript); } #endregion #region 请求分发与通用响应 /// /// 解析前端请求消息并转发到统一请求处理入口。 /// private async Task HandleAppRequestAsync(JsonElement request) { var id = request.TryGetProperty("id", out var idProperty) ? idProperty.GetString() : null; var url = request.TryGetProperty("url", out var urlProperty) ? urlProperty.GetString() : null; var method = request.TryGetProperty("method", out var methodProperty) ? methodProperty.GetString() : "GET"; var body = request.TryGetProperty("body", out var bodyProperty) ? bodyProperty.GetString() : null; var headers = ExtractHeaders(request); return await HandleAppRequestAsync(id, url, method, body, headers); } /// /// 统一请求处理:构建上下文、处理 OPTIONS、使用统一端点适配器分发。 /// private async Task HandleAppRequestAsync( string? id, string? rawUrl, string? method, string? body, Dictionary headers) { var response = new AppResponse { Kind = "app-response", Id = id, StatusCode = 200, StatusMessage = "OK", Headers = CreateJsonHeaders(), }; try { var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。")); if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = 200; response.StatusMessage = "OK"; response.Body = JsonSerializer.Serialize(new { success = true }); return response; } // 使用统一端点适配器处理请求 var (normalizedPath, queryParams) = ParseRequestUri(uri); var routeResult = await _endpointAdapter.HandleRequestAsync( path: normalizedPath, method: method ?? "GET", body: body, headers: headers, query: queryParams); if (routeResult.IsMatched) { response.StatusCode = routeResult.StatusCode; response.StatusMessage = routeResult.StatusMessage; response.Body = BuildSuccessResponseBody(routeResult.Data); foreach (var kvp in routeResult.ResponseHeaders) { response.Headers[kvp.Key] = kvp.Value; } return response; } response.StatusCode = 404; response.StatusMessage = "Not Found"; response.Body = JsonSerializer.Serialize(new { success = false, error = "API not found" }); return response; } catch (Exception ex) { response.StatusCode = 500; response.StatusMessage = "Internal Server Error"; response.Body = JsonSerializer.Serialize(new { success = false, error = ex.Message }); return response; } } /// /// 从 URI 解析规范化路径和查询参数(供统一端点适配器使用)。 /// private static (string normalizedPath, Dictionary query) ParseRequestUri(Uri uri) { var host = uri.Host ?? string.Empty; var absolutePath = uri.AbsolutePath ?? string.Empty; var combinedPath = $"{host}/{absolutePath}"; var pathSegments = combinedPath .Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(Uri.UnescapeDataString) .ToArray(); var normalizedPath = string.Join('/', pathSegments); var query = ParseQueryParameters(uri.Query); return (normalizedPath, query); } /// /// 统一构建成功响应体,保持前后端响应结构一致。 /// private static string BuildSuccessResponseBody(object? data) { return JsonSerializer.Serialize(new { success = true, data }); } /// /// 解析查询字符串为忽略大小写的字典。 /// private static Dictionary ParseQueryParameters(string? queryString) { var query = new Dictionary(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(queryString)) { return query; } var raw = queryString.TrimStart('?'); foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) { var separatorIndex = pair.IndexOf('='); if (separatorIndex < 0) { query[Uri.UnescapeDataString(pair)] = string.Empty; continue; } var key = Uri.UnescapeDataString(pair[..separatorIndex]); var value = Uri.UnescapeDataString(pair[(separatorIndex + 1)..]); query[key] = value; } return query; } /// /// 创建桥接响应的默认 JSON/CORS 头。 /// private static Dictionary CreateJsonHeaders() => new() { ["Content-Type"] = "application/json; charset=utf-8", ["Access-Control-Allow-Origin"] = "*", ["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS", ["Access-Control-Allow-Headers"] = "Content-Type, Authorization", }; /// /// 从前端请求消息中提取请求头。 /// private static Dictionary ExtractHeaders(JsonElement request) { if (!request.TryGetProperty("headers", out var headersElement) || headersElement.ValueKind != JsonValueKind.Object) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var property in headersElement.EnumerateObject()) { headers[property.Name] = property.Value.GetString() ?? string.Empty; } return headers; } /// /// 获取授权头,供鉴权逻辑扩展使用。 /// private static string? GetAuthorizationHeader(Dictionary headers) { return headers.FirstOrDefault( entry => string.Equals(entry.Key, "Authorization", StringComparison.OrdinalIgnoreCase)).Value; } /// /// 在异常情况下尝试提取请求 id,确保前端可收到对应错误响应。 /// private static string? TryGetRequestId(string messageJson) { try { using var document = JsonDocument.Parse(messageJson); return document.RootElement.TryGetProperty("id", out var idProperty) ? idProperty.GetString() : null; } catch { return null; } } #endregion #region 页面地址配置与本地静态服务 /// /// 获取在线启动地址配置(仅允许 http/https)。 /// private static Uri? GetConfiguredOnlineStartupUrl() { if (string.IsNullOrWhiteSpace(OnlineStartupUrl)) { return null; } if (!Uri.TryCreate(OnlineStartupUrl, UriKind.Absolute, out var uri)) { return null; } return uri.Scheme is "http" or "https" ? uri : null; } /// /// 获取本地启动文件路径,未配置时默认使用输出目录 www/index.html。 /// private static string? GetConfiguredLocalStartupPath() { if (!string.IsNullOrWhiteSpace(LocalStartupPath)) { return Path.GetFullPath(LocalStartupPath); } return Path.Combine(AppContext.BaseDirectory, "www", "index.html"); } /// /// 确保本地 HTTP 静态服务已启动;根目录变化时会重启。 /// private async Task EnsureLocalHttpServerStartedAsync(string localRoot) { if (!string.IsNullOrWhiteSpace(_localHttpBaseUrl) && _localHttpServer is not null && string.Equals(_localHttpRoot, localRoot, StringComparison.OrdinalIgnoreCase)) { return; } StopLocalHttpServer(); var port = GetAvailableTcpPort(); var prefix = $"http://127.0.0.1:{port}/"; _localHttpServerCts = new CancellationTokenSource(); _localHttpServer = new HttpListener(); _localHttpServer.Prefixes.Add(prefix); _localHttpServer.Start(); _localHttpBaseUrl = prefix; _localHttpRoot = localRoot; _ = Task.Run(() => RunLocalHttpServerLoopAsync(_localHttpServer, _localHttpServerCts.Token, localRoot)); } /// /// 本地静态服务主循环,持续接收并分发请求。 /// private static async Task RunLocalHttpServerLoopAsync(HttpListener listener, CancellationToken cancellationToken, string wwwRoot) { try { while (!cancellationToken.IsCancellationRequested) { var context = await listener.GetContextAsync(); _ = Task.Run(() => HandleLocalHttpRequest(context, wwwRoot), cancellationToken); } } catch { } } /// /// 处理本地静态资源请求并返回文件内容。 /// private static async Task HandleLocalHttpRequest(HttpListenerContext context, string wwwRoot) { try { var relativePath = context.Request.Url?.AbsolutePath.TrimStart('/') ?? string.Empty; if (string.IsNullOrWhiteSpace(relativePath)) { relativePath = "index.html"; } relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar); var fullPath = Path.GetFullPath(Path.Combine(wwwRoot, relativePath)); var fullRoot = Path.GetFullPath(wwwRoot); if (!fullPath.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase) || !File.Exists(fullPath)) { context.Response.StatusCode = 404; context.Response.Close(); return; } context.Response.ContentType = GetContentType(fullPath); await using var input = File.OpenRead(fullPath); context.Response.ContentLength64 = input.Length; await input.CopyToAsync(context.Response.OutputStream); context.Response.OutputStream.Close(); } catch { try { context.Response.StatusCode = 500; context.Response.Close(); } catch { } } } /// /// 根据后缀返回静态资源 Content-Type。 /// private static string GetContentType(string filePath) { return Path.GetExtension(filePath).ToLowerInvariant() switch { ".html" => "text/html; charset=utf-8", ".js" => "application/javascript; charset=utf-8", ".css" => "text/css; charset=utf-8", ".json" => "application/json; charset=utf-8", _ => "application/octet-stream", }; } /// /// 获取一个可用本地端口,用于启动本地静态服务。 /// private static int GetAvailableTcpPort() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); var port = ((IPEndPoint)listener.LocalEndpoint).Port; listener.Stop(); return port; } /// /// 尝试打开 WebView 开发者工具(兼容不同适配器方法名)。 /// private void TryOpenDevTools() { if (_webViewAdapter is null) { return; } var adapterType = _webViewAdapter.GetType(); var method = adapterType.GetMethod("OpenDevTools", BindingFlags.Public | BindingFlags.Instance) ?? adapterType.GetMethod("ShowDevTools", BindingFlags.Public | BindingFlags.Instance); method?.Invoke(_webViewAdapter, null); } /// /// 停止并释放本地静态服务资源。 /// private void StopLocalHttpServer() { try { _localHttpServerCts?.Cancel(); _localHttpServer?.Stop(); _localHttpServer?.Close(); } catch { } finally { _localHttpServerCts?.Dispose(); _localHttpServerCts = null; _localHttpServer = null; _localHttpBaseUrl = null; _localHttpRoot = null; } } #endregion #region DTO / 路由上下文模型 /// /// Bridge 通信响应 DTO,用于序列化返回给前端的数据。 /// private sealed class AppResponse { /// /// 获取或设置响应类型标识。 /// public string Kind { get; set; } = string.Empty; /// /// 获取或设置请求 ID(对应前端请求)。 /// public string? Id { get; set; } /// /// 获取或设置 HTTP 状态码。 /// public int StatusCode { get; set; } /// /// 获取或设置状态描述文本。 /// public string StatusMessage { get; set; } = string.Empty; /// /// 获取或设置响应体 JSON 字符串。 /// public string Body { get; set; } = string.Empty; /// /// 获取或设置响应头字典。 /// public Dictionary Headers { get; set; } = new(); } #endregion } }