From 295a05d4aee531923bcafd625a5789280ba9d585 Mon Sep 17 00:00:00 2001 From: ShaoHua <345265198@qqcom> Date: Wed, 11 Mar 2026 22:25:30 +0800 Subject: [PATCH] =?UTF-8?q?WPF=E5=92=8Cweb=E6=B3=9B=E5=9E=8B=E9=80=9A?= =?UTF-8?q?=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WebView2Demo/MainWindow.xaml.cs | 12 +- WebView2Demo/Models/InteropModels.cs | 60 +++++++++ WebView2Demo/WebInterop/ApiRouteManager.cs | 123 ++++++++++++++++++ .../WebInterop/Attributes/WebApiAttributes.cs | 20 +++ WebView2Demo/WebInterop/MainSystemGateway.cs | 101 ++++++++++++++ .../Services/SystemBusinessService.cs | 51 ++++++++ web-app/src/App.vue | 102 +++++---------- web-app/src/AppLogic.js | 60 +++++++++ web-app/src/utils/interop.js | 47 +++++++ 9 files changed, 506 insertions(+), 70 deletions(-) create mode 100644 WebView2Demo/Models/InteropModels.cs create mode 100644 WebView2Demo/WebInterop/ApiRouteManager.cs create mode 100644 WebView2Demo/WebInterop/Attributes/WebApiAttributes.cs create mode 100644 WebView2Demo/WebInterop/MainSystemGateway.cs create mode 100644 WebView2Demo/WebInterop/Services/SystemBusinessService.cs create mode 100644 web-app/src/AppLogic.js create mode 100644 web-app/src/utils/interop.js diff --git a/WebView2Demo/MainWindow.xaml.cs b/WebView2Demo/MainWindow.xaml.cs index f9ac14d..421eae5 100644 --- a/WebView2Demo/MainWindow.xaml.cs +++ b/WebView2Demo/MainWindow.xaml.cs @@ -9,6 +9,7 @@ using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using Microsoft.Web.WebView2.Core; +using WebView2Demo.WebInterop; namespace WebView2Demo { @@ -17,6 +18,8 @@ namespace WebView2Demo /// public partial class MainWindow : Window { + private MainSystemGateway _gateway = new(); + public MainWindow() { InitializeComponent(); @@ -33,11 +36,14 @@ namespace WebView2Demo // 2. 注入调试开启指令 (可选) webView.CoreWebView2.Settings.AreDevToolsEnabled = true; - // 3. 监听消息 + // 3. 注入网关 Host Object (供前端调用) + webView.CoreWebView2.AddHostObjectToScript("gateway", _gateway); + + // 4. 监听消息 webView.WebMessageReceived += WebView_WebMessageReceived; - // 4. 初始化完成后再导航,确保 window.chrome.webview 注入成功 - webView.Source = new Uri("http://localhost:5173"); + // 5. 初始化完成后再导航 + webView.Source = new Uri("http://localhost:5174"); } catch (Exception ex) { diff --git a/WebView2Demo/Models/InteropModels.cs b/WebView2Demo/Models/InteropModels.cs new file mode 100644 index 0000000..2ded954 --- /dev/null +++ b/WebView2Demo/Models/InteropModels.cs @@ -0,0 +1,60 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace WebView2Demo.Models +{ + /// + /// 标准 API 请求信封 (泛型版本,方便内部使用) + /// + /// 具体的业务数据类型 + public class StandardApiRequest + { + [JsonPropertyName("action")] + public string Action { get; set; } = string.Empty; + + [JsonPropertyName("data")] + public T? Data { get; set; } + } + + /// + /// 基础请求信封 (用于初次解析 Action) + /// + public class StandardApiRequest : StandardApiRequest { } + + /// + /// 标准 API 响应信封 + /// + /// 返回的业务数据类型 + public class StandardApiResponse + { + [JsonPropertyName("success")] + public bool Success { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + [JsonPropertyName("data")] + public T? Data { get; set; } + + [JsonPropertyName("timestamp")] + public long Timestamp { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + } + + /// + /// 基础响应信封 (默认返回对象类型) + /// + public class StandardApiResponse : StandardApiResponse { } + + /// + /// 示例用的业务参数模型 + /// + public class CalculationParams + { + [JsonPropertyName("a")] + public int A { get; set; } + + [JsonPropertyName("b")] + public int B { get; set; } + } +} diff --git a/WebView2Demo/WebInterop/ApiRouteManager.cs b/WebView2Demo/WebInterop/ApiRouteManager.cs new file mode 100644 index 0000000..ac58f11 --- /dev/null +++ b/WebView2Demo/WebInterop/ApiRouteManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using WebView2Demo.WebInterop.Attributes; + +namespace WebView2Demo.WebInterop +{ + /// + /// API 路由管理器:负责扫描 [WebApiControl] 和 [WebAction] 并执行分发 + /// + public class ApiRouteManager + { + // 缓存映射: ActionName -> (Instance, MethodInfo) + private readonly Dictionary _routeTable = new(); + private readonly JsonSerializerOptions _jsonOptions; + + public ApiRouteManager(JsonSerializerOptions jsonOptions) + { + _jsonOptions = jsonOptions; + ScanAndRegisterRoutes(); + } + + private void ScanAndRegisterRoutes() + { + // 扫描当前程序集所有标记了 [WebApiControl] 的类 + var types = Assembly.GetExecutingAssembly().GetTypes() + .Where(t => t.GetCustomAttribute() != null); + + foreach (var type in types) + { + // 创建业务类实例 (生产环境建议从 DI 容器获取) + var instance = Activator.CreateInstance(type); + if (instance == null) continue; + + // 扫描所有标记了 [WebAction] 的方法 + var methods = type.GetMethods() + .Where(m => m.GetCustomAttribute() != null); + + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + // 如果没指定 ActionName,则使用方法名 + string actionName = (attr?.ActionName ?? method.Name).ToLower(); + + if (!_routeTable.TryAdd(actionName, (instance, method))) + { + throw new InvalidOperationException($"重复的 API 路由注册: {actionName}"); + } + } + } + } + + /// + /// 核心分发逻辑 + /// + public async Task<(object? Data, string Message)> DispatchActionAsync(string action, JsonElement rawData) + { + if (!_routeTable.TryGetValue(action.ToLower(), out var route)) + { + return (null, $"未找到对应的业务逻辑: {action}"); + } + + try + { + var method = route.Method; + var instance = route.Instance; + var parameters = method.GetParameters(); + object?[]? invokeArgs = null; + + // 自动参数解析 (支持泛型) + if (parameters.Length > 0) + { + var paramType = parameters[0].ParameterType; + + // 安全检查:如果 rawData 是空的或未定义的,尝试传递 null 或默认值 + if (rawData.ValueKind == JsonValueKind.Undefined || rawData.ValueKind == JsonValueKind.Null) + { + invokeArgs = new object?[] { null }; + } + else + { + // 将原始 JsonElement 转换为方法所需的参数类型 + var arg = JsonSerializer.Deserialize(rawData.GetRawText(), paramType, _jsonOptions); + invokeArgs = new[] { arg }; + } + } + + // 执行方法 + object? result = method.Invoke(instance, invokeArgs); + + // 处理 Task 返回值 + if (result is Task task) + { + await task; + // 尝试获取 Task 的结果 (Result 属性) + var resultProperty = task.GetType().GetProperty("Result"); + if (resultProperty != null) + { + return (resultProperty.GetValue(task), "操作成功"); + } + else + { + // 普通 Task,没有返回值 + return (null, "操作成功"); + } + } + + return (result, "操作成功"); + } + catch (TargetInvocationException ex) + { + return (null, $"业务执行异常: {ex.InnerException?.Message ?? ex.Message}"); + } + catch (Exception ex) + { + return (null, $"网关解析异常: {ex.Message}"); + } + } + } +} diff --git a/WebView2Demo/WebInterop/Attributes/WebApiAttributes.cs b/WebView2Demo/WebInterop/Attributes/WebApiAttributes.cs new file mode 100644 index 0000000..89c1b55 --- /dev/null +++ b/WebView2Demo/WebInterop/Attributes/WebApiAttributes.cs @@ -0,0 +1,20 @@ +using System; + +namespace WebView2Demo.WebInterop.Attributes +{ + /// + /// 标记业务类为 API 控制器 + /// + [AttributeUsage(AttributeTargets.Class)] + public class WebApiControlAttribute : Attribute { } + + /// + /// 标记方法为可被前端调用的 Action + /// + [AttributeUsage(AttributeTargets.Method)] + public class WebActionAttribute : Attribute + { + public string? ActionName { get; } + public WebActionAttribute(string? actionName = null) => ActionName = actionName; + } +} diff --git a/WebView2Demo/WebInterop/MainSystemGateway.cs b/WebView2Demo/WebInterop/MainSystemGateway.cs new file mode 100644 index 0000000..fb814ba --- /dev/null +++ b/WebView2Demo/WebInterop/MainSystemGateway.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.InteropServices; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading.Tasks; +using WebView2Demo.Models; + +namespace WebView2Demo.WebInterop +{ + /// + /// 前后端互操作主网关类 [反射分发, 统一回包] + /// + [ClassInterface(ClassInterfaceType.None)] + [ComVisible(true)] + public class MainSystemGateway + { + private readonly ApiRouteManager _routeManager; + private readonly JsonSerializerOptions _jsonOptions = new() + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + PropertyNameCaseInsensitive = true, + WriteIndented = false + }; + + public MainSystemGateway() + { + // 初始化路由管理器 + _routeManager = new ApiRouteManager(_jsonOptions); + } + + /// + /// 统一 API 调用入口 (前端唯一调用的方法) + /// 遵循 COM 规范,接收 string 并返回 string + /// + public async Task InvokeAction(string requestJson) + { + // 这里我们调用内部泛型处理逻辑,T 使用 JsonElement 作为通用基类 + return await ProcessRequestInternal(requestJson); + } + + /// + /// 内部泛型处理逻辑:在这里进行真正的协议解析和分发 + /// + /// 请求数据的泛型类型 + private async Task ProcessRequestInternal(string requestJson) + { + try + { + // 1. 解析请求信封 (进入泛型世界) + var request = JsonSerializer.Deserialize>(requestJson, _jsonOptions); + if (request == null || string.IsNullOrWhiteSpace(request.Action)) + { + return MakeErrorResponse("请求 JSON 格式错误或 Action 缺失"); + } + + // 2. 路由分发 (通过反射查找 Service 和 Method 并执行) + // 注意:由于 ApiRouteManager 内部处理了 JsonElement 的动态解析,这里我们传入 Data 即可 + JsonElement rawData = request.Data is JsonElement element ? element : default; + var (data, msg) = await _routeManager.DispatchActionAsync(request.Action, rawData); + + // 3. 根据结果统一回包 + if (data == null && msg.Contains("未找到")) + { + return MakeErrorResponse(msg); + } + + return MakeSuccessResponse(data!, msg); + } + catch (Exception ex) + { + return MakeErrorResponse($"网关内部崩溃: {ex.Message}"); + } + } + + #region [ 响应封装辅助 ] + + private string MakeSuccessResponse(object data, string msg = "操作成功") + { + var response = new StandardApiResponse + { + Success = true, + Message = msg, + Data = data + }; + return JsonSerializer.Serialize(response, _jsonOptions); + } + + private string MakeErrorResponse(string msg) + { + var response = new StandardApiResponse + { + Success = false, + Message = msg, + Data = null + }; + return JsonSerializer.Serialize(response, _jsonOptions); + } + + #endregion + } +} diff --git a/WebView2Demo/WebInterop/Services/SystemBusinessService.cs b/WebView2Demo/WebInterop/Services/SystemBusinessService.cs new file mode 100644 index 0000000..ff36682 --- /dev/null +++ b/WebView2Demo/WebInterop/Services/SystemBusinessService.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using WebView2Demo.Models; +using WebView2Demo.WebInterop.Attributes; + +namespace WebView2Demo.WebInterop.Services +{ + [WebApiControl] + public class SystemBusinessService + { + /// + /// 获取系统配置 + /// + [WebAction("GetSystemInfo")] + public object GetInfo() + { + return new + { + OS = Environment.OSVersion.ToString(), + Runtime = "WebView2 .NET 10", + IsAdmin = true + }; + } + + /// + /// 执行原生弹窗 + /// + [WebAction("ShowDialog")] + public bool ShowDialog(string message) + { + // 确保在 UI 线程执行 + Application.Current.Dispatcher.Invoke(() => + { + MessageBox.Show(message, "WPF 业务 Service 弹窗"); + }); + return true; + } + + /// + /// 异步计算演示 + /// + [WebAction("Calculate")] + public async Task CalculateAsync(CalculationParams p) + { + // 模拟业务逻辑耗时 + await Task.Delay(500); + return (p?.A ?? 0) + (p?.B ?? 0); + } + } +} diff --git a/web-app/src/App.vue b/web-app/src/App.vue index bb15dab..fb60dc7 100644 --- a/web-app/src/App.vue +++ b/web-app/src/App.vue @@ -1,84 +1,52 @@ - Vue 3 + WebView2 交互演示 + WebView2 标准化交互网关 - 接收来自 WPF 的消息: - {{ receivedMsg }} + 1. 属性同步 (WebMessage) + 来自 WPF 的推送: {{ receivedMsg }} - 发送消息给 WPF: - - 发送到 WPF + 2. 动态 API 调用 (Host Objects + 信封) + + 获取 C# 系统信息 + 触发原生弹窗 + 计算 10 + 20 + + + + 系统信息: {{ systemInfo.OS }} ({{ systemInfo.Runtime }}) + + + + 计算结果: {{ calcResult }} + diff --git a/web-app/src/AppLogic.js b/web-app/src/AppLogic.js new file mode 100644 index 0000000..a39693c --- /dev/null +++ b/web-app/src/AppLogic.js @@ -0,0 +1,60 @@ +import { ref, onMounted } from 'vue' +import { InteropHelper } from './utils/interop' + +/** + * App 页面的业务逻辑 Hook + */ +export function useAppLogic() { + const receivedMsg = ref('等待消息...') + const systemInfo = ref(null) + const calcResult = ref(null) + + // 1. 获取系统信息 (演示带返回值的标准化调用) + const fetchSystemInfo = async () => { + try { + const data = await InteropHelper.callApi('GetSystemInfo') + systemInfo.value = data + } catch (err) { + alert('获取失败: ' + err.message) + } + } + + // 2. 发送弹窗指令 (演示带参数的标准化调用) + const triggerNativeAlert = async () => { + try { + await InteropHelper.callApi('ShowDialog', '这是来自 Vue 的标准化信封消息!') + } catch (err) { + console.error('弹窗指令发送失败:', err) + } + } + + // 3. 泛型计算演示 (演示复杂对象参数) + const runCalculation = async () => { + try { + const result = await InteropHelper.callApi('Calculate', { a: 10, b: 20 }) + calcResult.value = result + } catch (err) { + console.error('计算失败:', err) + } + } + + // 原有的 WebMessage 接收逻辑 + const handleMessage = (event) => { + receivedMsg.value = event.data + } + + onMounted(() => { + if (window.chrome && window.chrome.webview) { + window.chrome.webview.addEventListener('message', handleMessage) + } + }) + + return { + receivedMsg, + systemInfo, + calcResult, + fetchSystemInfo, + triggerNativeAlert, + runCalculation + } +} diff --git a/web-app/src/utils/interop.js b/web-app/src/utils/interop.js new file mode 100644 index 0000000..4a68ff1 --- /dev/null +++ b/web-app/src/utils/interop.js @@ -0,0 +1,47 @@ +/** + * 前后端互操作工具类 + */ +export class InteropHelper { + /** + * 调用原生 API (通过 StandardApiRequest/Response 协议) + * @param {string} action 行为名称 + * @param {any} data 业务数据 + * @returns {Promise} + */ + static async callApi(action, data = null) { + if (!window.chrome?.webview?.hostObjects?.gateway) { + const errorMsg = 'WebView2 环境未就绪 (hostObjects.gateway 缺失)'; + console.error(errorMsg); + throw new Error(errorMsg); + } + + const gateway = window.chrome.webview.hostObjects.gateway; + + // 1. 封装标准信封 + const requestEnvelope = { + action: action, + data: data + }; + + try { + // 2. 调用 C# 网关方法 (使用新的方法名 InvokeAction) + const responseJson = await gateway.InvokeAction(JSON.stringify(requestEnvelope)); + console.log(`API [${action}] 原始响应:`, responseJson); + + // 3. 解析标准回包 + const response = JSON.parse(responseJson); + + if (response.success) { + return response.data; + } else { + const errorMsg = response.message || '未知错误'; + console.error(`API [${action}] 返回失败:`, errorMsg); + throw new Error(errorMsg); + } + } catch (err) { + console.error(`API 调用崩溃 [${action}]:`, err); + // 如果是在控制台看到的报错,通常是因为 C# 端发生了未捕获异常 + throw err; + } + } +}
{{ receivedMsg }}
来自 WPF 的推送: {{ receivedMsg }}