本系列由浅入深逐个文件解析工作原理
目录:
- WebViewNativeApi.cs
- NativeApi.cs
- MainPage.xaml.cs
- 实战
- 串口
- 小票机
- 蓝牙
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 代码。
工作流程
- 初始化:在构造函数中,注册 WebView 的导航事件。
- 添加目标对象:通过 AddTarget 方法添加目标对象及其名称和 scheme。
- WebView 导航完成:在 OnWebViewInit 方法中,注入 JavaScript 代码并初始化目标对象。
- 处理本地调用请求:在 OnWebViewNavigatin 方法中,解析 URL 并执行相应的本地方法或属性访问。
- 执行本地方法:在 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("\"", """);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;});}
}