抽离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 文件,引入组件命名空间

xmlns:wpf="clr-namespace:HSoft.WebView.NetFramework.WPF;assembly=HSoft.WebView.NetFramework.WPF" 

使用组件

<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 version="1.0" encoding="utf-8"?> <Project>     <!--...-->     <ItemGroup>     <EmbeddedResource Include="wwwroot***">     </EmbeddedResource>     </ItemGroup>     <!--...--> </Project> 

你的网页启动页面位置如果是这样的wwwrootindex.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。

import { DotNet } from "@microsoft/dotnet-js-interop";  let dispatcher: DotNet.ICallDispatcher; dispatcher = DotNet.attachDispatcher({   sendByteArray: sendByteArray,   beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,   endInvokeJSFromDotNet: endInvokeJSFromDotNet, }); 

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

  • sendByteArray(当传递参数中含有字节数组的时候调用这个)
  • beginInvokeDotNetFromJS(从 JS 调用.Net 方法)
  • endInvokeJSFromDotNet(从.Net 调用 JS,JS 这边处理完毕需要调用此方法告知.Net 调用完毕)

sendByteArray

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

beginInvokeDotNetFromJS

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

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

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

(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 对象增加属性

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

完整代码

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

2、.Net

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

发表评论

您必须 [ 登录 ] 才能发表留言!

相关文章