抽离BlazorWebview中的.Net与Javascript的互操作库

做这个的目的是想使用 Blazor 中的 Javascript 与 C#中的互操作,但是又不需要加载 Blazor 整个类库,另外 BlazorWebView 组件没有支持直接通过 Http 协议加载 web 页面,调试的时候需要先把后端接口写好,然后前端打包,然后一起调试,感觉很麻烦,因此想能不能把互操作这部分功能单独抽离出来。后面研究了 asp.net core 关于这部分的源码,发现可行,于是抽离出来了这部分功能,由于 Microsoft.JSInterop 这个 nuget 包不支持.Net Framework,顺便还移植到了.Net Framework 平台。正常使用已将近 1 年。现写文章记录回忆一下,也给有需要的朋友研究研究。

一、如何使用

带互操作的 WebView 已经支持了.Net Framework 下的 WPF 和 MAUI 中的安卓端。工作上需要这两个,其他平台暂时不支持。官方 nuget 仓库上,上传了最近一个 WPF 的版本。

1、安装

使用 nuget 包管理器搜索HSoft.WebView.NetFramework.WPF然后安装即可。

2、引入 Webview 组件

打开一个 xaml 文件,引入组件命名空间

xml 复制代码
xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF"

使用组件

xml 复制代码
<Window
    x:Class="TestWVF.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:TestWVF"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <wpf:WebView Source="http://localhost:5173" />
    </Grid>
</Window>

如果是开发模式下,Source 填写你的前端服务器地址,生产环境,则一般填写http://0.0.0.0/index.html。项目新增一个 wwwroot 目录,然后编辑项目文件,添加如下节点,以便把网页文件嵌入程序集。

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<Project>
    <!--...-->
    <ItemGroup>
    <EmbeddedResource Include="wwwroot\**\*">
    </EmbeddedResource>
    </ItemGroup>
    <!--...-->
</Project>

你的网页启动页面位置如果是这样的wwwroot\index.html,则对应的Source为http://0.0.0.0/index.html。

二、原理

开门见山,借助 Microsoft.JSInterop 和前端的@microsoft/dotnet-js-interop 包,便可实现 Javascript和C#的互操作。这两个包定义除信息传递通道之外的所有必要的信息。因此,我们只需要把传送通道给补充上就可以正常工作。直接使用 Webview2 组件的 IPC 通讯,也就是 chrome.webview.postMessage 和 chrome.webview.addEventListener("message", (e: any))来发送和接受消息。

1、Javascript

在前端引入@microsoft/dotnet-js-interop 包。使用 DotNet.attachDispatcher 创建 dispatcher。

javascript 复制代码
import { DotNet } from "@microsoft/dotnet-js-interop";

let dispatcher: DotNet.ICallDispatcher;
dispatcher = DotNet.attachDispatcher({
  sendByteArray: sendByteArray,
  beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,
  endInvokeJSFromDotNet: endInvokeJSFromDotNet,
});

主要实现三个函数,这三个函数使用 postMessage 发送消息到.Net 端。

sendByteArray

javascript 复制代码
function sendByteArray(id: number, data: Uint8Array): void {
  const dataBase64Encoded = base64EncodeByteArray(data);
  (window as any).chrome.webview.postMessage([
    "ReceiveByteArrayFromJS",
    id,
    dataBase64Encoded,
  ]);
}

beginInvokeDotNetFromJS

javascript 复制代码
function beginInvokeDotNetFromJS(
  callId: number,
  assemblyName: string | null,
  methodIdentifier: string,
  dotNetObjectId: number | null,
  argsJson: string
): void {
  console.log("beginInvokeDotNetFromJS");
  (window as any).chrome.webview.postMessage([
    "beginInvokeDotNetFromJS",
    callId ? callId.toString() : null,
    assemblyName,
    methodIdentifier,
    dotNetObjectId || 0,
    argsJson,
  ]);
}

endInvokeJSFromDotNet

javascript 复制代码
function endInvokeJSFromDotNet(
  callId: number,
  succeeded: boolean,
  resultOrError: any
): void {
  console.log("beginInvokeDotNetFromJS");
  (window as any).chrome.webview.postMessage([
    "endInvokeJSFromDotNet",
    callId ? callId.toString() : null,
    succeeded,
    resultOrError,
  ]);
}

工具函数

javascript 复制代码
function base64EncodeByteArray(data: Uint8Array) {
  // Base64 encode a (large) byte array
  // Note `btoa(String.fromCharCode.apply(null, data as unknown as number[]));`
  // isn't sufficient as the `apply` over a large array overflows the stack.
  const charBytes = new Array(data.length);
  for (var i = 0; i < data.length; i++) {
    charBytes[i] = String.fromCharCode(data[i]);
  }
  const dataBase64Encoded = btoa(charBytes.join(""));
  return dataBase64Encoded;
}

// https://stackoverflow.com/a/21797381
// TODO: If the data is large, consider switching over to the native decoder as in https://stackoverflow.com/a/54123275
// But don't force it to be async all the time. Yielding execution leads to perceptible lag.
function base64ToArrayBuffer(base64: string): Uint8Array {
  const binaryString = atob(base64);
  const length = binaryString.length;
  const result = new Uint8Array(length);
  for (let i = 0; i < length; i++) {
    result[i] = binaryString.charCodeAt(i);
  }
  return result;
}

接收来自.Net 的消息并处理

javascript 复制代码
(window as any).chrome.webview.addEventListener("message", (e: any) => {
  var ob = JSON.parse(e.data);

  switch (ob[0]) {
    case "EndInvokeDotNet": {
      dispatcher.endInvokeDotNetFromJS(ob[1], ob[2], ob[3]);
      break;
    }
    case "BeginInvokeJS": {
      dispatcher.beginInvokeJSFromDotNet(ob[1], ob[2], ob[3], ob[4], ob[5]);
      break;
    }
    case "SendByteArrayToJS": {
      let id = ob[1];
      let base64Data = ob[2];
      const data = base64ToArrayBuffer(base64Data);
      dispatcher.receiveByteArray(id,data);
      break;
    }
    default: {
      console.error(`不支持的消息类型${e.data}`);
    }
  }
});

window 对象增加属性

javascript 复制代码
(window as any)["DotNet"] = DotNet;
export { DotNet };

完整代码

javascript 复制代码
import { DotNet } from "@microsoft/dotnet-js-interop";

let dispatcher: DotNet.ICallDispatcher;
dispatcher = DotNet.attachDispatcher({
  sendByteArray: sendByteArray,
  beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,
  endInvokeJSFromDotNet: endInvokeJSFromDotNet,
});


function sendByteArray(id: number, data: Uint8Array): void {
    const dataBase64Encoded = base64EncodeByteArray(data);
    (window as any).chrome.webview.postMessage([
      "ReceiveByteArrayFromJS",
      id,
      dataBase64Encoded,
    ]);
  }

function beginInvokeDotNetFromJS(
  callId: number,
  assemblyName: string | null,
  methodIdentifier: string,
  dotNetObjectId: number | null,
  argsJson: string
): void {
  console.log("beginInvokeDotNetFromJS");
  (window as any).chrome.webview.postMessage([
    "beginInvokeDotNetFromJS",
    callId ? callId.toString() : null,
    assemblyName,
    methodIdentifier,
    dotNetObjectId || 0,
    argsJson,
  ]);
}

function endInvokeJSFromDotNet(
  callId: number,
  succeeded: boolean,
  resultOrError: any
): void {
  console.log("beginInvokeDotNetFromJS");
  (window as any).chrome.webview.postMessage([
    "endInvokeJSFromDotNet",
    callId ? callId.toString() : null,
    succeeded,
    resultOrError,
  ]);
}


function base64EncodeByteArray(data: Uint8Array) {
  // Base64 encode a (large) byte array
  // Note `btoa(String.fromCharCode.apply(null, data as unknown as number[]));`
  // isn't sufficient as the `apply` over a large array overflows the stack.
  const charBytes = new Array(data.length);
  for (var i = 0; i < data.length; i++) {
    charBytes[i] = String.fromCharCode(data[i]);
  }
  const dataBase64Encoded = btoa(charBytes.join(""));
  return dataBase64Encoded;
}
// https://stackoverflow.com/a/21797381
// TODO: If the data is large, consider switching over to the native decoder as in https://stackoverflow.com/a/54123275
// But don't force it to be async all the time. Yielding execution leads to perceptible lag.
function base64ToArrayBuffer(base64: string): Uint8Array {
    const binaryString = atob(base64);
    const length = binaryString.length;
    const result = new Uint8Array(length);
    for (let i = 0; i < length; i++) {
      result[i] = binaryString.charCodeAt(i);
    }
    return result;
  }
(window as any).chrome.webview.addEventListener("message", (e: any) => {
    var ob = JSON.parse(e.data);

    switch (ob[0]) {
      case "EndInvokeDotNet": {
        dispatcher.endInvokeDotNetFromJS(ob[1], ob[2], ob[3]);
        break;
      }
      case "BeginInvokeJS": {
        dispatcher.beginInvokeJSFromDotNet(ob[1], ob[2], ob[3], ob[4], ob[5]);
        break;
      }
      case "SendByteArrayToJS": {
        let id = ob[1];
        let base64Data = ob[2];
        const data = base64ToArrayBuffer(base64Data);
        dispatcher.receiveByteArray(id,data);
        break;
      }
      default: {
        console.error(`不支持的消息类型${e.data}`);
      }
    }
  });

(window as any)["DotNet"] = DotNet;
export { DotNet };

二、.Net

在.Net 这边类似,使用 WebView2 的 WebMessageReceived 事件和 PostWebMessageAsString 方法来与前端通讯,后端通过 WebMessageReceived 处理来自前端的beginInvokeDotNetFromJSendInvokeJSFromDotNetReceiveByteArrayFromJS 的消息,然后通过静态类 DotNetDispatcher 中的 BeginInvokeDotNet、EndInvokeJS、ReceiveByteArray 来处理,通过继承 JSRuntime,实现 BeginInvokeJS、EndInvokeDotNet、SendByteArray 方法,通过 PostWebMessageAsString 发送数据到前端。在这里不给出代码,感兴趣的直接查看 https://github.com/HekunX/wvf 仓库。

相关推荐
EanoJiang2 小时前
CSharp_base
c#
xiaowu0803 小时前
C# 使用Windows API实现键盘钩子的类
windows·c#·计算机外设
十步杀一人_千里不留行7 小时前
面向 C# 初学者的完整教程
开发语言·c#
LcVong8 小时前
一篇文章学会开发第一个ASP.NET网页
后端·c#·asp.net·web
刘梦凡呀10 小时前
C# .NET Core 批量下载文件
windows·c#·.netcore
码观天工14 小时前
C#高性能开发之类型系统:从C# 7.0 到C# 14的类型系统演进全景
性能优化·c#·.net·memory·高性能·record·c#14·类型系统
程序员秘密基地14 小时前
基于c#,wpf,ef框架,sql server数据库,音乐播放器
sql·sqlserver·c#·.net·wpf
钢铁男儿15 小时前
C#内存管理深度解析:值类型与引用类型全解析
前端·算法·c#
董先生_ad986ad16 小时前
C# 全局 Mutex 是否需使用 `Global\` 前缀
开发语言·c#
小浪学编程16 小时前
C#学习1_认识项目/程序结构
开发语言·前端·学习·c#·游戏引擎