從 JavaScript 執行 .NET

本文說明如何使用 JS[JSImport]/[JSExport] Interop 從 JavaScript (JS) 使用 .NET。

如需其他指引,請參閱 .NET 執行階段 (dotnet/runtime) GitHub 存放庫中的設定和裝載 .NET WebAssembly 應用程式指引。 我們計畫在 2023 年或 2024 年初更新本文,在交叉連結指引中包含新的資訊。

現有的 JS 應用程式可以使用 .NET 7 或更新版本中擴充的用戶端 WebAssembly 支援,重複使用 JS 中的 .NET 程式庫或建置新型以 .NET 為基礎的應用程式和架構。

注意

本文著重於從 JS 應用程式執行 .NET,而不需要依賴 Blazor。 如需在 Blazor WebAssembly 應用程式中使用 [JSImport]/[JSExport] Interop 的指引,請參閱 JavaScript JSImport/JSExport 與 ASP.NET Core Blazor 互通。

當您只預期在 WebAssembly (WASM) 上執行時,這些方法是合適的。 程式庫可以透過呼叫 OperatingSystem.IsBrowser 進行執行階段檢查,以判斷應用程式是否在 WASM 上執行。

必要條件

.NET 7.0 SDK

安裝 .NET SDK 的最新版本。

安裝 wasm-tools 工作負載,這會帶入相關的 MSBuild 目標。

dotnet workload install wasm-tools

您可以選擇性地安裝 wasm-experimental 工作負載,其中包含在瀏覽器應用程式 (WebAssembly Browser App) 或 Node.js 型主控台應用程式 (WebAssembly Console App) 中在 WebAssembly 上開始使用 .NET 的實驗專案範本。 如果您打算將 JS[JSImport]/[JSExport] Interop 整合至現有的 JS 應用程式,則不需要此工作負載。

dotnet workload install wasm-experimental

如需詳細資訊,請參閱實驗性工作負載和專案範本一節。

Namespace

本文所述的 JS Interop API 是由 System.Runtime.InteropServices.JavaScript 命名空間中的屬性所控制。

專案組態

若要設定專案 (.csproj) 以啟用 JS Interop:

  • 目標 net7.0 或更新版本:

    <TargetFramework>net7.0</TargetFramework>
    
  • 指定 browser-wasm 作為執行階段識別碼:

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • 指定可執行檔輸出型別:

    <OutputType>Exe</OutputType>
    
  • 啟用 AllowUnsafeBlocks 屬性,此屬性允許 Roslyn 編譯器中的程式碼產生器使用指標進行 JS Interop:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    警告

    JS Interop API 需要啟用 AllowUnsafeBlocks。 在 .NET 應用程式中實作本身不安全的程式碼時請小心,這可能會帶來安全性和穩定性風險。 如需詳細資訊,請參閱不安全的程式碼、指標型別和函式指標

  • 指定 WasmMainJSPath 以指向磁碟上的檔案。 此檔案會隨應用程式一起發佈,但如果您將 .NET 整合到現有 JS 應用程式中,則不需要使用此檔案。

    在下列範例中,磁碟上的 JS 的檔案是 main.js,但任何 JS 檔案名稱都是允許的:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

設定後的範例專案檔 (.csproj):

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

WASM 上的 JavaScript interop

下列範例中的 API 是從 dotnet.js 匯入的。 這些 API 讓您能夠設定可匯入 C# 程式碼中的具名模組,並呼叫 .NET 程式碼所公開的方法,包括 Program.Main

重要

本文中所定義的「匯入」和「匯出」都是從 .NET 的觀點來看:

  • 應用程式匯入 JS 方法,以便從 .NET 呼叫它們。
  • 應用程式匯出 .NET 方法,以便從 JS 呼叫它們。

在以下範例中:

  • dotnet.js 檔案是用來建立和啟動 .NET WebAssembly 執行階段。 dotnet.js 會作為應用程式組建輸出的一部分產生,並可在 AppBundle 資料夾中找到:

    bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle

    {BUILD CONFIGURATION} 預留位置是組建組態 (例如,DebugRelease),而 {TARGET FRAMEWORK} 預留位置是目標 Framework (例如,net7.0)。

    重要

    若要與現有的應用程式整合,請複製 AppBundle 資料夾的內容,以便與應用程式的其餘部分一起提供。 對於實際執行環境部署,請在命令殼層中使用 dotnet publish -c Release 命令發佈應用程式,並使用該應用程式部署 AppBundle 資料夾。

  • dotnet.create() 設定 .NET WebAssembly 執行階段。

  • setModuleImports 將名稱與包含 JS 函式的模組相關聯,以便將其匯入 .NET。 JS 模組包含 window.location.href 函式,其會傳回目前的頁面位址 (URL)。 該模組的名稱可以是任何字串 (不一定要是檔案名稱),但必須與 JSImportAttribute 使用的名稱相符 (本文稍後說明)。 window.location.href 函式會匯入 C# 中,並由 C# 方法 GetHRef 呼叫。 本節稍後會顯示 GetHRef 方法。

  • exports.MyClass.Greeting() 從 JS 呼叫 .NET (MyClass.Greeting)。 Greeting C# 方法會傳回一個字串,其中包含呼叫 window.location.href 函式的結果。 本節稍後會顯示 Greeting 方法。

  • dotnet.run() 執行 Program.Main

JS 模組:

import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig } = 
  await dotnet.create();

setModuleImports("main.js", {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById("out").innerHTML = text;
await dotnet.run();

若要匯入 JS 函式,以便從 C# 呼叫函式,請在相符的方法簽章上使用新的 JSImportAttributeJSImportAttribute 的第一個參數是要匯入 JS 函式的名稱,而第二個參數是模組的名稱。

在下列範例中,當呼叫 GetHRef 方法時,會從 main.js 模組呼叫 window.location.href 函式:

[JSImport("window.location.href", "main.js")]
internal static partial string GetHRef();

在匯入的方法簽章中,您可以使用 .NET 型別作為參數和傳回值,這些值是由執行階段自動封送處理。 使用 JSMarshalAsAttribute<T> 來控制匯入的方法參數如何封送處理。 例如,您可以選擇將 long 封送處理為 System.Runtime.InteropServices.JavaScript.JSType.NumberSystem.Runtime.InteropServices.JavaScript.JSType.BigInt。 您可以將 Action/Func<TResult> 回呼作為參數傳遞,這些回呼會封送為可呼叫的 JS 函式。 您可以傳遞 JS 和受控物件參考,而且它們會封送處理為 Proxy 物件,讓物件在跨邊界保持運作,直到 Proxy 被垃圾回收為止。 您也可以使用 Task 結果匯入和匯出非同步方法,這些方法會按照 JS Promises 進行封送處理。 大部分封送處理的型別在雙向運作中皆可運作,作為參數和傳回值,用於匯入和匯出的方法。

下表指出支援的型別對應。

.NET JavaScript Nullable TaskPromise JSMarshalAs 選用 Array of
Boolean Boolean 支援 支援 支援 不支援
Byte Number 支援 支援 支援 支援
Char String 支援 支援 支援 不支援
Int16 Number 支援 支援 支援 不支援
Int32 Number 支援 支援 支援 支援
Int64 Number 支援 支援 不支援 不支援
Int64 BigInt 支援 支援 不支援 不支援
Single Number 支援 支援 支援 不支援
Double Number 支援 支援 支援 支援
IntPtr Number 支援 支援 支援 不支援
DateTime Date 支援 支援 不支援 不支援
DateTimeOffset Date 支援 支援 不支援 不支援
Exception Error 不支援 支援 支援 不支援
JSObject Object 不支援 支援 支援 支援
String String 不支援 支援 支援 支援
Object Any 不支援 支援 不支援 支援
Span<Byte> MemoryView 不支援 不支援 不支援 不支援
Span<Int32> MemoryView 不支援 不支援 不支援 不支援
Span<Double> MemoryView 不支援 不支援 不支援 不支援
ArraySegment<Byte> MemoryView 不支援 不支援 不支援 不支援
ArraySegment<Int32> MemoryView 不支援 不支援 不支援 不支援
ArraySegment<Double> MemoryView 不支援 不支援 不支援 不支援
Task Promise 不支援 不支援 支援 不支援
Action Function 不支援 不支援 不支援 不支援
Action<T1> Function 不支援 不支援 不支援 不支援
Action<T1, T2> Function 不支援 不支援 不支援 不支援
Action<T1, T2, T3> Function 不支援 不支援 不支援 不支援
Func<TResult> Function 不支援 不支援 不支援 不支援
Func<T1, TResult> Function 不支援 不支援 不支援 不支援
Func<T1, T2, TResult> Function 不支援 不支援 不支援 不支援
Func<T1, T2, T3, TResult> Function 不支援 不支援 不支援 不支援

下列條件適用於型別對應和封送處理值:

  • Array of 資料行會指出 .NET 型別是否可封送處理為 JSArray。 範例:C# int[] (Int32) 對應至 JSNumberArray
  • 將 JS 值傳遞至使用錯誤型別值的 C# 時,架構在大部分情況下會擲回例外狀況。 架構不會在 JS 中執行編譯時間型別檢查。
  • JSObjectExceptionTaskArraySegment 建立 GCHandle 及 Proxy。 您可以在開發人員程式碼中觸發處置,或稍後允許 .NET 記憶體回收 (GC) 處置物件。 這些型別具有顯著的效能額外負荷。
  • Array:封送處理陣列會在 JS 或 .NET 中建立陣列的複本。
  • MemoryView
    • MemoryView 是 .NET WebAssembly 執行階段的 JS 類別,用於封送處理 SpanArraySegment
    • 與封送處理陣列不同,封送處理 SpanArraySegment 不會建立基礎記憶體的複本。
    • MemoryView 只能由 .NET WebAssembly 執行階段正確具現化。 因此,無法將 JS 函式匯入為具有 SpanArraySegment 參數的 .NET 方法。
    • Span 建立的 MemoryView 只在 Interop 呼叫期間才有效。 如同 Span 在呼叫堆疊上配置,在 Interop 呼叫之後不會保存,因此無法匯出傳回 Span 的 .NET 方法。
    • 針對 ArraySegment 建立的 MemoryView 在 Interop 呼叫之後存留下來,而且有助於共用緩衝區。 在針對 ArraySegment 建立的 MemoryView 上呼叫 dispose() 會處置 Proxy,並取消釘選基礎 .NET 陣列。 我們建議在 MemoryViewtry-finally 區塊中呼叫 dispose()

您可以藉由在函式名稱中使用 globalThis 前置詞以及使用 [JSImport] 屬性來匯入可在全域命名空間上可存取的函式,而無須提供模組名稱來匯入。 在下列範例中,console.log 的前置詞為 globalThis。 匯入的函式是由 C# Log 方法呼叫,該方法接受 C# 字串訊息(message) 並將 C# 字串封送處理為console.log 的 JSString

[JSImport("globalThis.console.log")]
internal static partial void Log([JSMarshalAs<JSType.String>] string message);

若要匯出 .NET 方法,以便從 JS 呼叫它,請使用 JSExportAttribute

在下列範例中,Greeting 方法會傳回包含呼叫 GetHRef 方法結果的字串。 如先前所示,GetHref C# 方法會從 main.js 模組針對 window.location.href 函式呼叫 JS。 window.location.href 會傳回目前的頁面位址(URL):

[JSExport]
internal static string Greeting()
{
    var text = $"Hello, World! Greetings from {GetHRef()}";
    Console.WriteLine(text);

    return text;
}

實驗性工作負載和專案範本

若要示範 JS Interop 功能並取得 JS Interop 專案範本,請安裝 wasm-experimental 工作負載:

dotnet workload install wasm-experimental

wasm-experimental 工作負載包含兩個專案範本:wasmbrowserwasmconsole。 這些範本目前處於實驗階段,這表示範本的開發人員工作流程正在不斷演進。 不過,範本中使用的 .NET 和 JS API 在 .NET 7 中受到支援,並為在 JS 的 WASM 上使用 .NET 提供基礎。

瀏覽器應用程式

您可以使用 wasmbrowser 範本建立瀏覽器應用程式,以建立一個 Web 應用程式,示範如何在瀏覽器中一起使用 .NET 和 JS:

dotnet new wasmbrowser

從 Visual Studio 或使用 .NET CLI 建置應用程式:

dotnet build

建置的應用程式位於 bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle 目錄中。 {BUILD CONFIGURATION} 預留位置是組建組態 (例如,DebugRelease)。 {TARGET FRAMEWORK} 預留位置是目標 Framework Moniker (例如,net7.0)。

從 Visual Studio 或使用 .NET CLI 建置並執行應用程式:

dotnet run

或者,從 AppBundle 目錄啟動任何靜態檔案伺服器:

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/browser-wasm/AppBundle

在上述範例中,{TARGET FRAMEWORK} 預留位置是目標 Framework Moniker (例如,net7.0)。

Node.js 主控台應用程式

您可以使用 wasmconsole 範本建立主控台應用程式,以建立在 WASM 下作為 Node.jsV8 主控台應用程式執行的應用程式:

dotnet new wasmconsole

從 Visual Studio 或使用 .NET CLI 建置應用程式:

dotnet build

建置的應用程式位於 bin/{BUILD CONFIGURATION}/{TARGET FRAMEWORK}/browser-wasm/AppBundle 目錄中。 {BUILD CONFIGURATION} 預留位置是組建組態 (例如,DebugRelease)。 {TARGET FRAMEWORK} 預留位置是目標 Framework Moniker (例如,net7.0)。

從 Visual Studio 或使用 .NET CLI 建置並執行應用程式:

dotnet run

或者,從 AppBundle 目錄啟動任何靜態檔案伺服器:

node bin/$(Configuration)/{TARGET FRAMEWORK}/browser-wasm/AppBundle/main.mjs

在上述範例中,{TARGET FRAMEWORK} 預留位置是目標 Framework Moniker (例如,net7.0)。

其他資源