ToB企服应用市场:ToB评测及商务社交产业平台

标题: 另辟新径实现 Blazor/MAUI 本机交互(一) [打印本页]

作者: 尚未崩坏    时间: 2025-2-12 08:00
标题: 另辟新径实现 Blazor/MAUI 本机交互(一)
本系列由浅入深逐个文件剖析工作原理

目次:
WebViewNativeApi.cs

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

工作流程

通过这种方式,NativeBridge 类实现了在 .NET MAUI 应用程序中的 WebView 和本地代码之间的双向通讯。
完备代码
  1. using System.Reflection;
  2. using System.Runtime.CompilerServices;
  3. using System.Text.Json;
  4. using System.Text.RegularExpressions;
  5. namespace WebViewNativeApi;
  6. /// <summary>
  7. /// NativeBridge 类, 用于在 .NET MAUI 应用程序中的 WebView 和本地代码之间进行通信, 主要负责在 WebView 和本地代码之间建立桥梁
  8. /// </summary>
  9. public class NativeBridge
  10. {
  11.     /// <summary>
  12.     /// 默认的 URL scheme,用于识别本地调用
  13.     /// </summary>
  14.     private const string DEFAULT_SCHEME = "native://";
  15.     /// <summary>
  16.     /// JavaScript 代码,用于在 WebView 中创建一个代理对象,通过该对象可以调用本地方法
  17.     /// </summary>
  18.     private const string INTERFACE_JS = "window['createNativeBridgeProxy'] = " +
  19.         "(name, methods, properties, scheme = '" + DEFAULT_SCHEME + "') =>" +
  20.         "{" +
  21.         "    let apiCalls = new Map();" +
  22.         "" +
  23.         "    function randomUUID() {" +
  24.         "       return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>" +
  25.         "               (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));" +
  26.         "    }" +
  27.         "" +
  28.         "    function createRequest(target, success, reject, argumentsList) {" +
  29.         "        let uuid = randomUUID();" +
  30.         "        while(apiCalls.has(uuid)) { uuid = randomUUID(); };" +
  31.         "        apiCalls.set(uuid, { 'success': success, 'reject': reject, 'arguments': argumentsList });" +
  32.         "        location.href = scheme + name + '/' + target + '/' + uuid + '/';" +
  33.         "    }" +
  34.         "" +
  35.         "    return new Proxy({" +
  36.         "            getArguments : (token) => {" +
  37.         "                return apiCalls.get(token).arguments;" +
  38.         "            }," +
  39.         "            returnValue : (token, value) => {" +
  40.         "                let ret = value;" +
  41.         "                try { ret = JSON.parse(ret); } catch(e) { };" +
  42.         "                let callback = apiCalls.get(token).success;" +
  43.         "                if (callback && typeof callback === 'function')" +
  44.         "                    callback(ret);" +
  45.         "                apiCalls.delete(token);" +
  46.         "            }," +
  47.         "            rejectCall : (token, error) => {" +
  48.         "                let callback = apiCalls.get(token).reject;" +
  49.         "                if (callback && typeof callback === 'function')" +
  50.         "                    callback(error);" +
  51.         "                apiCalls.delete(token);" +
  52.         "            }" +
  53.         "        }," +
  54.         "        {" +
  55.         "            get: (target, prop, receiver) => {" +
  56.         "                if (methods.includes(prop)) {" +
  57.         "                    return new Proxy(() => {}, {" +
  58.         "                        apply: (target, thisArg, argumentsList) => {" +
  59.         "                            return new Promise((success, reject) => {" +
  60.         "                                    createRequest(prop, success, reject, argumentsList);" +
  61.         "                                });" +
  62.         "                        }" +
  63.         "                    });" +
  64.         "                }" +
  65.         "                if (!properties.includes(prop)) {" +
  66.         "                    return Reflect.get(target, prop, receiver);" +
  67.         "                }" +
  68.         "                return new Promise((success, reject) => {" +
  69.         "                        createRequest(prop, success, reject, []);" +
  70.         "                    });" +
  71.         "            }," +
  72.         "            set: (target, prop, value) => {" +
  73.         "                return new Promise((success, reject) => {" +
  74.         "                        createRequest(prop, success, reject, [value]);" +
  75.         "                    });" +
  76.         "            }" +
  77.         "        });" +
  78.         "};";
  79.     /// <summary>
  80.     /// WebView 控件的引用
  81.     /// </summary>
  82.     private readonly WebView? _webView = null;
  83.     /// <summary>
  84.     /// 用于存储本地对象的字典,存储目标对象及其名称和 scheme
  85.     /// </summary>
  86.     private readonly Dictionary<(string, string), object> _targets = [];
  87.     /// <summary>
  88.     /// 是否已经初始化
  89.     /// </summary>
  90.     private bool _isInit = false;
  91.     /// <summary>
  92.     /// 存储当前的查询信息
  93.     /// </summary>
  94.     private (string?, string?, string?, object?) _query = ("", "", "", null);
  95.     /// <summary>
  96.     /// 存储上一次导航的域名
  97.     /// </summary>
  98.     private string? lastDomain;
  99.     /// <summary>
  100.     /// 存储要注入的目标 JavaScript 代码
  101.     /// </summary>
  102.     public string? TargetJS;
  103.     /// <summary>
  104.     /// 构造函数,初始化 WebView 并注册导航事件
  105.     /// </summary>
  106.     /// <param name="wv"></param>
  107.     public NativeBridge(WebView? wv)
  108.     {
  109.         if (wv != null)
  110.         {
  111.             _webView = wv;
  112.             _webView.Navigated += OnWebViewInit;
  113.             _webView.Navigating += OnWebViewNavigatin;
  114.         }
  115.     }
  116.     /// <summary>
  117.     /// 添加目标对象及其名称和 scheme
  118.     /// </summary>
  119.     /// <param name="name"></param>
  120.     /// <param name="obj"></param>
  121.     /// <param name="sheme"></param>
  122.     public void AddTarget(string name, object obj, string sheme = DEFAULT_SCHEME)
  123.     {
  124.         if (obj == null)
  125.         {
  126.             return;
  127.         }
  128.         _targets.Add((name, sheme), obj);
  129.         if (_isInit)
  130.         {
  131.             AddTargetToWebView(name, obj, sheme);
  132.         }
  133.     }
  134.     /// <summary>
  135.     /// WebView 初始化事件处理程序,在 WebView 导航完成后调用,注入 JavaScript 代码并初始化目标对象。
  136.     /// </summary>
  137.     /// <param name="sender"></param>
  138.     /// <param name="e"></param>
  139.     private async void OnWebViewInit(object? sender, WebNavigatedEventArgs e)
  140.     {
  141.         var currentDomain = new Uri(e.Url).Host;
  142.         if (lastDomain != currentDomain)
  143.         {
  144.             _isInit = false;
  145.             lastDomain = currentDomain;
  146.         }
  147.         else
  148.         {
  149.             var isInjected = await RunJS("window.dialogs !== undefined");
  150.             if (isInjected == "false")
  151.             {
  152.                 _isInit = false;
  153.             }
  154.         }
  155.         if (!_isInit)
  156.         {
  157.             _ = await RunJS(INTERFACE_JS);
  158.             if (TargetJS != null)
  159.             {
  160.                 _ = await RunJS(TargetJS);
  161.             }
  162.             foreach (KeyValuePair<(string, string), object> entry in _targets)
  163.             {
  164.                 AddTargetToWebView(entry.Key.Item1, entry.Value, entry.Key.Item2);
  165.             }
  166.             _isInit = true;
  167.         }
  168.     }
  169.     /// <summary>
  170.     /// WebView 导航事件处理程序,在 WebView 导航时调用,根据 URL 判断是否调用本地方法。
  171.     /// </summary>
  172.     /// <param name="sender"></param>
  173.     /// <param name="e"></param>
  174.     private void OnWebViewNavigatin(object? sender, WebNavigatingEventArgs e)
  175.     {
  176.         if (!_isInit)
  177.         {
  178.             return;
  179.         }
  180.         foreach (KeyValuePair<(string, string), object> entry in _targets)
  181.         {
  182.             var startStr = entry.Key.Item2 + entry.Key.Item1;
  183.             if (!e.Url.StartsWith(startStr))
  184.             {
  185.                 continue;
  186.             }
  187.             var request = e.Url[(e.Url.IndexOf(startStr) + startStr.Length)..].ToLower();
  188.             request = request.Trim(['/', '\\']);
  189.             var requestArgs = request.Split('/');
  190.             if (requestArgs.Length < 2)
  191.             {
  192.                 return;
  193.             }
  194.             e.Cancel = true;
  195.             var prop = requestArgs[0];
  196.             var token = requestArgs[1];
  197.             Type type = entry.Value.GetType();
  198.             if (type.GetMember(prop) == null)
  199.             {
  200.                 RunJS("window." + entry.Key.Item1 + ".rejectCall('" + token + "', 'Member not found!');");
  201.                 return;
  202.             }
  203.             _query = (entry.Key.Item1, token, prop, entry.Value);
  204.             Task.Run(() =>
  205.             {
  206.                 RunCommand(_query.Item1, _query.Item2, _query.Item3, _query.Item4);
  207.                 _query = ("", "", "", null);
  208.             });
  209.             return;
  210.         }
  211.     }
  212.     /// <summary>
  213.     /// 将目标对象的方法和属性注入到 WebView 中。
  214.     /// </summary>
  215.     /// <param name="name"></param>
  216.     /// <param name="obj"></param>
  217.     /// <param name="sheme"></param>
  218.     private void AddTargetToWebView(string name, object obj, string sheme)
  219.     {
  220.         var type = obj.GetType();
  221.         var methods = new List<string>();
  222.         var properties = new List<string>();
  223.         foreach (MethodInfo method in type.GetMethods())
  224.         {
  225.             methods.Add(method.Name);
  226.         }
  227.         foreach (PropertyInfo p in type.GetProperties())
  228.         {
  229.             properties.Add(p.Name);
  230.         }
  231.         RunJS("window." + name + " = window.createNativeBridgeProxy('" + name + "', " + JsonSerializer.Serialize(methods) + ", " +
  232.             JsonSerializer.Serialize(properties) + ", '" + sheme + "');");
  233.     }
  234.     /// <summary>
  235.     /// 判断方法是否为异步方法
  236.     /// </summary>
  237.     /// <param name="method"></param>
  238.     /// <returns></returns>
  239.     private static bool IsAsyncMethod(MethodInfo method)
  240.     {
  241.         var attType = typeof(AsyncStateMachineAttribute);
  242.         var attrib = (AsyncStateMachineAttribute?)method.GetCustomAttribute(attType);
  243.         return (attrib != null);
  244.     }
  245.     /// <summary>
  246.     /// 调用本地方法,执行本地方法或属性访问,并将结果返回给 WebView
  247.     /// </summary>
  248.     /// <param name="name"></param>
  249.     /// <param name="token"></param>
  250.     /// <param name="prop"></param>
  251.     /// <param name="obj"></param>
  252.     private async void RunCommand(string name, string token, string prop, object obj)
  253.     {
  254.         try
  255.         {
  256.             var type = obj.GetType();
  257.             var readArguments = await RunJS("window." + name + ".getArguments('" + token + "');");
  258.             var jsonObjects = JsonSerializer.Deserialize<JsonElement[]>(Regex.Unescape(readArguments ?? ""));
  259.             var method = type.GetMethod(prop);
  260.             if (method != null)
  261.             {
  262.                 var parameters = method.GetParameters();
  263.                 var arguments = new object[parameters.Length];
  264.                 if (jsonObjects != null && jsonObjects.Length > 0)
  265.                 {
  266.                     foreach (var arg in parameters)
  267.                     {
  268.                         if (jsonObjects.Length <= arg.Position && arg.DefaultValue != null)
  269.                         {
  270.                             arguments[arg.Position] = arg.DefaultValue;
  271.                         }
  272.                         else
  273.                         {
  274.                             var jsonObject = jsonObjects[arg.Position];
  275.                             var jsonObject2 = jsonObject.Deserialize(arg.ParameterType);
  276.                             if (jsonObject2 != null)
  277.                             {
  278.                                 arguments[arg.Position] = jsonObject2;
  279.                             }
  280.                         }
  281.                     }
  282.                 }
  283.                 var result = method.Invoke(obj, arguments);
  284.                 var serializedRet = "null";
  285.                 if (result != null)
  286.                 {
  287.                     if (IsAsyncMethod(method))
  288.                     {
  289.                         Task task = (Task)result;
  290.                         await task.ConfigureAwait(false);
  291.                         result = ((dynamic)task).Result;
  292.                     }
  293.                     serializedRet = JsonSerializer.Serialize(result);
  294.                 }
  295.                 await RunJS("window." + name + ".returnValue('" + token + "', " + serializedRet + ");");
  296.             }
  297.             else
  298.             {
  299.                 var propety = type.GetProperty(prop);
  300.                 if (propety != null)
  301.                 {
  302.                     if (jsonObjects != null && jsonObjects.Length > 0)
  303.                     {
  304.                         propety.SetValue(obj, jsonObjects[0].Deserialize(propety.PropertyType));
  305.                     }
  306.                     var result = JsonSerializer.Serialize(propety.GetValue(obj, null));
  307.                     await RunJS("window." + name + ".returnValue('" + token + "', " + result + ");");
  308.                 }
  309.                 else
  310.                 {
  311.                     await RunJS("window." + name + ".rejectCall('" + token + "', 'Member not found!');");
  312.                 }
  313.             }
  314.         }
  315.         catch (Exception e)
  316.         {
  317.             var error = e.Message + " (" + e.GetHashCode().ToString() + ")";
  318.             error = error.Replace("\\n", " ");
  319.             error = error.Replace("\n", " ");
  320.             error = error.Replace(""", """);
  321.             await RunJS("window." + name + ".rejectCall('" + token + "', '" + error + "');");
  322.         }
  323.     }
  324.     /// <summary>
  325.     /// 发送自定义事件到 WebView
  326.     /// </summary>
  327.     /// <param name="type"></param>
  328.     /// <param name="detail"></param>
  329.     /// <param name="optBubbles"></param>
  330.     /// <param name="optCancelable"></param>
  331.     /// <param name="optComposed"></param>
  332.     /// <returns></returns>
  333.     public async Task sendEvent(string type, Dictionary<string, string>? detail = null, bool optBubbles = false, bool optCancelable = false, bool optComposed = false)
  334.     {
  335.         List<string> opts = [];
  336.         if (optBubbles)
  337.         {
  338.             opts.Add("bubbles: true");
  339.         }
  340.         if (optCancelable)
  341.         {
  342.             opts.Add("cancelable: true");
  343.         }
  344.         if (optComposed)
  345.         {
  346.             opts.Add("composed: true");
  347.         }
  348.         if (detail != null)
  349.         {
  350.             opts.Add("detail: " + JsonSerializer.Serialize(detail));
  351.         }
  352.         var optsStr = (opts.Count > 0 ? ", { " + string.Join(", ", opts) + " }" : "");
  353.         await RunJS("const nativeEvent = new CustomEvent('" + type + "'" + optsStr + "); document.dispatchEvent(nativeEvent);");
  354.     }
  355.     /// <summary>
  356.     /// 在 WebView 中执行 JavaScript 代码
  357.     /// </summary>
  358.     /// <param name="code"></param>
  359.     /// <returns></returns>
  360.     public Task<string?> RunJS(string code)
  361.     {
  362.         if (_webView == null)
  363.         {
  364.             return Task.FromResult<string?>(null);
  365.         }
  366.         return _webView.Dispatcher.DispatchAsync(() =>
  367.         {
  368.             var resultCode = code;
  369.             if (resultCode.Contains("\\n") || resultCode.Contains('\n'))
  370.             {
  371.                 resultCode = "console.error('Called js from native api contain new line symbols!')";
  372.             }
  373.             else
  374.             {
  375.                 resultCode = "try { " + resultCode + " } catch(e) { console.error(e); }";
  376.             }
  377.             var result = _webView.EvaluateJavaScriptAsync(resultCode);
  378.             return result;
  379.         });
  380.     }
  381. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4