另辟新径实现 Blazor/MAUI 本机交互(一)

news/2025/2/11 18:35:53/文章来源:https://www.cnblogs.com/densen2014/p/18638327

本系列由浅入深逐个文件解析工作原理

目录:

  1. WebViewNativeApi.cs
  2. NativeApi.cs
  3. MainPage.xaml.cs
  4. 实战
  5. 串口
  6. 小票机
  7. 蓝牙

WebViewNativeApi.cs

WebViewNativeApi.cs 文件中的代码实现了一个 NativeBridge 类,用于在 .NET MAUI 应用程序中的 WebView 和本地代码之间进行通信。以下是该代码的工作原理说明:

类和字段

  • NativeBridge 类:主要负责在 WebView 和本地代码之间建立桥梁。
  • DEFAULT_SCHEME:默认的 URL scheme,用于识别本地调用。
  • INTERFACE_JS:JavaScript 代码,用于在 WebView 中创建一个代理对象,通过该对象可以调用本地方法。
  • _webView:WebView 控件的引用。
  • _targets:存储目标对象及其名称和 scheme。
  • _isInit:标识 WebView 是否已初始化。
  • _query:存储当前的查询信息。
  • lastDomain:存储上一次导航的域名。
  • TargetJS:存储要注入的目标 JavaScript 代码。
    构造函数
  • NativeBridge(WebView? wv):构造函数,初始化 WebView 并注册导航事件。
    方法
  • AddTarget(string name, object obj, string sheme = DEFAULT_SCHEME):添加目标对象及其名称和 scheme。
  • OnWebViewInit(object? sender, WebNavigatedEventArgs e):在 WebView 导航完成后调用,注入 JavaScript 代码并初始化目标对象。
  • OnWebViewNavigatin(object? sender, WebNavigatingEventArgs e):在 WebView 导航时调用,处理本地调用请求。
  • AddTargetToWebView(string name, object obj, string sheme):将目标对象的方法和属性注入到 WebView 中。
  • IsAsyncMethod(MethodInfo method):判断方法是否为异步方法。
  • RunCommand(string name, string token, string prop, object obj):执行本地方法或属性访问,并将结果返回给 WebView。
  • sendEvent(string type, Dictionary<string, string>? detail = null, bool optBubbles = false, bool optCancelable = false, bool optComposed = false):发送自定义事件到 WebView。
  • RunJS(string code):在 WebView 中执行 JavaScript 代码。

工作流程

  1. 初始化:在构造函数中,注册 WebView 的导航事件。
  2. 添加目标对象:通过 AddTarget 方法添加目标对象及其名称和 scheme。
  3. WebView 导航完成:在 OnWebViewInit 方法中,注入 JavaScript 代码并初始化目标对象。
  4. 处理本地调用请求:在 OnWebViewNavigatin 方法中,解析 URL 并执行相应的本地方法或属性访问。
  5. 执行本地方法:在 RunCommand 方法中,调用目标对象的方法或属性,并将结果返回给 WebView。

通过这种方式,NativeBridge 类实现了在 .NET MAUI 应用程序中的 WebView 和本地代码之间的双向通信。

完整代码

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.RegularExpressions;namespace WebViewNativeApi;/// <summary>
/// NativeBridge 类, 用于在 .NET MAUI 应用程序中的 WebView 和本地代码之间进行通信, 主要负责在 WebView 和本地代码之间建立桥梁
/// </summary>
public class NativeBridge
{/// <summary>/// 默认的 URL scheme,用于识别本地调用/// </summary>private const string DEFAULT_SCHEME = "native://";/// <summary>/// JavaScript 代码,用于在 WebView 中创建一个代理对象,通过该对象可以调用本地方法/// </summary>private const string INTERFACE_JS = "window['createNativeBridgeProxy'] = " +"(name, methods, properties, scheme = '" + DEFAULT_SCHEME + "') =>" +"{" +"    let apiCalls = new Map();" +"" +"    function randomUUID() {" +"       return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>" +"               (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));" +"    }" +"" +"    function createRequest(target, success, reject, argumentsList) {" +"        let uuid = randomUUID();" +"        while(apiCalls.has(uuid)) { uuid = randomUUID(); };" +"        apiCalls.set(uuid, { 'success': success, 'reject': reject, 'arguments': argumentsList });" +"        location.href = scheme + name + '/' + target + '/' + uuid + '/';" +"    }" +"" +"    return new Proxy({" +"            getArguments : (token) => {" +"                return apiCalls.get(token).arguments;" +"            }," +"            returnValue : (token, value) => {" +"                let ret = value;" +"                try { ret = JSON.parse(ret); } catch(e) { };" +"                let callback = apiCalls.get(token).success;" +"                if (callback && typeof callback === 'function')" +"                    callback(ret);" +"                apiCalls.delete(token);" +"            }," +"            rejectCall : (token, error) => {" +"                let callback = apiCalls.get(token).reject;" +"                if (callback && typeof callback === 'function')" +"                    callback(error);" +"                apiCalls.delete(token);" +"            }" +"        }," +"        {" +"            get: (target, prop, receiver) => {" +"                if (methods.includes(prop)) {" +"                    return new Proxy(() => {}, {" +"                        apply: (target, thisArg, argumentsList) => {" +"                            return new Promise((success, reject) => {" +"                                    createRequest(prop, success, reject, argumentsList);" +"                                });" +"                        }" +"                    });" +"                }" +"                if (!properties.includes(prop)) {" +"                    return Reflect.get(target, prop, receiver);" +"                }" +"                return new Promise((success, reject) => {" +"                        createRequest(prop, success, reject, []);" +"                    });" +"            }," +"            set: (target, prop, value) => {" +"                return new Promise((success, reject) => {" +"                        createRequest(prop, success, reject, [value]);" +"                    });" +"            }" +"        });" +"};";/// <summary>/// WebView 控件的引用/// </summary>private readonly WebView? _webView = null;/// <summary>/// 用于存储本地对象的字典,存储目标对象及其名称和 scheme/// </summary>private readonly Dictionary<(string, string), object> _targets = [];/// <summary>/// 是否已经初始化/// </summary>private bool _isInit = false;/// <summary>/// 存储当前的查询信息/// </summary>private (string?, string?, string?, object?) _query = ("", "", "", null);/// <summary>/// 存储上一次导航的域名/// </summary>private string? lastDomain;/// <summary>/// 存储要注入的目标 JavaScript 代码/// </summary>public string? TargetJS;/// <summary>/// 构造函数,初始化 WebView 并注册导航事件/// </summary>/// <param name="wv"></param>public NativeBridge(WebView? wv){if (wv != null){_webView = wv;_webView.Navigated += OnWebViewInit;_webView.Navigating += OnWebViewNavigatin;}}/// <summary>/// 添加目标对象及其名称和 scheme/// </summary>/// <param name="name"></param>/// <param name="obj"></param>/// <param name="sheme"></param>public void AddTarget(string name, object obj, string sheme = DEFAULT_SCHEME){if (obj == null){return;}_targets.Add((name, sheme), obj);if (_isInit){AddTargetToWebView(name, obj, sheme);}}/// <summary>/// WebView 初始化事件处理程序,在 WebView 导航完成后调用,注入 JavaScript 代码并初始化目标对象。/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private async void OnWebViewInit(object? sender, WebNavigatedEventArgs e){var currentDomain = new Uri(e.Url).Host;if (lastDomain != currentDomain){_isInit = false;lastDomain = currentDomain;}else{var isInjected = await RunJS("window.dialogs !== undefined");if (isInjected == "false"){_isInit = false;}}if (!_isInit){_ = await RunJS(INTERFACE_JS);if (TargetJS != null){_ = await RunJS(TargetJS);}foreach (KeyValuePair<(string, string), object> entry in _targets){AddTargetToWebView(entry.Key.Item1, entry.Value, entry.Key.Item2);}_isInit = true;}}/// <summary>/// WebView 导航事件处理程序,在 WebView 导航时调用,根据 URL 判断是否调用本地方法。/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void OnWebViewNavigatin(object? sender, WebNavigatingEventArgs e){if (!_isInit){return;}foreach (KeyValuePair<(string, string), object> entry in _targets){var startStr = entry.Key.Item2 + entry.Key.Item1;if (!e.Url.StartsWith(startStr)){continue;}var request = e.Url[(e.Url.IndexOf(startStr) + startStr.Length)..].ToLower();request = request.Trim(['/', '\\']);var requestArgs = request.Split('/');if (requestArgs.Length < 2){return;}e.Cancel = true;var prop = requestArgs[0];var token = requestArgs[1];Type type = entry.Value.GetType();if (type.GetMember(prop) == null){RunJS("window." + entry.Key.Item1 + ".rejectCall('" + token + "', 'Member not found!');");return;}_query = (entry.Key.Item1, token, prop, entry.Value);Task.Run(() =>{RunCommand(_query.Item1, _query.Item2, _query.Item3, _query.Item4);_query = ("", "", "", null);});return;}}/// <summary>/// 将目标对象的方法和属性注入到 WebView 中。/// </summary>/// <param name="name"></param>/// <param name="obj"></param>/// <param name="sheme"></param>private void AddTargetToWebView(string name, object obj, string sheme){var type = obj.GetType();var methods = new List<string>();var properties = new List<string>();foreach (MethodInfo method in type.GetMethods()){methods.Add(method.Name);}foreach (PropertyInfo p in type.GetProperties()){properties.Add(p.Name);}RunJS("window." + name + " = window.createNativeBridgeProxy('" + name + "', " + JsonSerializer.Serialize(methods) + ", " +JsonSerializer.Serialize(properties) + ", '" + sheme + "');");}/// <summary>/// 判断方法是否为异步方法/// </summary>/// <param name="method"></param>/// <returns></returns>private static bool IsAsyncMethod(MethodInfo method){var attType = typeof(AsyncStateMachineAttribute);var attrib = (AsyncStateMachineAttribute?)method.GetCustomAttribute(attType);return (attrib != null);}/// <summary>/// 调用本地方法,执行本地方法或属性访问,并将结果返回给 WebView/// </summary>/// <param name="name"></param>/// <param name="token"></param>/// <param name="prop"></param>/// <param name="obj"></param>private async void RunCommand(string name, string token, string prop, object obj){try{var type = obj.GetType();var readArguments = await RunJS("window." + name + ".getArguments('" + token + "');");var jsonObjects = JsonSerializer.Deserialize<JsonElement[]>(Regex.Unescape(readArguments ?? ""));var method = type.GetMethod(prop);if (method != null){var parameters = method.GetParameters();var arguments = new object[parameters.Length];if (jsonObjects != null && jsonObjects.Length > 0){foreach (var arg in parameters){if (jsonObjects.Length <= arg.Position && arg.DefaultValue != null){arguments[arg.Position] = arg.DefaultValue;}else{var jsonObject = jsonObjects[arg.Position];var jsonObject2 = jsonObject.Deserialize(arg.ParameterType);if (jsonObject2 != null){arguments[arg.Position] = jsonObject2;}}}}var result = method.Invoke(obj, arguments);var serializedRet = "null";if (result != null){if (IsAsyncMethod(method)){Task task = (Task)result;await task.ConfigureAwait(false);result = ((dynamic)task).Result;}serializedRet = JsonSerializer.Serialize(result);}await RunJS("window." + name + ".returnValue('" + token + "', " + serializedRet + ");");}else{var propety = type.GetProperty(prop);if (propety != null){if (jsonObjects != null && jsonObjects.Length > 0){propety.SetValue(obj, jsonObjects[0].Deserialize(propety.PropertyType));}var result = JsonSerializer.Serialize(propety.GetValue(obj, null));await RunJS("window." + name + ".returnValue('" + token + "', " + result + ");");}else{await RunJS("window." + name + ".rejectCall('" + token + "', 'Member not found!');");}}}catch (Exception e){var error = e.Message + " (" + e.GetHashCode().ToString() + ")";error = error.Replace("\\n", " ");error = error.Replace("\n", " ");error = error.Replace("\"", "&quot;");await RunJS("window." + name + ".rejectCall('" + token + "', '" + error + "');");}}/// <summary>/// 发送自定义事件到 WebView/// </summary>/// <param name="type"></param>/// <param name="detail"></param>/// <param name="optBubbles"></param>/// <param name="optCancelable"></param>/// <param name="optComposed"></param>/// <returns></returns>public async Task sendEvent(string type, Dictionary<string, string>? detail = null, bool optBubbles = false, bool optCancelable = false, bool optComposed = false){List<string> opts = [];if (optBubbles){opts.Add("bubbles: true");}if (optCancelable){opts.Add("cancelable: true");}if (optComposed){opts.Add("composed: true");}if (detail != null){opts.Add("detail: " + JsonSerializer.Serialize(detail));}var optsStr = (opts.Count > 0 ? ", { " + string.Join(", ", opts) + " }" : "");await RunJS("const nativeEvent = new CustomEvent('" + type + "'" + optsStr + "); document.dispatchEvent(nativeEvent);");}/// <summary>/// 在 WebView 中执行 JavaScript 代码/// </summary>/// <param name="code"></param>/// <returns></returns>public Task<string?> RunJS(string code){if (_webView == null){return Task.FromResult<string?>(null);}return _webView.Dispatcher.DispatchAsync(() =>{var resultCode = code;if (resultCode.Contains("\\n") || resultCode.Contains('\n')){resultCode = "console.error('Called js from native api contain new line symbols!')";}else{resultCode = "try { " + resultCode + " } catch(e) { console.error(e); }";}var result = _webView.EvaluateJavaScriptAsync(resultCode);return result;});}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/882197.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI 如何重塑劳动力市场:基于 Claude 数据的深度分析

前言 本文翻译自 Anthropic 今天发布的 The Anthropic Economic Index ,经济指数报告,这份报告基于 Claude 的数据对目前的 AI 使用情况做了汇总。 引言 在未来的几年里,人工智能系统将对人们的工作方式产生重大影响。因此,我们推出了 Anthropic Economic Index,这是一个旨…

Nacos Python SDK 强势来袭,动态管理大模型 Prompt!

Nacos 从 0.8.0 版本开始就一直参与 Python 生态建设,努力作为 Python 生态中分布式微服务发现和配置管理的解决方案一直往前演进。目前随着 AI 领域的发展,Nacos 社区的 Python 开发者用户越来越多,因此这次我们迭代了 Python 的 GA 稳定版本,对不少历史问题做了修复以及易…

踩坑记录-二分搜索的不同情况

二分搜索的不同情况 二分搜索可以用来查找满足条件的值,但是满足条件的值可能只有1个,也可能有多个。比如查找1的索引,对于【1,1,2,2】来说,就有2个。一般要求的就是:满足条件最大值/满足条件最小值。 二分搜索详细介绍可以参考:https://programmercarl.com/0704.二分…

《ESP32-S3使用指南—IDF版 V1.6》第五章 搭建开发环境

第五章 搭建开发环境 1)实验平台:正点原子DNESP32S3开发板 2)章节摘自【正点原子】ESP32-S3使用指南—IDF版 V1.6 3)购买链接:https://detail.tmall.com/item.htm?&id=768499342659 4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/AT…

内测之家介绍

内测之家:助力应用开发与迭代的专业平台内测之家是一款功能强大且全面的应用内测与管理平台,专为 iOS 和 Android 开发者打造,旨在为他们提供便捷高效、安全可靠的一站式服务。无论是从资源安全到传输安全,还是从数据保护到应用管理、统计分析,内测之家都展现出卓越的能力…

如何用好 AI 编码工具,让通义灵码帮你做更多工作

通义灵码,是阿里云与通义实验室联合打造的智能编码辅助工具,提供行级/函数级实时续写、自然语言生成代码、单元测试生成、代码优化、注释生成、代码解释、研发智能问答、异常报错排查等能力,提供代码智能生成、研发智能问答能力。通义灵码,是阿里云与通义实验室联合打造的智…

用EXPLAIN检查SQL是否有慢查询

先看下面两张图: 图一 图二 同样都是查询语句,图一的条件采用的是模糊匹配,产生了全表扫面(type:a…

【触想智能】工控一体机在机械臂上应用的四大优势

随着工业自动化的发展,机械臂已成为现代制造业中不可或缺的一部分。为了使机械臂能够高效、精确地执行各种任务,工控一体机的应用变得越来越广泛。触想工控一体机在机械臂上的应用工控一体机是一种集计算、控制和通信于一体的高性能工业计算机,它不仅具备强大的计算能力,还…

CTFShow-Web167:

CTFShow-Web167:.htaccess利用 <button type="button" class="layui-btn" id="upload" lay-data="{url: upload.php, accept: images,exts:jpg}"> 限制上传类型为jpg文件 题目提示httpd,并且404页面返回Apache/2.4.25 (Debian…

对极几何(Epipolar Geometry)总结

为什么stereo很有用? 当我们需要从单一视角恢复结构时,我们的信息来源有以下几种: \(\bullet\) 从标定架可以获取标定架的位置 / 姿态以及相机内参 K。 \(\bullet\) 从无穷远点和线,加上正交的线和平面等信息,可以获取场景的结构和相机内参 K 。 但是由于内在歧义性,从单…

P10451 做题随笔

Solution 题意 原题链接 对每组数据,给定两颗用 01 序列描述的树,描述规则如下:按照 \(\text{DFS}\) 序进行遍历; 若序列中某位为 0,表示除根节点外的节点进栈;为 1 则表示出栈。要求判断一树是否可以通过交换子树的方式变换成另一子树(对于本题,即两树同构)。 分析 1…