HybridWebView
.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?
,是包含 Web 应用内容的应用原始资产资源中的路径。 默认值为 wwwroot,映射到 Resources/Raw/wwwroot。
此外,HybridWebView 定义在收到原始消息时引发的 RawMessageReceived 事件。 事件附带的 HybridWebViewRawMessageReceivedEventArgs 对象定义包含消息的 Message 属性。
应用的 C# 代码可以使用 InvokeJavaScriptAsync 和 EvaluateJavaScriptAsync 方法在 HybridWebView 中调用同步和异步 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 视图控件中,并在应用的上下文中运行。 应用的任何部分都可以访问外部 Web 服务,但这不是必须的。
重要
默认情况下, HybridWebView 启用完全修整或本机 AOT 时,该控件将不可用。 若要更改此行为,请参阅 剪裁功能开关。
创建 .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); } </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> 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": function __InvokeJavaScript(taskId, methodName, args) { if (methodName[Symbol.toStringTag] === 'AsyncFunction') { // For async methods, we need to call the method and then trigger the callback when it's done const asyncPromise = methodName(...args); asyncPromise .then(asyncResult => { window.HybridWebView.__TriggerAsyncCallback(taskId, asyncResult); }) .catch(error => console.error(error)); } else { // For sync methods, we can call the method and trigger the callback immediately const syncResult = methodName(...args); window.HybridWebView.__TriggerAsyncCallback(taskId, syncResult); } }, "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { // Make sure the result is a string if (result && typeof (result) !== 'string') { result = JSON.stringify(result); } window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + result); } } 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>
修改类的方法
MauiProgram
,CreateMauiApp
以便在应用在调试配置中运行时在基础 WebView 控件上启用开发人员工具。 为此,请 AddHybridWebViewDeveloperTools 对 IServiceCollection 对象调用该方法: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 编码。
调用同步 JavaScript
可以使用 EvaluateJavaScriptAsync 和 InvokeJavaScriptAsync 方法调用同步 JavaScript 方法。 在以下示例中,InvokeJavaScriptAsync 方法用于演示如何调用嵌入在应用 Web 内容中的 JavaScript。 例如,可以在 Web 内容中定义用于添加两个数字的简单 Javascript 方法:
function AddNumbers(a, b) {
return a + b;
}
可以使用 InvokeJavaScriptAsync 方法从 C# 中调用 AddNumbers
方法:
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;
}
可以使用 InvokeJavaScriptAsync 方法从 C# 中调用 EvaluateMeWithParamsAndAsyncReturn
JavaScript 方法:
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 调用 C#
应用内的 HybridWebView JavaScript 代码可以同步调用 C# 方法,并具有可选参数和可选返回值。 可以通过以下方式来实现此目的:
- 定义将从 JavaScript 调用的公共 C# 方法。
- SetInvokeJavaScriptTarget调用该方法以设置将成为 JavaScript HybridWebView调用的目标的对象。
- 从 JavaScript 调用 C# 方法。
重要
目前不支持从 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 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);
}
...
}
然后,可以使用函数从 JavaScript window.HybridWebView.InvokeDotNet
调用对象集SetInvokeJavaScriptTarget上的公共方法:
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']);
window.HybridWebView.InvokeDotNet
JavaScript 函数使用可选参数和可选的返回值调用指定的 C# 方法。
注意
调用 window.HybridWebView.InvokeDotNet
JavaScript 函数需要应用包含 本文前面列出的HybridWebView.js JavaScript 库。