.NET Multi-platform App UI (.NET MAUI) HybridWebView 支持在 Web 视图中托管任意 HTML/JS/CSS 内容,并允许 Web 视图 (JavaScript) 中的代码与托管 Web 视图 (C#/.NET) 的代码之间进行通信。 例如,如果已有 React JS 应用,则可以将其托管在跨平台 .NET MAUI 本机应用中,并使用 C# 和 .NET 生成应用的后端。
HybridWebView 定义以下属性:
-
DefaultFile,类型为
string?
,指定 HybridRoot 中应作为默认文件的文件。 默认值为 index.html。 -
HybridRoot,类型为
string?
,是应用程序原始资产资源中包含网页应用内容的路径。 默认值为 wwwroot,映射到 Resources/Raw/wwwroot。
此外,HybridWebView 定义在收到原始消息时引发的 RawMessageReceived 事件。 事件附带的 HybridWebViewRawMessageReceivedEventArgs 对象定义包含消息的 Message 属性。
应用程序的 C# 代码可以使用 HybridWebView 和 InvokeJavaScriptAsync 方法在 EvaluateJavaScriptAsync 中调用同步和异步的 JavaScript 方法。 应用的 JavaScript 代码还可以调用同步和异步 C# 方法。 有关详细信息,请参阅 从 C# 调用 JavaScript,以及 从 JavaScript 调用 C# 。
若要使用 HybridWebView 生成 .NET MAUI 应用,需要:
- 应用的 Web 内容,由静态 HTML、JavaScript、CSS、图像和其他文件组成。
- 作为应用 UI 的一部分的 HybridWebView 控件。 这可以通过在应用的 XAML 中引用它来实现。
- Web 内容和 C#/.NET 中的代码,使用 HybridWebView API 在两个组件之间发送消息。
整个应用(包括 Web 内容)已打包并在设备上本地运行,并可以发布到对应的应用商店。 Web 内容嵌入在本机网页视图控件中,并在应用程序的上下文中运行。 应用的任何部分都可以访问外部 Web 服务,但这不是必须的。
重要
默认情况下,当启用完全修整或本机 AOT 时,HybridWebView 控件将不可用。 若要更改此行为,请参阅 剪裁功能开关。
创建 .NET MAUI HybridWebView 应用
若要使用 HybridWebView 生成 .NET MAUI 应用,需要:
打开现有的 .NET MAUI 应用项目或新建 .NET MAUI 应用项目。
将 Web 内容添加到 .NET MAUI 应用项目。
应用的 Web 内容应作为原始资产包含在 .NET MAUI 项目中。 原始资产是应用的 Resources\Raw 文件夹中的任何文件,包括子文件夹。 对于默认 HybridWebView,Web 内容应放置在 Resources\Raw\wwwroot 文件夹中,其主文件名为 index.html。
简单的应用可能包含以下文件和内容:
Resources\Raw\wwwroot\index.html,其中包含主 UI 内容:
<!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title></title> <link rel="icon" href="data:,"> <link rel="stylesheet" href="styles/app.css"> <script src="scripts/HybridWebView.js"></script> <script> function LogMessage(msg) { var messageLog = document.getElementById("messageLog"); messageLog.value += '\r\n' + msg; } window.addEventListener( "HybridWebViewMessageReceived", function (e) { LogMessage("Raw message: " + e.detail.message); }); function AddNumbers(a, b) { var result = { "result": a + b, "operationName": "Addition" }; return result; } var count = 0; async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) { const response = await fetch("/asyncdata.txt"); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } var jsonData = await response.json(); jsonData[s1] = s2; const msg = 'JSON data is available: ' + JSON.stringify(jsonData); window.HybridWebView.SendRawMessage(msg) return jsonData; } async function InvokeDoSyncWork() { LogMessage("Invoking DoSyncWork"); await window.HybridWebView.InvokeDotNet('DoSyncWork'); LogMessage("Invoked DoSyncWork"); } async function InvokeDoSyncWorkParams() { LogMessage("Invoking DoSyncWorkParams"); await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']); LogMessage("Invoked DoSyncWorkParams"); } async function InvokeDoSyncWorkReturn() { LogMessage("Invoking DoSyncWorkReturn"); const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn'); LogMessage("Invoked DoSyncWorkReturn, return value: " + retValue); } async function InvokeDoSyncWorkParamsReturn() { LogMessage("Invoking DoSyncWorkParamsReturn"); const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']); LogMessage("Invoked DoSyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value); } async function InvokeDoAsyncWork() { LogMessage("Invoking DoAsyncWork"); await window.HybridWebView.InvokeDotNet('DoAsyncWork'); LogMessage("Invoked DoAsyncWork"); } async function InvokeDoAsyncWorkParams() { LogMessage("Invoking DoAsyncWorkParams"); await window.HybridWebView.InvokeDotNet('DoAsyncWorkParams', [123, 'hello']); LogMessage("Invoked DoAsyncWorkParams"); } async function InvokeDoAsyncWorkReturn() { LogMessage("Invoking DoAsyncWorkReturn"); const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkReturn'); LogMessage("Invoked DoAsyncWorkReturn, return value: " + retValue); } async function InvokeDoAsyncWorkParamsReturn() { LogMessage("Invoking DoAsyncWorkParamsReturn"); const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkParamsReturn', [123, 'hello']); LogMessage("Invoked DoAsyncWorkParamsReturn, return value: message=" + retValue.Message + ", value=" + retValue.Value); } </script> </head> <body> <div> Hybrid sample! </div> <div> <button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button> </div> <div> <button onclick="InvokeDoSyncWork()">Call C# sync method (no params)</button> <button onclick="InvokeDoSyncWorkParams()">Call C# sync method (params)</button> <button onclick="InvokeDoSyncWorkReturn()">Call C# method (no params) and get simple return value</button> <button onclick="InvokeDoSyncWorkParamsReturn()">Call C# method (params) and get complex return value</button> </div> <div> <button onclick="InvokeDoAsyncWork()">Call C# async method (no params)</button> <button onclick="InvokeDoAsyncWorkParams()">Call C# async method (params)</button> <button onclick="InvokeDoAsyncWorkReturn()">Call C# async method (no params) and get simple return value</button> <button onclick="InvokeDoAsyncWorkParamsReturn()">Call C# async method (params) and get complex return value</button> </div> <div> Log: <textarea readonly id="messageLog" style="width: 80%; height: 10em;"></textarea> </div> <div> Consider checking out this PDF: <a href="docs/sample.pdf">sample.pdf</a> </div> </body> </html>
Resources\Raw\wwwroot\scripts\HybridWebView.js,其中包含标准 HybridWebView JavaScript 库:
window.HybridWebView = { "Init": function Init() { function DispatchHybridWebViewMessage(message) { const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); window.dispatchEvent(event); } if (window.chrome && window.chrome.webview) { // Windows WebView2 window.chrome.webview.addEventListener('message', arg => { DispatchHybridWebViewMessage(arg.data); }); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView window.external = { "receiveMessage": message => { DispatchHybridWebViewMessage(message); } }; } else { // Android WebView window.addEventListener('message', arg => { DispatchHybridWebViewMessage(arg.data); }); } }, "SendRawMessage": function SendRawMessage(message) { window.HybridWebView.__SendMessageInternal('__RawMessage', message); }, "InvokeDotNet": async function InvokeDotNetAsync(methodName, paramValues) { const body = { MethodName: methodName }; if (typeof paramValues !== 'undefined') { if (!Array.isArray(paramValues)) { paramValues = [paramValues]; } for (var i = 0; i < paramValues.length; i++) { paramValues[i] = JSON.stringify(paramValues[i]); } if (paramValues.length > 0) { body.ParamValues = paramValues; } } const message = JSON.stringify(body); var requestUrl = `${window.___location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; const rawResponse = await fetch(requestUrl, { method: 'GET', headers: { 'Accept': 'application/json' } }); const response = await rawResponse.json(); if (response) { if (response.IsJson) { return JSON.parse(response.Result); } return response.Result; } return null; }, "__SendMessageInternal": function __SendMessageInternal(type, message) { const messageToSend = type + '|' + message; if (window.chrome && window.chrome.webview) { // Windows WebView2 window.chrome.webview.postMessage(messageToSend); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend); } else { // Android WebView hybridWebViewHost.sendMessage(messageToSend); } }, "__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) { try { var result = null; if (methodName[Symbol.toStringTag] === 'AsyncFunction') { result = await methodName(...args); } else { result = methodName(...args); } window.HybridWebView.__TriggerAsyncCallback(taskId, result); } catch (ex) { console.error(ex); window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex); } }, "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { const json = JSON.stringify(result); window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + json); }, "__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) { if (!error) { json = { Message: "Unknown error", StackTrace: Error().stack }; } else if (error instanceof Error) { json = { Name: error.name, Message: error.message, StackTrace: error.stack }; } else if (typeof (error) === 'string') { json = { Message: error, StackTrace: Error().stack }; } else { json = { Message: JSON.stringify(error), StackTrace: Error().stack }; } json = JSON.stringify(json); window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json); } } window.HybridWebView.Init();
然后,将任何其他 Web 内容添加到项目。
警告
在某些情况下,Visual Studio 可能会向项目的 .csproj 文件添加不正确的条目。 使用原始资产的默认位置时,.csproj 文件中不应有任何这些文件或文件夹的条目。
将 HybridWebView 控件添加到应用:
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*"> <Button Text="Send message to JavaScript" Clicked="OnSendMessageButtonClicked" /> <HybridWebView x:Name="hybridWebView" RawMessageReceived="OnHybridWebViewRawMessageReceived" Grid.Row="1" /> </Grid>
修改
CreateMauiApp
类的MauiProgram
方法,以便在应用程序以调试配置运行时,在基础的 WebView 控件上启用开发人员工具。 为此,请在IServiceCollection对象上调用AddHybridWebViewDeveloperTools方法:using Microsoft.Extensions.Logging; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); #if DEBUG builder.Services.AddHybridWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif // Register any app services on the IServiceCollection object return builder.Build(); } }
使用 HybridWebView API 在 JavaScript 和 C# 代码之间发送消息:
private void OnSendMessageButtonClicked(object sender, EventArgs e) { hybridWebView.SendRawMessage($"Hello from C#!"); } private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e) { await DisplayAlert("Raw Message Received", e.Message, "OK"); }
因为没有执行其他处理,所以上述消息被归类为原始消息。 还可以对消息中的数据进行编码,以执行更高级的消息传递。
从 C# 调用 JavaScript
应用的 C# 代码可以使用可选参数和可选返回值在 HybridWebView 中同步和异步调用 JavaScript 方法。 这可以通过 InvokeJavaScriptAsync 和 EvaluateJavaScriptAsync 方法实现:
- EvaluateJavaScriptAsync 方法运行通过参数提供的 JavaScript 代码,并将结果作为字符串返回。
- 该方法 InvokeJavaScriptAsync 调用指定的 JavaScript 方法,可以选择传入参数值,并指定指示返回值的类型的泛型参数。 它返回泛型参数类型的对象,该对象包含调用 JavaScript 方法的返回值。 在内部,参数和返回值已经过 JSON 编码。
注意
.NET 10 包括一个 InvokeJavaScriptAsync 重载,该重载调用指定的 JavaScript 方法而不指定有关返回类型的任何信息。 有关详细信息,请参阅 调用不返回值的 JavaScript 方法。
调用同步 JavaScript
可以使用 EvaluateJavaScriptAsync 和 InvokeJavaScriptAsync 方法调用同步 JavaScript 方法。 在以下示例中,InvokeJavaScriptAsync 方法用于演示如何调用嵌入在应用 Web 内容中的 JavaScript。 例如,可以在 Web 内容中定义用于添加两个数字的简单 Javascript 方法:
function AddNumbers(a, b) {
return a + b;
}
可以从 C# 使用 InvokeJavaScriptAsync 方法调用 AddNumbers
JavaScript 方法。
double x = 123d;
double y = 321d;
double result = await hybridWebView.InvokeJavaScriptAsync<double>(
"AddNumbers", // JavaScript method name
HybridSampleJSContext.Default.Double, // JSON serialization info for return type
[x, y], // Parameter values
[HybridSampleJSContext.Default.Double, HybridSampleJSContext.Default.Double]); // JSON serialization info for each parameter
方法调用需要指定 JsonTypeInfo
对象,这些对象包括操作中使用的类型的序列化信息。 在项目中包括以下 partial
类即可自动创建这些对象:
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(double))]
internal partial class HybridSampleJsContext : JsonSerializerContext
{
// This type's attributes specify JSON serialization info to preserve type structure
// for trimmed builds.
}
重要
HybridSampleJsContext
类必须为 partial
,以便代码生成可以在编译项目时提供实现。 如果该类型嵌套到另一个类型中,则该类型也必须为 partial
。
调用异步 JavaScript
可以使用 EvaluateJavaScriptAsync 和 InvokeJavaScriptAsync 方法调用异步 JavaScript 方法。 在以下示例中,InvokeJavaScriptAsync 方法用于演示如何调用嵌入在应用 Web 内容中的 JavaScript。 例如,可以在 Web 内容中定义异步检索数据的 Javascript 方法:
async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
const response = await fetch("/asyncdata.txt");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
var jsonData = await response.json();
jsonData[s1] = s2;
return jsonData;
}
可以从 C# 中调用 EvaluateMeWithParamsAndAsyncReturn
方法来调用 InvokeJavaScriptAsync 方法。
Dictionary<string, string> asyncResult = await hybridWebView.InvokeJavaScriptAsync<Dictionary<string, string>>(
"EvaluateMeWithParamsAndAsyncReturn", // JavaScript method name
HybridSampleJSContext.Default.DictionaryStringString, // JSON serialization info for return type
["new_key", "new_value"], // Parameter values
[HybridSampleJSContext.Default.String, HybridSampleJSContext.Default.String]); // JSON serialization info for each parameter
在此示例中, asyncResult
包含 Dictionary<string, string>
来自 Web 请求的 JSON 数据。
方法调用需要指定 JsonTypeInfo
对象,这些对象包括操作中使用的类型的序列化信息。 在项目中包括以下 partial
类即可自动创建这些对象:
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(string))]
internal partial class HybridSampleJSContext : JsonSerializerContext
{
// This type's attributes specify JSON serialization info to preserve type structure
// for trimmed builds.
}
重要
HybridSampleJsContext
类必须为 partial
,以便代码生成可以在编译项目时提供实现。 如果该类型嵌套到另一个类型中,则该类型也必须为 partial
。
调用不返回值的 JavaScript 方法
InvokeJavaScriptAsync 方法还可用于调用不返回值的 JavaScript 方法。 有一些替代方法可以做到这一点。
调用 InvokeJavaScriptAsync,指定 JavaScript 方法名称和任何可选参数:
await hybridWebView.InvokeJavaScriptAsync("javaScriptWithVoidReturn"); // JavaScript method name
在此示例中,仅指定 JavaScript 方法名称。
在不指定泛型参数的情况下调用 InvokeJavaScriptAsync 方法:
await hybridWebView.InvokeJavaScriptAsync( "javaScriptWithParamsAndVoidReturn", // JavaScript method name HybridSampleJSContext.Default.Double, // JSON serialization info for return type [x, y], // Parameter values [HybridSampleJSContext.Default.Double, HybridSampleJSContext.Default.Double]); // JSON serialization info for each parameter
在此示例中,虽然不需要泛型参数,但仍需要为返回类型提供 JSON 序列化信息,即使它未使用。
指定泛型参数时调用 InvokeJavaScriptAsync 方法:
await hybridWebView.InvokeJavaScriptAsync<double>( "javaScriptWithParamsAndVoidReturn", // JavaScript method name null, // JSON serialization info for return type [x, y], // Parameter values [HybridSampleJSContext.Default.Double, HybridSampleJSContext.Default.Double]); // JSON serialization info for each parameter
在此示例中,需要泛型参数,
null
作为返回类型的 JSON 序列化信息的值传递。
将 JavaScript 异常发送到 .NET
默认情况下,在 HybridWebView 中调用 JavaScript 方法可以隐藏 JavaScript 代码引发的异常。 若要选择将 JavaScript 异常发送到 .NET,并在 .NET 中重新引发为异常,请将以下代码添加到 MauiProgram
类:
static MauiProgram()
{
AppContext.SetSwitch("HybridWebView.InvokeJavaScriptThrowsExceptions", true);
}
这可实现以下情况:如果 C# 代码调用 JavaScript 代码,并且 JavaScript 代码失败,则会将 JavaScript 失败发送到 .NET,该故障将作为可捕获和处理的 .NET 异常重新引发。
从 JavaScript 调用 C#
HybridWebView 中的应用 JavaScript 代码可以同步和异步调用 C# 方法,并具有可选参数和可选返回值。 可以通过以下方式来实现此目的:
- 定义将从 JavaScript 调用的公共 C# 方法。
- 调用SetInvokeJavaScriptTarget方法来设置一个对象,该对象将成为从HybridWebView进行JavaScript调用的目标。
- 从 JavaScript 调用 C# 方法。
以下示例定义用于从 JavaScript 调用的公共同步和异步方法:
public partial class MainPage : ContentPage
{
...
public void DoSyncWork()
{
Debug.WriteLine("DoSyncWork");
}
public void DoSyncWorkParams(int i, string s)
{
Debug.WriteLine($"DoSyncWorkParams: {i}, {s}");
}
public string DoSyncWorkReturn()
{
Debug.WriteLine("DoSyncWorkReturn");
return "Hello from C#!";
}
public SyncReturn DoSyncWorkParamsReturn(int i, string s)
{
Debug.WriteLine($"DoSyncWorkParamReturn: {i}, {s}");
return new SyncReturn
{
Message = "Hello from C#!" + s,
Value = i
};
}
public async Task DoAsyncWork()
{
Debug.WriteLine("DoAsyncWork");
await Task.Delay(1000);
}
public async Task DoAsyncWorkParams(int i, string s)
{
Debug.WriteLine($"DoAsyncWorkParams: {i}, {s}");
await Task.Delay(1000);
}
public async Task<String> DoAsyncWorkReturn()
{
Debug.WriteLine("DoAsyncWorkReturn");
await Task.Delay(1000);
return "Hello from C#!";
}
public async Task<SyncReturn> DoAsyncWorkParamsReturn(int i, string s)
{
Debug.WriteLine($"DoAsyncWorkParamsReturn: {i}, {s}");
await Task.Delay(1000);
return new SyncReturn
{
Message = "Hello from C#!" + s,
Value = i
};
}
public class SyncReturn
{
public string? Message { get; set; }
public int Value { get; set; }
}
}
然后,必须调用 SetInvokeJavaScriptTarget 该方法来设置对象,该对象将是以下 HybridWebView项的 JavaScript 调用的目标:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
hybridWebView.SetInvokeJavaScriptTarget(this);
}
...
}
然后,可以通过window.HybridWebView.InvokeDotNet
函数从JavaScript中调用通过SetInvokeJavaScriptTarget方法设置的对象上的公共方法。
// Synchronous methods
await window.HybridWebView.InvokeDotNet('DoSyncWork');
await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']);
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn');
const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']);
// Asynchronous methods
await window.HybridWebView.InvokeDotNet('DoAsyncWork');
await window.HybridWebView.InvokeDotNet('DoAsyncWorkParams', [123, 'hello']);
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkReturn');
const retValue = await window.HybridWebView.InvokeDotNet('DoAsyncWorkParamsReturn', [123, 'hello']);
window.HybridWebView.InvokeDotNet
JavaScript 函数使用可选参数和可选的返回值调用指定的 C# 方法。
注意
调用 window.HybridWebView.InvokeDotNet
JavaScript 函数需要应用包含 本文前面列出的HybridWebView.js JavaScript 库。