太行山呼叫风车 發表於 2025-2-11 18:30:00

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

<h1 id="本系列由浅入深逐个文件解析工作原理">本系列由浅入深逐个文件解析工作原理</h1>
<p>目录:</p>
<ol>
<li>WebViewNativeApi.cs</li>
<li>NativeApi.cs</li>
<li>MainPage.xaml.cs</li>
<li>实战</li>
<li>串口</li>
<li>小票机</li>
<li>蓝牙</li>
</ol>
<h2 id="webviewnativeapics">WebViewNativeApi.cs</h2>
<p>WebViewNativeApi.cs 文件中的代码实现了一个 NativeBridge 类,用于在 .NET MAUI 应用程序中的 WebView 和本地代码之间进行通信。以下是该代码的工作原理说明:</p>
<h3 id="类和字段">类和字段</h3>
<ul>
<li>NativeBridge 类:主要负责在 WebView 和本地代码之间建立桥梁。</li>
<li>DEFAULT_SCHEME:默认的 URL scheme,用于识别本地调用。</li>
<li>INTERFACE_JS:JavaScript 代码,用于在 WebView 中创建一个代理对象,通过该对象可以调用本地方法。</li>
<li>_webView:WebView 控件的引用。</li>
<li>_targets:存储目标对象及其名称和 scheme。</li>
<li>_isInit:标识 WebView 是否已初始化。</li>
<li>_query:存储当前的查询信息。</li>
<li>lastDomain:存储上一次导航的域名。</li>
<li>TargetJS:存储要注入的目标 JavaScript 代码。<br>
构造函数</li>
<li>NativeBridge(WebView? wv):构造函数,初始化 WebView 并注册导航事件。<br>
方法</li>
<li>AddTarget(string name, object obj, string sheme = DEFAULT_SCHEME):添加目标对象及其名称和 scheme。</li>
<li>OnWebViewInit(object? sender, WebNavigatedEventArgs e):在 WebView 导航完成后调用,注入 JavaScript 代码并初始化目标对象。</li>
<li>OnWebViewNavigatin(object? sender, WebNavigatingEventArgs e):在 WebView 导航时调用,处理本地调用请求。</li>
<li>AddTargetToWebView(string name, object obj, string sheme):将目标对象的方法和属性注入到 WebView 中。</li>
<li>IsAsyncMethod(MethodInfo method):判断方法是否为异步方法。</li>
<li>RunCommand(string name, string token, string prop, object obj):执行本地方法或属性访问,并将结果返回给 WebView。</li>
<li>sendEvent(string type, Dictionary&lt;string, string&gt;? detail = null, bool optBubbles = false, bool optCancelable = false, bool optComposed = false):发送自定义事件到 WebView。</li>
<li>RunJS(string code):在 WebView 中执行 JavaScript 代码。</li>
</ul>
<h3 id="工作流程">工作流程</h3>
<ol>
<li>初始化:在构造函数中,注册 WebView 的导航事件。</li>
<li>添加目标对象:通过 AddTarget 方法添加目标对象及其名称和 scheme。</li>
<li>WebView 导航完成:在 OnWebViewInit 方法中,注入 JavaScript 代码并初始化目标对象。</li>
<li>处理本地调用请求:在 OnWebViewNavigatin 方法中,解析 URL 并执行相应的本地方法或属性访问。</li>
<li>执行本地方法:在 RunCommand 方法中,调用目标对象的方法或属性,并将结果返回给 WebView。</li>
</ol>
<p>通过这种方式,NativeBridge 类实现了在 .NET MAUI 应用程序中的 WebView 和本地代码之间的双向通信。</p>
<h3 id="完整代码">完整代码</h3>
<pre><code>using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace WebViewNativeApi;

/// &lt;summary&gt;
/// NativeBridge 类, 用于在 .NET MAUI 应用程序中的 WebView 和本地代码之间进行通信, 主要负责在 WebView 和本地代码之间建立桥梁
/// &lt;/summary&gt;
public class NativeBridge
{
    /// &lt;summary&gt;
    /// 默认的 URL scheme,用于识别本地调用
    /// &lt;/summary&gt;
    private const string DEFAULT_SCHEME = "native://";

    /// &lt;summary&gt;
    /// JavaScript 代码,用于在 WebView 中创建一个代理对象,通过该对象可以调用本地方法
    /// &lt;/summary&gt;
    private const string INTERFACE_JS = "window['createNativeBridgeProxy'] = " +
      "(name, methods, properties, scheme = '" + DEFAULT_SCHEME + "') =&gt;" +
      "{" +
      "    let apiCalls = new Map();" +
      "" +
      "    function randomUUID() {" +
      "       return '10000000-1000-4000-8000-100000000000'.replace(//g, c =&gt;" +
      "               (+c ^ crypto.getRandomValues(new Uint8Array(1)) &amp; 15 &gt;&gt; +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) =&gt; {" +
      "                return apiCalls.get(token).arguments;" +
      "            }," +
      "            returnValue : (token, value) =&gt; {" +
      "                let ret = value;" +
      "                try { ret = JSON.parse(ret); } catch(e) { };" +
      "                let callback = apiCalls.get(token).success;" +
      "                if (callback &amp;&amp; typeof callback === 'function')" +
      "                  callback(ret);" +
      "                apiCalls.delete(token);" +
      "            }," +
      "            rejectCall : (token, error) =&gt; {" +
      "                let callback = apiCalls.get(token).reject;" +
      "                if (callback &amp;&amp; typeof callback === 'function')" +
      "                  callback(error);" +
      "                apiCalls.delete(token);" +
      "            }" +
      "      }," +
      "      {" +
      "            get: (target, prop, receiver) =&gt; {" +
      "                if (methods.includes(prop)) {" +
      "                  return new Proxy(() =&gt; {}, {" +
      "                        apply: (target, thisArg, argumentsList) =&gt; {" +
      "                            return new Promise((success, reject) =&gt; {" +
      "                                    createRequest(prop, success, reject, argumentsList);" +
      "                              });" +
      "                        }" +
      "                  });" +
      "                }" +
      "                if (!properties.includes(prop)) {" +
      "                  return Reflect.get(target, prop, receiver);" +
      "                }" +
      "                return new Promise((success, reject) =&gt; {" +
      "                        createRequest(prop, success, reject, []);" +
      "                  });" +
      "            }," +
      "            set: (target, prop, value) =&gt; {" +
      "                return new Promise((success, reject) =&gt; {" +
      "                        createRequest(prop, success, reject, );" +
      "                  });" +
      "            }" +
      "      });" +
      "};";

    /// &lt;summary&gt;
    /// WebView 控件的引用
    /// &lt;/summary&gt;
    private readonly WebView? _webView = null;

    /// &lt;summary&gt;
    /// 用于存储本地对象的字典,存储目标对象及其名称和 scheme
    /// &lt;/summary&gt;
    private readonly Dictionary&lt;(string, string), object&gt; _targets = [];

    /// &lt;summary&gt;
    /// 是否已经初始化
    /// &lt;/summary&gt;
    private bool _isInit = false;

    /// &lt;summary&gt;
    /// 存储当前的查询信息
    /// &lt;/summary&gt;
    private (string?, string?, string?, object?) _query = ("", "", "", null);

    /// &lt;summary&gt;
    /// 存储上一次导航的域名
    /// &lt;/summary&gt;
    private string? lastDomain;

    /// &lt;summary&gt;
    /// 存储要注入的目标 JavaScript 代码
    /// &lt;/summary&gt;
    public string? TargetJS;

    /// &lt;summary&gt;
    /// 构造函数,初始化 WebView 并注册导航事件
    /// &lt;/summary&gt;
    /// &lt;param name="wv"&gt;&lt;/param&gt;
    public NativeBridge(WebView? wv)
    {
      if (wv != null)
      {
            _webView = wv;
            _webView.Navigated += OnWebViewInit;
            _webView.Navigating += OnWebViewNavigatin;
      }
    }

    /// &lt;summary&gt;
    /// 添加目标对象及其名称和 scheme
    /// &lt;/summary&gt;
    /// &lt;param name="name"&gt;&lt;/param&gt;
    /// &lt;param name="obj"&gt;&lt;/param&gt;
    /// &lt;param name="sheme"&gt;&lt;/param&gt;
    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);
      }
    }

    /// &lt;summary&gt;
    /// WebView 初始化事件处理程序,在 WebView 导航完成后调用,注入 JavaScript 代码并初始化目标对象。
    /// &lt;/summary&gt;
    /// &lt;param name="sender"&gt;&lt;/param&gt;
    /// &lt;param name="e"&gt;&lt;/param&gt;
    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&lt;(string, string), object&gt; entry in _targets)
            {
                AddTargetToWebView(entry.Key.Item1, entry.Value, entry.Key.Item2);
            }

            _isInit = true;
      }
    }

    /// &lt;summary&gt;
    /// WebView 导航事件处理程序,在 WebView 导航时调用,根据 URL 判断是否调用本地方法。
    /// &lt;/summary&gt;
    /// &lt;param name="sender"&gt;&lt;/param&gt;
    /// &lt;param name="e"&gt;&lt;/param&gt;
    private void OnWebViewNavigatin(object? sender, WebNavigatingEventArgs e)
    {
      if (!_isInit)
      {
            return;
      }

      foreach (KeyValuePair&lt;(string, string), object&gt; 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 &lt; 2)
            {
                return;
            }

            e.Cancel = true;

            var prop = requestArgs;
            var token = requestArgs;

            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(() =&gt;
            {
                RunCommand(_query.Item1, _query.Item2, _query.Item3, _query.Item4);
                _query = ("", "", "", null);
            });
            return;
      }
    }


    /// &lt;summary&gt;
    /// 将目标对象的方法和属性注入到 WebView 中。
    /// &lt;/summary&gt;
    /// &lt;param name="name"&gt;&lt;/param&gt;
    /// &lt;param name="obj"&gt;&lt;/param&gt;
    /// &lt;param name="sheme"&gt;&lt;/param&gt;
    private void AddTargetToWebView(string name, object obj, string sheme)
    {
      var type = obj.GetType();
      var methods = new List&lt;string&gt;();
      var properties = new List&lt;string&gt;();
      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 + "');");
    }

    /// &lt;summary&gt;
    /// 判断方法是否为异步方法
    /// &lt;/summary&gt;
    /// &lt;param name="method"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    private static bool IsAsyncMethod(MethodInfo method)
    {
      var attType = typeof(AsyncStateMachineAttribute);
      var attrib = (AsyncStateMachineAttribute?)method.GetCustomAttribute(attType);
      return (attrib != null);
    }

    /// &lt;summary&gt;
    /// 调用本地方法,执行本地方法或属性访问,并将结果返回给 WebView
    /// &lt;/summary&gt;
    /// &lt;param name="name"&gt;&lt;/param&gt;
    /// &lt;param name="token"&gt;&lt;/param&gt;
    /// &lt;param name="prop"&gt;&lt;/param&gt;
    /// &lt;param name="obj"&gt;&lt;/param&gt;
    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&lt;JsonElement[]&gt;(Regex.Unescape(readArguments ?? ""));
            var method = type.GetMethod(prop);
            if (method != null)
            {
                var parameters = method.GetParameters();
                var arguments = new object;
                if (jsonObjects != null &amp;&amp; jsonObjects.Length &gt; 0)
                {
                  foreach (var arg in parameters)
                  {
                        if (jsonObjects.Length &lt;= arg.Position &amp;&amp; arg.DefaultValue != null)
                        {
                            arguments = arg.DefaultValue;
                        }
                        else
                        {
                            var jsonObject = jsonObjects;
                            var jsonObject2 = jsonObject.Deserialize(arg.ParameterType);
                            if (jsonObject2 != null)
                            {
                              arguments = 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 &amp;&amp; jsonObjects.Length &gt; 0)
                  {
                        propety.SetValue(obj, jsonObjects.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("\"", "&amp;quot;");
            await RunJS("window." + name + ".rejectCall('" + token + "', '" + error + "');");
      }
    }

    /// &lt;summary&gt;
    /// 发送自定义事件到 WebView
    /// &lt;/summary&gt;
    /// &lt;param name="type"&gt;&lt;/param&gt;
    /// &lt;param name="detail"&gt;&lt;/param&gt;
    /// &lt;param name="optBubbles"&gt;&lt;/param&gt;
    /// &lt;param name="optCancelable"&gt;&lt;/param&gt;
    /// &lt;param name="optComposed"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public async Task sendEvent(string type, Dictionary&lt;string, string&gt;? detail = null, bool optBubbles = false, bool optCancelable = false, bool optComposed = false)
    {
      List&lt;string&gt; 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 &gt; 0 ? ", { " + string.Join(", ", opts) + " }" : "");
      await RunJS("const nativeEvent = new CustomEvent('" + type + "'" + optsStr + "); document.dispatchEvent(nativeEvent);");
    }

    /// &lt;summary&gt;
    /// 在 WebView 中执行 JavaScript 代码
    /// &lt;/summary&gt;
    /// &lt;param name="code"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    public Task&lt;string?&gt; RunJS(string code)
    {
      if (_webView == null)
      {
            return Task.FromResult&lt;string?&gt;(null);
      }
      return _webView.Dispatcher.DispatchAsync(() =&gt;
      {
            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;
      });
    }
}
</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    <h4 id="关联项目">关联项目</h4>
<p><font color="blue">FreeSql QQ群:4336577</font></p>
<p><font color="blue">BA &amp; Blazor QQ群:795206915</font></p>
<p><font color="blue">Maui Blazor 中文社区 QQ群:645660665</font></p>
<h4 id="知识共享许可协议">知识共享许可协议</h4>
<p>本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。</p>
<h4 id="转载声明">转载声明</h4>
<p>本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://www.cnblogs.com/densen2014/p/18638327</p>
<h4 id="alexchow">AlexChow</h4>
<p>今日头条 | 博客园 | 知乎 | Gitee | GitHub</p>

<p><img src="https://img2023.cnblogs.com/blog/1980213/202302/1980213-20230201233143321-1727894703.png" alt="image" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/densen2014/p/18638327
頁: [1]
查看完整版本: 另辟新径实现 Blazor/MAUI 本机交互(一)