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?,是应用程序原始资产资源中包含网页应用内容的路径。 默认值为 wwwroot,映射到 Resources/Raw/wwwroot

此外,HybridWebView 定义在收到原始消息时引发的 RawMessageReceived 事件。 事件附带的 HybridWebViewRawMessageReceivedEventArgs 对象定义包含消息的 Message 属性。

应用程序的 C# 代码可以使用 HybridWebViewInvokeJavaScriptAsync 方法在 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 控件将不可用。 若要更改此行为,请参阅 剪裁功能开关

警告

在 Windows 上,使用已安装到 Program Files 目录的基于 WebView2 的控件的应用可能无法正确呈现内容。 之所以发生这种情况,是因为 WebView2 尝试将其缓存和用户数据文件写入应用的安装目录,该目录具有受限的 Program Files写入权限。 若要解决此问题,请在 WEBVIEW2_USER_DATA_FOLDER 初始化任何 WebView 控件之前设置环境变量:

#if WINDOWS
var userDataFolder = Path.Combine(FileSystem.AppDataDirectory, "WebView2");
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", userDataFolder);
#endif

将此代码放在App.xaml.cs构造函数中,或者在Platforms\Windows\App.xaml.cs在创建任何WebView控件之前。 这会指示 WebView2 在用户的 AppData 目录中使用可写位置,而不是受限的程序文件位置。

浏览器引擎

WebView 在每个平台上使用不同的浏览器引擎来呈现 Web 内容:

  • Windows:使用基于 Microsoft Edge (Chromium) 浏览器引擎的 WebView2。 这为 Edge 浏览器提供现代 Web 标准支持和一致的行为。
  • Android:使用 android.webkit.WebView,该引擎基于 Chromium 浏览器引擎。 特定版本取决于设备上安装的 Android WebView 系统组件。
  • iOS 和 Mac Catalyst:使用 WKWebView,它基于 Safari WebKit 浏览器引擎。 这是 iOS 和 macOS 上的 Safari 浏览器使用的相同引擎。

这些特定于平台的实现意味着 Web 内容可能在平台之间呈现不同,而某些特定于平台的 Web API 可能仅在某些平台上可用。 开发跨平台应用时,在所有目标平台上测试 Web 内容,以确保一致的行为。

创建 .NET MAUI HybridWebView 应用

若要使用 HybridWebView 生成 .NET MAUI 应用,需要:

  1. 打开现有的 .NET MAUI 应用项目或新建 .NET MAUI 应用项目。

  2. 将 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>
      
      <!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="_framework/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 文件中不应有任何这些文件或文件夹的条目。

  3. HybridWebView 控件添加到应用:

    <Grid RowDefinitions="Auto,*"
          ColumnDefinitions="*">
        <Button Text="Send message to JavaScript"
                Clicked="OnSendMessageButtonClicked" />
        <HybridWebView x:Name="hybridWebView"
                       RawMessageReceived="OnHybridWebViewRawMessageReceived"
                       Grid.Row="1" />
    </Grid>
    
  4. 修改 CreateMauiApp 类的 MauiProgram 方法,以便在应用程序以调试配置运行时,在基础的 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();
        }
    }
    
  5. 使用 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");
    }
    
    private void OnSendMessageButtonClicked(object sender, EventArgs e)
    {
        hybridWebView.SendRawMessage($"Hello from C#!");
    }
    
    private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
    {
        await DisplayAlertAsync("Raw Message Received", e.Message, "OK");
    }
    

    因为没有执行其他处理,所以上述消息被归类为原始消息。 还可以对消息中的数据进行编码,以执行更高级的消息传递。

从 C# 调用 JavaScript

应用的 C# 代码可以使用可选参数和可选返回值在 HybridWebView 中同步和异步调用 JavaScript 方法。 这可以通过 InvokeJavaScriptAsyncEvaluateJavaScriptAsync 方法实现:

  • EvaluateJavaScriptAsync 方法运行通过参数提供的 JavaScript 代码,并将结果作为字符串返回。
  • 该方法 InvokeJavaScriptAsync 调用指定的 JavaScript 方法,可以选择传入参数值,并指定指示返回值的类型的泛型参数。 它返回泛型参数类型的对象,该对象包含调用 JavaScript 方法的返回值。 在内部,参数和返回值已经过 JSON 编码。

注意

.NET 10 包括一个 InvokeJavaScriptAsync 重载,该重载调用指定的 JavaScript 方法而不指定有关返回类型的任何信息。 有关详细信息,请参阅 调用不返回值的 JavaScript 方法。 x

调用同步 JavaScript

可以使用 EvaluateJavaScriptAsyncInvokeJavaScriptAsync 方法调用同步 JavaScript 方法。 在以下示例中,InvokeJavaScriptAsync 方法用于演示如何调用嵌入在应用 Web 内容中的 JavaScript。 例如,可以在 Web 内容中定义用于添加两个数字的简单 Javascript 方法:

function AddNumbers(a, b) {
    return a + b;
}

可以从 C# 使用 AddNumbers 方法调用 InvokeJavaScriptAsync 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

可以使用 EvaluateJavaScriptAsyncInvokeJavaScriptAsync 方法调用异步 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 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 方法

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);
}

默认情况下,由您的JavaScript代码抛出的任何异常都会发送到.NET,然后在.NET中重新抛出为.NET异常。

这可实现以下情况:如果 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);
    }
    ...
}

然后,可以通过SetInvokeJavaScriptTarget函数从JavaScript中调用通过window.HybridWebView.InvokeDotNet方法设置的对象上的公共方法。

// 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 库。

自定义初始化和访问平台 Web 视图

注意

.NET 10 包括用于直接处理初始化开始(WebViewInitializing)和初始化结束(WebViewInitialized)的事件。

虽然 HybridWebView 不公开面向应用的初始化/初始化事件 BlazorWebView,但你仍然可以自定义基础平台 Web 视图并在它们准备就绪后运行代码:

  • Windows (WebView2):平台视图是 HybridWebView,它继承 WebView2 并添加 RunAfterInitialize(Action) ,以便你可以在它准备就绪后安全地访问 CoreWebView2
  • Android (android.webkit.WebView):创建后,通过处理程序访问和配置平台 WebView
  • iOS/Mac Catalyst (WKWebView):在创建后访问和配置平台 WKWebView 。 某些选项(例如某些 WKWebViewConfiguration 设置)必须在创建时设置;.NET MAUI 会设置这些选项的合理默认值。

创建处理程序后访问平台视图

按平台处理 HandlerChanged (或重写 OnHandlerChanged )和分支:

using Microsoft.Maui.Platform; // For MauiHybridWebView on Windows

void HybridWebView_HandlerChanged(object? sender, EventArgs e)
{
    if (sender is not HybridWebView hv || hv.Handler?.PlatformView is null)
        return;

#if WINDOWS
    if (hv.Handler.PlatformView is MauiHybridWebView winView)
    {
        winView.RunAfterInitialize(() =>
        {
            // CoreWebView2 is guaranteed to be initialized here
            winView.CoreWebView2.Settings.IsZoomControlEnabled = false;
            winView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
        });
    }
#elif ANDROID
    if (hv.Handler.PlatformView is Android.Webkit.WebView androidView)
    {
        // Safe to tweak most settings after creation
        androidView.Settings.BuiltInZoomControls = false;
        androidView.Settings.DisplayZoomControls = false;
    }
#elif IOS || MACCATALYST
    if (hv.Handler.PlatformView is WebKit.WKWebView wk)
    {
        wk.AllowsBackForwardNavigationGestures = true;
        // Many WKWebViewConfiguration options can’t be changed now – see note below
    }
#endif
}

将此连接一次,例如在 XAML 代码隐藏中:

public MainPage()
{
    InitializeComponent();
    hybridWebView.HandlerChanged += HybridWebView_HandlerChanged;
}

重要

在 iOS/Mac Catalyst 上,必须先设置某些 WKWebViewConfiguration 选项,然后才能创建视图。 默认情况下,.NET MAUI 启用常见选项(内联媒体播放、自动播放、JavaScript 等),因此典型方案无需额外的代码即可工作。 如果需要不同的创建时间选项,请使用下面的高级方法。

高级:使用自定义处理程序提供创建时配置

如果需要更改创建时选项(例如,在 iOS/Mac Catalyst 上更改 WKWebViewConfiguration ),请注册自定义处理程序并重写 CreatePlatformView

using using Microsoft.Maui.Handlers; // For HybridWebViewHandler

// In MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<HybridWebView, MyHybridWebViewHandler>();
});

// Custom handler (iOS/Mac Catalyst shown; similar ideas apply for other platforms)
public class MyHybridWebViewHandler : HybridWebViewHandler
{
#if IOS || MACCATALYST
    protected override WebKit.WKWebView CreatePlatformView()
    {
        var config = new WebKit.WKWebViewConfiguration
        {
            // Example: change defaults established by MAUI
            AllowsInlineMediaPlayback = false,
        };

        // Recreate the platform view with your configuration
        var webview = new MauiHybridWebView(this, CoreGraphics.CGRect.Empty, config);
        return webview;
    }
#endif
}

注意

创建时配置是一种高级方案。 验证每个平台上的行为,并尽可能首选初始化后调整。

公开 HybridWebView 面向应用的初始化/初始化事件,以便在基础平台 Web 视图准备就绪后自定义基础平台 Web 视图并运行代码:

在大多数情况下,只需为 WebViewInitialized 事件添加事件处理程序:

<HybridWebView WebViewInitialized="HybridWebViewInitialized" />

在代码隐藏中,可以使用该 PlatformArgs 属性访问平台功能。 此属性将提供一个实例 PlatformWebViewInitializedEventArgs ,使你能够访问控制基础 Web 视图所需的所有可用设置和配置:

private void HybridWebViewInitialized(object sender, WebViewInitializedEventArgs e)
{
    if (e.PlatformArgs is null)
        return;

#if WINDOWS
    e.PlatformArgs.Settings.IsZoomControlEnabled = false;
    e.PlatformArgs.Settings.AreDefaultContextMenusEnabled = false;
#elif ANDROID
    e.PlatformArgs.Settings.BuiltInZoomControls = false;
    e.PlatformArgs.Settings.DisplayZoomControls = false;
#elif IOS || MACCATALYST
    e.PlatformArgs.Configuration.IgnoresViewportScaleLimits = false;
#endif
}

重要

在每个平台上,在创建 Web 视图 之前 ,可能需要设置一些配置。

在大多数情况下,只需为 WebViewInitializing 事件添加事件处理程序:

<HybridWebView WebViewInitializing="HybridWebViewInitializing" />

在代码隐藏中,可以使用该 PlatformArgs 属性访问平台功能。 此属性将提供一个实例 PlatformWebViewInitializingEventArgs ,使你能够访问控制基础 Web 视图所需的所有可用设置和配置:

private void HybridWebViewInitializing(object sender, WebViewInitializingEventArgs e)
{
    if (e.PlatformArgs is null)
        return;

#if IOS || MACCATALYST
    // Example: override defaults established by .NET MAUI
    e.PlatformArgs.Configuration.AllowsInlineMediaPlayback = false;
#endif
}

截获 Web 请求

HybridWebView 可以截获和响应源自托管 Web 内容的 Web 请求。 这可实现修改标头、重定向请求或提供本地响应等方案。

若要截获 Web 请求,请处理事件 WebResourceRequested 。 在事件处理程序中,设置为Handledtrue并通过以下方式SetResponse(statusCode, statusDescription, contentType, streamOrTaskOfStream)提供响应:

<HybridWebView WebResourceRequested="HybridWebView_WebResourceRequested" />
private void HybridWebView_WebResourceRequested(object sender, WebViewWebResourceRequestedEventArgs e)
{
    // NOTE:
    // - This method MUST be synchronous; it's invoked on the WebView's thread.
    // - You MUST call SetResponse (even a minimal response) if you set Handled = true.

    // Example: serve a local image instead of the network resource
    if (e.Uri.ToString().EndsWith("sample-image.png", StringComparison.OrdinalIgnoreCase))
    {
        e.Handled = true;
        e.SetResponse(200, "OK", "image/png", GetLocalImageStreamAsync());
        return;
    }

    // Example: inject an authorization header for API calls
    if (e.Uri.Host.Equals("api.contoso.com", StringComparison.OrdinalIgnoreCase))
    {
        e.RequestHeaders["Authorization"] = $"Bearer {GetToken()}";
        // Fall through without setting Handled so the request proceeds normally
    }
}

private Task<Stream> GetLocalImageStreamAsync()
{
    // Return a stream containing PNG bytes (for example from a MauiAsset)
    return FileSystem.OpenAppPackageFileAsync("wwwroot/images/sample-image.png");
}

注意

避免长时间运行的工作。WebResourceRequested 如果设置 Handled = true,则必须立即提供响应。 对于异步内容,请传递一个 Task<Stream>SetResponse 以便 WebView 可以在流完成时继续。

常见模式包括:

  • 注入或重写特定主机的标头。
  • 返回脱机或测试方案的本地文件或内存中内容。
  • 通过返回具有适当 Location 标头的 3xx 状态代码重定向到其他 URI。

实现限制

  • Android
    • Android 不直接允许对请求进行“拦截并继续处理”。 实现的目的是通知你有请求即将发生,你可以替换整个请求,也可以选择不进行任何操作,让 Web 视图执行请求。
    • Android 不支持自定义方案。
  • iOS/Mac Catalyst
    • iOS 和 Mac Catalyst 不允许截获 httphttps 请求。
平台 截获 HTTPS 拦截自定义协议 请求修改
Android
iOS
Mac Catalyst
Windows操作系统

::: moniker-end