抽離BlazorWebview中的.Net與Javascript的互操作庫(kù)
做這個(gè)的目的是想使用 Blazor 中的 Javascript 與 C#中的互操作,但是又不需要加載 Blazor 整個(gè)類庫(kù),另外 BlazorWebView 組件沒有支持直接通過 Http 協(xié)議加載 web 頁(yè)面,調(diào)試的時(shí)候需要先把后端接口寫好,然后前端打包,然后一起調(diào)試,感覺很麻煩,因此想能不能把互操作這部分功能單獨(dú)抽離出來(lái)。后面研究了 asp.net core 關(guān)于這部分的源碼,發(fā)現(xiàn)可行,于是抽離出來(lái)了這部分功能,由于 Microsoft.JSInterop 這個(gè) nuget 包不支持.Net Framework,順便還移植到了.Net Framework 平臺(tái)。正常使用已將近 1 年。現(xiàn)寫文章記錄回憶一下,也給有需要的朋友研究研究。
一、如何使用
帶互操作的 WebView 已經(jīng)支持了.Net Framework 下的 WPF 和 MAUI 中的安卓端。工作上需要這兩個(gè),其他平臺(tái)暫時(shí)不支持。官方 nuget 倉(cāng)庫(kù)上,上傳了最近一個(gè) WPF 的版本。
1、安裝
使用 nuget 包管理器搜索HSoft.WebView.NetFramework.WPF然后安裝即可。
2、引入 Webview 組件
打開一個(gè) 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>
如果是開發(fā)模式下,Source 填寫你的前端服務(wù)器地址,生產(chǎn)環(huán)境,則一般填寫http://0.0.0.0/index.html。項(xiàng)目新增一個(gè) wwwroot 目錄,然后編輯項(xiàng)目文件,添加如下節(jié)點(diǎn),以便把網(wǎng)頁(yè)文件嵌入程序集。
<?xml version="1.0" encoding="utf-8"?>
<Project>
<!--...-->
<ItemGroup>
<EmbeddedResource Include="wwwroot\**\*">
</EmbeddedResource>
</ItemGroup>
<!--...-->
</Project>
你的網(wǎng)頁(yè)啟動(dòng)頁(yè)面位置如果是這樣的wwwroot\index.html,則對(duì)應(yīng)的Source為http://0.0.0.0/index.html。
二、原理
開門見山,借助 Microsoft.JSInterop 和前端的@microsoft/dotnet-js-interop 包,便可實(shí)現(xiàn) Javascript和C#的互操作。這兩個(gè)包定義除信息傳遞通道之外的所有必要的信息。因此,我們只需要把傳送通道給補(bǔ)充上就可以正常工作。直接使用 Webview2 組件的 IPC 通訊,也就是 chrome.webview.postMessage 和 chrome.webview.addEventListener("message", (e: any))來(lái)發(fā)送和接受消息。
1、Javascript
在前端引入@microsoft/dotnet-js-interop 包。使用 DotNet.attachDispatcher 創(chuàng)建 dispatcher。
import { DotNet } from "@microsoft/dotnet-js-interop";
let dispatcher: DotNet.ICallDispatcher;
dispatcher = DotNet.attachDispatcher({
sendByteArray: sendByteArray,
beginInvokeDotNetFromJS: beginInvokeDotNetFromJS,
endInvokeJSFromDotNet: endInvokeJSFromDotNet,
});
主要實(shí)現(xiàn)三個(gè)函數(shù),這三個(gè)函數(shù)使用 postMessage 發(fā)送消息到.Net 端。
- sendByteArray(當(dāng)傳遞參數(shù)中含有字節(jié)數(shù)組的時(shí)候調(diào)用這個(gè))
- beginInvokeDotNetFromJS(從 JS 調(diào)用.Net 方法)
- endInvokeJSFromDotNet(從.Net 調(diào)用 JS,JS 這邊處理完畢需要調(diào)用此方法告知.Net 調(diào)用完畢)
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,
]);
}
工具函數(shù)
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;
}
接收來(lái)自.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 對(duì)象增加屬性
(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 方法來(lái)與前端通訊,后端通過 WebMessageReceived 處理來(lái)自前端的beginInvokeDotNetFromJS、endInvokeJSFromDotNet、ReceiveByteArrayFromJS的消息,然后通過靜態(tài)類 DotNetDispatcher 中的 BeginInvokeDotNet、EndInvokeJS、ReceiveByteArray 來(lái)處理,通過繼承 JSRuntime,實(shí)現(xiàn) BeginInvokeJS、EndInvokeDotNet、SendByteArray 方法,通過 PostWebMessageAsString 發(fā)送數(shù)據(jù)到前端。在這里不給出代碼,感興趣的直接查看 https://github.com/HekunX/wvf 倉(cāng)庫(kù)。

浙公網(wǎng)安備 33010602011771號(hào)