從 JavaScript 執行 .NET
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本。
本文說明如何使用 JS[JSImport]
/[JSExport]
Interop 從 JavaScript (JS) 使用 .NET。
如需其他指引,請參閱 .NET 執行階段 (dotnet/runtime
) GitHub 存放庫中的設定和裝載 .NET WebAssembly 應用程式指引。
現有的 JS 應用程式可以使用擴充的用戶端 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 SDK 的最新版本。
在系統管理命令殼層中安裝 wasm-tools
工作負載,這會帶入相關的 MSBuild 目標:
dotnet workload install wasm-tools
這些工具也可以透過 Visual Studio 安裝程式,在 Visual Studio 安裝程式中的 ASP.NET 和網頁程式開發下進行安裝。 從選擇性元件的清單選取 [.NET WebAssembly 建置工具] 選項。
您可以選擇性地安裝 wasm-experimental
工作負載,其中包含在瀏覽器應用程式 (WebAssembly Browser App) 或 Node.js 型主控台應用程式 (WebAssembly Console App) 中在 WebAssembly 上開始使用 .NET 的實驗專案範本。 如果您打算將 JS[JSImport]
/[JSExport]
Interop 整合至現有的 JS 應用程式,則不需要此工作負載。
dotnet workload install wasm-experimental
您也可以使用下列命令,從 Microsoft.NET.Runtime.WebAssembly.Templates
NuGet 套件安裝範本:
dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates
如需詳細資訊,請參閱實驗性工作負載和專案範本一節。
Namespace
本文所述的 JS Interop API 是由 System.Runtime.InteropServices.JavaScript 命名空間中的屬性所控制。
專案組態
若要設定專案 (.csproj
) 以啟用 JS Interop:
設定目標 Framework Moniker (
{TARGET FRAMEWORK}
預留位置):<TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
支援 .NET 7 (
net7.0
) 或更新版本。啟用 AllowUnsafeBlocks 屬性,此屬性允許 Roslyn 編譯器中的程式碼產生器使用指標進行 JS Interop:
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
警告
JS Interop API 需要啟用 AllowUnsafeBlocks。 在 .NET 應用程式中實作本身不安全的程式碼時請小心,這可能會帶來安全性和穩定性風險。 如需詳細資訊,請參閱不安全的程式碼、指標型別和函式指標。
下列是設定後的範例專案檔 (.csproj
)。 {TARGET FRAMEWORK}
預留位置是目標架構:
<Project Sdk="Microsoft.NET.Sdk.WebAssembly">
<PropertyGroup>
<TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
-
<TargetFramework>net7.0</TargetFramework>
支援 .NET 7 (
net7.0
) 或更新版本。 指定
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
將作為應用程式建置輸出的一部分產生。重要
若要與現有的應用程式整合,請將發佈輸出資料夾†的內容複製到現有的應用程式部署資產,以便與應用程式的其餘部分一起提供。 對於實際執行環境部署,請在命令殼層中使用
dotnet publish -c Release
命令發佈應用程式,並使用該應用程式部署輸出資料夾的內容。†發佈輸出資料夾是發佈設定檔的目標位置。 .NET 8 或更新版本中的 Release 設定檔預設值為
bin/Release/{TARGET FRAMEWORK}/publish
,其中{TARGET FRAMEWORK}
預留位置是目標架構 (例如net8.0
)。dotnet.create()
設定 .NET WebAssembly 執行階段。
setModuleImports
將名稱與包含 JS 函式的模組相關聯,以便將其匯入 .NET。 JS 模組包含接受的dom.setInnerText
函數,以及元素選取器和顯目 UI 目前 stopwatch 時間。 該模組的名稱可以是任何字串 (不一定要是檔案名稱),但必須與JSImportAttribute
使用的名稱相符 (本文稍後說明)。dom.setInnerText
函式會匯入 C# 中,並由 C# 方法SetInnerText
呼叫。 本節稍後會顯示SetInnerText
方法。exports.StopwatchSample.Reset()
從 JS 呼叫 .NET (StopwatchSample.Reset
)。Reset
C# 方法會在執行時重新啟動 stopwatch,或在未執行時將其重設。 本節稍後會顯示Reset
方法。exports.StopwatchSample.Toggle()
從 JS 呼叫 .NET (StopwatchSample.Toggle
)。Toggle
C# 方法會根據目前是否執行,啟動或停止 stopwatch。 本節稍後會顯示Toggle
方法。runMain()
執行Program.Main
。
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 './_framework/dotnet.js'
const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
.withApplicationArguments("start")
.create();
setModuleImports('main.js', {
dom: {
setInnerText: (selector, time) =>
document.querySelector(selector).innerText = time
}
});
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
document.getElementById('reset').addEventListener('click', e => {
exports.StopwatchSample.Reset();
e.preventDefault();
});
const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
const isRunning = exports.StopwatchSample.Toggle();
pauseButton.innerText = isRunning ? 'Pause' : 'Start';
e.preventDefault();
});
await runMain();
import { dotnet } from './_framework/dotnet.js'
const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.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();
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# 呼叫函式,請在相符的方法簽章上使用新的 JSImportAttribute。 JSImportAttribute 的第一個參數是要匯入 JS 函式的名稱,而第二個參數是模組的名稱。
在下列範例中,當呼叫 SetInnerText
方法時,會從 main.js
模組呼叫 dom.setInnerText
函式:
[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);
在下列範例中,當呼叫 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.Number 或 System.Runtime.InteropServices.JavaScript.JSType.BigInt。 您可以將 Action/Func<TResult> 回呼作為參數傳遞,這些回呼會封送為可呼叫的 JS 函式。 您可以傳遞 JS 和受控物件參考,而且它們會封送處理為 Proxy 物件,讓物件在跨邊界保持運作,直到 Proxy 被垃圾回收為止。 您也可以使用 Task 結果匯入和匯出非同步方法,這些方法會按照 JS Promises 進行封送處理。 大部分封送處理的型別在雙向運作中皆可運作,作為參數和傳回值,用於匯入和匯出的方法。
下表指出支援的型別對應。
.NET | JavaScript | Nullable |
Task 至 Promise |
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
) 對應至 JSNumber
的Array
。- 將 JS 值傳遞至使用錯誤型別值的 C# 時,架構在大部分情況下會擲回例外狀況。 架構不會在 JS 中執行編譯時間型別檢查。
JSObject
、Exception
、Task
和ArraySegment
建立GCHandle
及 Proxy。 您可以在開發人員程式碼中觸發處置,或稍後允許 .NET 記憶體回收 (GC) 處置物件。 這些型別具有顯著的效能額外負荷。Array
:封送處理陣列會在 JS 或 .NET 中建立陣列的複本。MemoryView
MemoryView
是 .NET WebAssembly 執行階段的 JS 類別,用於封送處理Span
和ArraySegment
。- 與封送處理陣列不同,封送處理
Span
或ArraySegment
不會建立基礎記憶體的複本。 MemoryView
只能由 .NET WebAssembly 執行階段正確具現化。 因此,無法將 JS 函式匯入為具有Span
或ArraySegment
參數的 .NET 方法。- 為
Span
建立的MemoryView
只在 Interop 呼叫期間才有效。 如同Span
在呼叫堆疊上配置,在 Interop 呼叫之後不會保存,因此無法匯出傳回Span
的 .NET 方法。 - 針對
ArraySegment
建立的MemoryView
在 Interop 呼叫之後存留下來,而且有助於共用緩衝區。 在針對ArraySegment
建立的MemoryView
上呼叫dispose()
會處置 Proxy,並取消釘選基礎 .NET 陣列。 我們建議在MemoryView
的try-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。
在下列範例中,每個方法都會匯出至 JS,而且可以從 JS 函數呼叫:
Toggle
方法根據執行狀態啟動或停止 stopwatch。Reset
方法會在執行時重新啟動 stopwatch 或在未執行時將其重設。IsRunning
方法表示 stopwatch 是否執行。
[JSExport]
internal static bool Toggle()
{
if (stopwatch.IsRunning)
{
stopwatch.Stop();
return false;
}
else
{
stopwatch.Start();
return true;
}
}
[JSExport]
internal static void Reset()
{
if (stopwatch.IsRunning)
stopwatch.Restart();
else
stopwatch.Reset();
Render();
}
[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;
在下列範例中,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
工作負載包含兩個專案範本:wasmbrowser
和 wasmconsole
。 這些範本目前處於實驗階段,這表示範本的開發人員工作流程正在不斷演進。 不過,範本中使用的 .NET 和 JS API 在 .NET 8 中受到支援,並為在 JS 的 WASM 上使用 .NET 提供基礎。
您也可以使用下列命令,從 Microsoft.NET.Runtime.WebAssembly.Templates
NuGet 套件安裝範本:
dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates
瀏覽器應用程式
您可以從命令行使用 wasmbrowser
範本建立瀏覽器應用程式,以建立一個 Web 應用程式,示範如何在瀏覽器中一起使用 .NET 和 JS:
dotnet new wasmbrowser
或者,您也可以在 Visual Studio 中使用 WebAssembly Browser App 專案範本建立應用程式。
從 Visual Studio 或使用 .NET CLI 建置應用程式:
dotnet build
從 Visual Studio 或使用 .NET CLI 建置並執行應用程式:
dotnet run
或者,安裝並使用 dotnet serve
命令:
dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish
在上述範例中,{TARGET FRAMEWORK}
預留位置是目標 Framework Moniker。
Node.js 主控台應用程式
您可以使用 wasmconsole
範本建立主控台應用程式,以建立在 WASM 下作為 Node.js 或 V8 主控台應用程式執行的應用程式:
dotnet new wasmconsole
或者,您也可以在 Visual Studio 中使用 WebAssembly Console App 專案範本建立應用程式。
從 Visual Studio 或使用 .NET CLI 建置應用程式:
dotnet build
從 Visual Studio 或使用 .NET CLI 建置並執行應用程式:
dotnet run
或者,從包含 main.mjs
檔案的發佈輸出目錄啟動任何靜態檔案伺服器:
node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs
在上述範例中,{TARGET FRAMEWORK}
預留位置是目標 Framework Moniker,而 {PATH}
預留位置是 main.mjs
檔案的路徑。
其他資源
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應