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

曂沅仴駦  论坛元老 | 2025-4-1 19:54:30 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1783|帖子 1783|积分 5349

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
做这个的目的是想利用 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 文件,引入组件命名空间
  1. xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF"
复制代码
利用组件
  1. <Window
  2.     x:
  3.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.     xmlns:local="clr-namespace:TestWVF"
  7.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8.     xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF"
  9.     Title="MainWindow"
  10.     Width="800"
  11.     Height="450"
  12.     mc:Ignorable="d">
  13.     <Grid>
  14.         <wpf:WebView Source="http://localhost:5173" />
  15.     </Grid>
  16. </Window>
复制代码
如果是开辟模式下,Source 填写你的前端服务器地址,生产环境,则一样平常填写http://0.0.0.0/index.html。项目新增一个 wwwroot 目录,然后编辑项目文件,添加如下节点,以便把网页文件嵌入程序集。
  1. <Window
  2.     x:
  3.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.     xmlns:local="clr-namespace:TestWVF"
  7.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8.     xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF"
  9.     Title="MainWindow"
  10.     Width="800"
  11.     Height="450"
  12.     mc:Ignorable="d">
  13.     <Grid>
  14.         <wpf:WebView Source="http://localhost:5173" />
  15.     </Grid>
  16. </Window>        
复制代码
你的网页启动页面位置如果是这样的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。
  1. import { DotNet } from "@microsoft/dotnet-js-interop";
  2. let dispatcher: DotNet.ICallDispatcher;
  3. dispatcher = DotNet.attachDispatcher({
  4.   sendByteArray: sendByteArray,
  5.   beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,
  6.   endInvokeJSFromDotNet: endInvokeJSFromDotNet,
  7. });
复制代码
重要实现三个函数,这三个函数利用 postMessage 发送消息到.Net 端。

  • sendByteArray(当传递参数中含有字节数组的时候调用这个)
  • beginInvokeDotNetFromJS(从 JS 调用.Net 方法)
  • endInvokeJSFromDotNet(从.Net 调用 JS,JS 这边处理完毕需要调用此方法告知.Net 调用完毕)
sendByteArray
  1. function sendByteArray(id: number, data: Uint8Array): void {
  2.   const dataBase64Encoded = base64EncodeByteArray(data);
  3.   (window as any).chrome.webview.postMessage([
  4.     "ReceiveByteArrayFromJS",
  5.     id,
  6.     dataBase64Encoded,
  7.   ]);
  8. }
复制代码
beginInvokeDotNetFromJS
  1. function beginInvokeDotNetFromJS(
  2.   callId: number,
  3.   assemblyName: string | null,
  4.   methodIdentifier: string,
  5.   dotNetObjectId: number | null,
  6.   argsJson: string
  7. ): void {
  8.   console.log("beginInvokeDotNetFromJS");
  9.   (window as any).chrome.webview.postMessage([
  10.     "beginInvokeDotNetFromJS",
  11.     callId ? callId.toString() : null,
  12.     assemblyName,
  13.     methodIdentifier,
  14.     dotNetObjectId || 0,
  15.     argsJson,
  16.   ]);
  17. }
复制代码
endInvokeJSFromDotNet
  1. function endInvokeJSFromDotNet(
  2.   callId: number,
  3.   succeeded: boolean,
  4.   resultOrError: any
  5. ): void {
  6.   console.log("beginInvokeDotNetFromJS");
  7.   (window as any).chrome.webview.postMessage([
  8.     "endInvokeJSFromDotNet",
  9.     callId ? callId.toString() : null,
  10.     succeeded,
  11.     resultOrError,
  12.   ]);
  13. }
复制代码
工具函数
  1. function base64EncodeByteArray(data: Uint8Array) {
  2.   // Base64 encode a (large) byte array
  3.   // Note `btoa(String.fromCharCode.apply(null, data as unknown as number[]));`
  4.   // isn't sufficient as the `apply` over a large array overflows the stack.
  5.   const charBytes = new Array(data.length);
  6.   for (var i = 0; i < data.length; i++) {
  7.     charBytes[i] = String.fromCharCode(data[i]);
  8.   }
  9.   const dataBase64Encoded = btoa(charBytes.join(""));
  10.   return dataBase64Encoded;
  11. }
  12. // https://stackoverflow.com/a/21797381
  13. // TODO: If the data is large, consider switching over to the native decoder as in https://stackoverflow.com/a/54123275
  14. // But don't force it to be async all the time. Yielding execution leads to perceptible lag.
  15. function base64ToArrayBuffer(base64: string): Uint8Array {
  16.   const binaryString = atob(base64);
  17.   const length = binaryString.length;
  18.   const result = new Uint8Array(length);
  19.   for (let i = 0; i < length; i++) {
  20.     result[i] = binaryString.charCodeAt(i);
  21.   }
  22.   return result;
  23. }
复制代码
接收来自.Net 的消息并处理
  1. (window as any).chrome.webview.addEventListener("message", (e: any) => {
  2.   var ob = JSON.parse(e.data);
  3.   switch (ob[0]) {
  4.     case "EndInvokeDotNet": {
  5.       dispatcher.endInvokeDotNetFromJS(ob[1], ob[2], ob[3]);
  6.       break;
  7.     }
  8.     case "BeginInvokeJS": {
  9.       dispatcher.beginInvokeJSFromDotNet(ob[1], ob[2], ob[3], ob[4], ob[5]);
  10.       break;
  11.     }
  12.     case "SendByteArrayToJS": {
  13.       let id = ob[1];
  14.       let base64Data = ob[2];
  15.       const data = base64ToArrayBuffer(base64Data);
  16.       dispatcher.receiveByteArray(id,data);
  17.       break;
  18.     }
  19.     default: {
  20.       console.error(`不支持的消息类型${e.data}`);
  21.     }
  22.   }
  23. });
复制代码
window 对象增长属性
  1. (window as any)["DotNet"] = DotNet;
  2. export { DotNet };
复制代码
完整代码
  1. import { DotNet } from "@microsoft/dotnet-js-interop";
  2. let dispatcher: DotNet.ICallDispatcher;
  3. dispatcher = DotNet.attachDispatcher({
  4.   sendByteArray: sendByteArray,
  5.   beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,
  6.   endInvokeJSFromDotNet: endInvokeJSFromDotNet,
  7. });function sendByteArray(id: number, data: Uint8Array): void {    const dataBase64Encoded = base64EncodeByteArray(data);    (window as any).chrome.webview.postMessage([      "ReceiveByteArrayFromJS",      id,      dataBase64Encoded,    ]);  }function beginInvokeDotNetFromJS(
  8.   callId: number,
  9.   assemblyName: string | null,
  10.   methodIdentifier: string,
  11.   dotNetObjectId: number | null,
  12.   argsJson: string
  13. ): void {
  14.   console.log("beginInvokeDotNetFromJS");
  15.   (window as any).chrome.webview.postMessage([
  16.     "beginInvokeDotNetFromJS",
  17.     callId ? callId.toString() : null,
  18.     assemblyName,
  19.     methodIdentifier,
  20.     dotNetObjectId || 0,
  21.     argsJson,
  22.   ]);
  23. }function endInvokeJSFromDotNet(
  24.   callId: number,
  25.   succeeded: boolean,
  26.   resultOrError: any
  27. ): void {
  28.   console.log("beginInvokeDotNetFromJS");
  29.   (window as any).chrome.webview.postMessage([
  30.     "endInvokeJSFromDotNet",
  31.     callId ? callId.toString() : null,
  32.     succeeded,
  33.     resultOrError,
  34.   ]);
  35. }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;
  36. export { DotNet };
复制代码
2、.Net

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曂沅仴駦

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表