Freigeben über


JavaScript [JSImport]/[JSExport] Interop mit einem WebAssembly Browser App Projekt

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Warnung

Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der .NET- und .NET Core-Supportrichtlinie. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

In diesem Artikel wird erklärt, wie man ein WebAssembly Browser App-Projekt einrichtet, um .NET von JavaScript (JS) aus unter Verwendung von JS[JSImport]/[JSExport] Interop auszuführen. Weitere Informationen und Beispiele finden Sie unter JavaScript-Interoperabilität mit `[JSImport]`/`[JSExport]` in .NET WebAssembly.

Weitere Anleitungen finden Sie im Leitfaden Konfigurieren und Hosten von .NET WebAssembly-Anwendungen im .NET Runtime-GitHub-Repository (dotnet/runtime).

Vorhandene JS-Apps können die erweiterte clientseitige WebAssembly-Unterstützung nutzen, um .NET-Bibliotheken von JS wiederzuverwenden oder neue .NET-basierte Apps und Frameworks zu erstellen.

Hinweis

In diesem Artikel liegt der Schwerpunkt auf der Ausführung von .NET aus JS-Apps ohne Abhängigkeiten von Blazor. Einen Leitfaden zur Verwendung der Interoperabilität durch [JSImport]/[JSExport] in Blazor WebAssembly-Apps finden Sie unter JavaScript-Interoperabilität durch JSImport/JSExport mit ASP.NET Core Blazor.

Diese Ansätze sind geeignet, wenn Sie nur erwarten, dass die Blazor App auf WebAssembly (WASM) läuft. Bibliotheken können durch Aufruf von OperatingSystem.IsBrowser eine Laufzeitüberprüfung vornehmen, um zu ermitteln, ob die App über WASM ausgeführt wird.

Voraussetzungen

.NET SDK (neueste Version)

Installieren Sie die wasm-tools-Workload in einer administrativen Befehlsshell mit den zugehörigen MSBuild-Zielen:

dotnet workload install wasm-tools

Die Tools können auch über die Workload ASP.NET- und Webentwicklung im Visual Studio-Installer installiert werden. Wählen Sie die Option .NET-WebAssembly-Buildtools aus der Liste der optionalen Komponenten aus.

Optional können Sie den wasm-experimental-Workload installieren, der die folgenden experimentellen Projektvorlagen hinzufügt:

  • WebAssembly Browser App für die ersten Schritte mit .NET auf WebAssembly in einer Browser App.
  • WebAssembly Console App für den Einstieg in eine Node.js-basierte Console App.

Nach der Installation des Workloads können diese neuen Vorlagen bei der Erstellung eines neuen Projekts ausgewählt werden. Diese Workload ist nicht erforderlich, wenn Sie planen, die JS-Interoperabilität durch [JSImport]/[JSExport] in eine vorhandene JS-App zu integrieren.

dotnet workload install wasm-experimental

Die Vorlagen können auch mit dem folgenden Befehl über das NuGet-Paket Microsoft.NET.Runtime.WebAssembly.Templates installiert werden:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

Weitere Informationen finden Sie im Abschnitt Experimentelle Workload und Projektvorlagen.

Namespace

Die in diesem Artikel beschriebene JS-Interop-API wird durch Attribute im Namespace System.Runtime.InteropServices.JavaScript gesteuert.

Projektkonfiguration

So konfigurieren Sie ein Projekt (.csproj) zum Aktivieren der JS-Interoperabilität

  • Legen Sie den Zielframeworkmoniker fest (Platzhalter {TARGET FRAMEWORK}):

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    .NET 7 (net7.0) oder höher wird unterstützt.

  • Aktivieren Sie die AllowUnsafeBlocks-Eigenschaft, damit der Codegenerator im Roslyn-Compiler Zeiger für die JS-Interoperabilität verwenden kann:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Warnung

    Die JS-Interop-API erfordert die Aktivierung von AllowUnsafeBlocks. Seien Sie vorsichtig, wenn Sie Ihren eigenen unsicheren Code in .NET-Anwendungen implementieren, da dies zu Sicherheits- und Stabilitätsrisiken führen kann. Weitere Informationen finden Sie unter Unsicherer Code, Zeigertypen und Funktionszeiger.

Das folgende Beispiel zeigt eine Projektdatei (.csproj) nach der Konfiguration. Der Platzhalter {TARGET FRAMEWORK} ist das Zielframework:

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

  <PropertyGroup>
    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

</Project>
  • Legen Sie den Zielframeworkmoniker fest:

    <TargetFramework>net7.0</TargetFramework>
    

    .NET 7 (net7.0) oder höher wird unterstützt.

  • Geben Sie als Runtimebezeichner browser-wasm an:

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • Geben Sie einen ausführbaren Ausgabetyp an:

    <OutputType>Exe</OutputType>
    
  • Aktivieren Sie die AllowUnsafeBlocks-Eigenschaft, damit der Codegenerator im Roslyn-Compiler Zeiger für die JS-Interoperabilität verwenden kann:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Warnung

    Die JS-Interop-API erfordert die Aktivierung von AllowUnsafeBlocks. Seien Sie vorsichtig, wenn Sie Ihren eigenen unsicheren Code in .NET-Anwendungen implementieren, da dies zu Sicherheits- und Stabilitätsrisiken führen kann. Weitere Informationen finden Sie unter Unsicherer Code, Zeigertypen und Funktionszeiger.

  • Geben Sie als WasmMainJSPath einen Verweis auf eine Datei auf dem Datenträger an. Diese Datei wird mit der App veröffentlicht, die Verwendung der Datei ist jedoch nicht erforderlich, wenn Sie .NET in eine vorhandene JS-App integrieren.

    Im folgenden Beispiel befindet sich die JS-Datei auf dem Datenträger main.js, es ist aber jeder JS-Dateiname zulässig:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

Beispielprojektdatei (.csproj) nach der Konfiguration:

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

JavaScript-Interoperabilität in WASM

Die APIs im folgenden Beispiel werden aus dotnet.js importiert. Mit diesen APIs können Sie benannte Module einrichten, die in Ihren C#-Code importiert und in Methoden aufgerufen werden können, die von Ihrem .NET-Code verfügbar gemacht werden, einschließlich Program.Main.

Wichtig

In diesem Artikel werden die Begriffe „Importieren“ und „Exportieren“ aus der Perspektive von .NET definiert:

  • Eine App importiert JS-Methoden, damit sie in .NET aufgerufen werden können.
  • Eine App exportiert .NET-Methoden, damit sie in JS aufgerufen werden können.

Im folgenden Beispiel:

  • Die Datei dotnet.js wird verwendet, um die .NET-WebAssembly-Runtime zu erstellen und zu starten. dotnet.js wird als Teil der Buildausgabe der App generiert.

    Wichtig

    Für die Integration mit einer vorhandenen App kopieren Sie den Inhalt des Ausgabeordners der Veröffentlichung† in die Bereitstellungsressourcen der vorhandenen App, damit sie zusammen mit dem Rest der App bereitgestellt werden können. Veröffentlichen Sie für Produktionsbereitstellungen die App mit dem Befehl dotnet publish -c Release in einer Befehlsshell, und stellen Sie den Inhalt des Ausgabeordners mit der App bereit.

    † Der Ausgabeordner der Veröffentlichung ist der Zielspeicherort Ihres Veröffentlichungsprofils. Der Standardwert für ein Release-Profil in .NET 8 oder höher ist bin/Release/{TARGET FRAMEWORK}/publish, wobei der Platzhalter {TARGET FRAMEWORK} das Zielframework ist (z. B. net8.0).

  • dotnet.create() richtet die .NET WebAssembly-Runtime ein.

  • setModuleImports ordnet einen Namen einem Modul von JS-Funktionen für den Import in .NET zu. Das JS-Modul enthält eine dom.setInnerText-Funktion, die den Elementselektor und die Zeit zum Anzeigen der aktuellen Stoppuhrzeit auf der Benutzeroberfläche akzeptiert. Der Name des Moduls kann eine beliebige Zeichenfolge sein (es muss kein Dateiname sein), aber er muss mit dem Namen übereinstimmen, der für das JSImportAttribute verwendet wird (Eine Erläuterung finden Sie weiter unten in diesem Artikel.). Die dom.setInnerText-Funktion wird in C# importiert und von der C#-Methode SetInnerText aufgerufen. Die SetInnerText-Methode wird später in diesem Abschnitt beschrieben.

  • exports.StopwatchSample.Reset() führt Aufrufe in .NET (StopwatchSample.Reset) von JS durch. Die C#-Methode Reset startet die Stoppuhr neu, wenn sie ausgeführt wird, oder setzt sie zurück, wenn sie nicht ausgeführt wird. Die Reset-Methode wird später in diesem Abschnitt beschrieben.

  • exports.StopwatchSample.Toggle() führt Aufrufe in .NET (StopwatchSample.Toggle) von JS durch. Die C#-Methode Toggle startet oder unterbricht die Stoppuhr, je nachdem, ob sie derzeit ausgeführt wird. Die Toggle-Methode wird später in diesem Abschnitt beschrieben.

  • runMain() führt Program.Main aus.

  • setModuleImports ordnet einen Namen einem Modul von JS-Funktionen für den Import in .NET zu. Das JS-Modul enthält eine window.location.href-Funktion, die die aktuelle Seitenadresse (URL) zurückgibt. Der Name des Moduls kann eine beliebige Zeichenfolge sein (es muss kein Dateiname sein), aber er muss mit dem Namen übereinstimmen, der für das JSImportAttribute verwendet wird (Eine Erläuterung finden Sie weiter unten in diesem Artikel.). Die window.location.href-Funktion wird in C# importiert und von der C#-Methode GetHRef aufgerufen. Die GetHRef-Methode wird später in diesem Abschnitt beschrieben.

  • exports.MyClass.Greeting() führt Aufrufe in .NET (MyClass.Greeting) von JS durch. Die C#-Methode Greeting gibt eine Zeichenfolge zurück, die das Ergebnis des Aufrufs der window.location.href-Funktion enthält. Die Greeting-Methode wird später in diesem Abschnitt beschrieben.

  • dotnet.run() führt Program.Main aus.

Modul 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();

Um eine JS-Funktion zu importieren, damit sie in C# aufgerufen werden kann, verwenden Sie das neue JSImportAttribute in einer übereinstimmenden Methodensignatur. Der erste Parameter des JSImportAttribute ist der Name der zu importierenden JS-Funktion, und der zweite Parameter ist der Name des Moduls.

Im folgenden Beispiel wird die dom.setInnerText-Funktion aus dem Modul main.js aufgerufen, wenn die SetInnerText-Methode aufgerufen wird:

[JSImport("dom.setInnerText", "main.js")]
internal static partial void SetInnerText(string selector, string content);

Im folgenden Beispiel wird die window.location.href-Funktion aus dem Modul main.js aufgerufen, wenn die GetHRef-Methode aufgerufen wird:

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

In der importierten Methodensignatur können Sie .NET-Typen für Parameter und Rückgabewerte verwenden, die automatisch von der Laufzeit gemarshallt werden. Sie können mit JSMarshalAsAttribute<T> steuern, wie die importierten Methodenparameter gemarshallt werden. Beispielsweise können Sie entscheiden, ob ein Wert vom Typ long als System.Runtime.InteropServices.JavaScript.JSType.Number oder System.Runtime.InteropServices.JavaScript.JSType.BigInt gemarshallt wird. Sie können Action/Func<TResult>-Rückrufe als Parameter übergeben, die als aufrufbare JS-Funktionen gemarshallt werden. Sie können sowohl JS als auch verwaltete Objektverweise übergeben. Diese werden als Proxyobjekte gemarshallt, wobei das Objekt über die Grenze hinweg aktiv bleibt, bis beim Proxy eine Garbage Collection durchgeführt wird. Zudem können asynchrone Methoden mit einem Task-Ergebnis importiert und exportiert werden. Die Ergebnisse werden als JSZusage gemarshallt. Die meisten der gemarshallten Typen funktionieren in beide Richtungen, als Parameter und als Rückgabewerte, und sowohl bei importierten als auch bei exportierten Methoden.

Weitere Informationen über die Typzuordnung und Beispiele finden Sie unter JavaScript-Interoperabilität durch `[JSImport]`/`[JSExport]` mit in .NET WebAssembly.

Funktionen, auf die im globalen Namespace zugegriffen werden kann, können mithilfe des Präfixes globalThis im Funktionsnamen und mithilfe des Attributs [JSImport] ohne Angabe eines Modulnamens importiert werden. Im folgenden Beispiel hat console.log das Präfix globalThis. Die importierte Funktion wird von der C#-Log Methode aufgerufen, die eine C#-Zeichenfolgenmeldung (message) akzeptiert und die C#-Zeichenfolge in einen JSString für console.log marshallt:

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

Verwenden Sie das JSExportAttribute, um eine .NET-Methode zu exportieren, damit sie über JS aufgerufen werden kann.

Im folgenden Beispiel wird jede Methode in JS exportiert und kann aus JS-Funktionen aufgerufen werden:

  • Die Toggle-Methode startet oder unterbricht die Stoppuhr je nach Ausführungsstatus.
  • Die Reset-Methode startet die Stoppuhr neu, wenn sie ausgeführt wird, oder setzt sie zurück, wenn sie nicht ausgeführt wird.
  • Die IsRunning-Methode gibt an, ob die Stoppuhr ausgeführt wird.
[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;

Im folgenden Beispiel gibt die Greeting-Methode eine Zeichenfolge zurück, die das Ergebnis des Aufrufs der GetHRef-Methode enthält. Wie zuvor gezeigt, ruft die C#-Methode GetHref die window.location.href-Funktion in JS aus dem Modul main.js auf. window.location.href gibt die aktuelle Seitenadresse (URL) zurück:

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

Experimentelle Workload und Projektvorlagen

Installieren Sie die Workload wasm-experimental, um die JS-Interoperabilität zu veranschaulichen und die Projektvorlagen für die JS-Interoperabilität abzurufen:

dotnet workload install wasm-experimental

Die Workload wasm-experimental enthält die beiden Projektvorlagen wasmbrowser und wasmconsole. Diese Vorlagen sind derzeit experimentell. Dies bedeutet, dass sich der Entwicklerworkflow für die Vorlagen noch in der Entwicklung befindet. Die in den Vorlagen verwendeten .NET- und JS-APIs werden jedoch in .NET 8 unterstützt und bieten eine Grundlage für die Verwendung von .NET in WASM von JS.

Die Vorlagen können auch mit dem folgenden Befehl über das NuGet-Paket Microsoft.NET.Runtime.WebAssembly.Templates installiert werden:

dotnet new install Microsoft.NET.Runtime.WebAssembly.Templates

Browser-App

Sie können mit der Vorlage wasmbrowser an der Befehlszeile eine Browser-App erstellen, die eine Web-App erstellt, mit der die Verwendung von .NET und JS zusammen in einem Browser veranschaulicht werden kann:

dotnet new wasmbrowser

Alternativ können Sie in Visual Studio die App mithilfe der Projektvorlage WebAssembly Browser App erstellen.

Erstellen Sie die App in Visual Studio oder mithilfe der .NET CLI:

dotnet build

Erstellen Sie die App in Visual Studio oder mithilfe der .NET CLI, und führen Sie sie aus:

dotnet run

Alternativ können Sie den Befehl dotnet serve installieren und verwenden:

dotnet serve -d:bin/$(Configuration)/{TARGET FRAMEWORK}/publish

Im vorherigen Beispiel ist der Platzhalter {TARGET FRAMEWORK} der Zielframeworkmoniker.

Node.js-Konsolen-App

Sie können eine Konsolen-App mit der Vorlage wasmconsole erstellen, die eine App erstellt, die unter WASM als Node.js- oder V8-Konsolen-App ausgeführt wird:

dotnet new wasmconsole

Alternativ können Sie in Visual Studio die App mithilfe der Projektvorlage WebAssembly Console App erstellen.

Erstellen Sie die App in Visual Studio oder mithilfe der .NET CLI:

dotnet build

Erstellen Sie die App in Visual Studio oder mithilfe der .NET CLI, und führen Sie sie aus:

dotnet run

Alternativ können Sie einen beliebigen statischen Dateiserver aus dem Ausgabeverzeichnis der Veröffentlichung starten, das die Datei main.mjs enthält:

node bin/$(Configuration)/{TARGET FRAMEWORK}/{PATH}/main.mjs

Im vorherigen Beispiel ist der Platzhalter {TARGET FRAMEWORK} der Zielframeworkmoniker, und der Platzhalter {PATH} ist der Pfad zur Datei main.mjs.

Zusätzliche Ressourcen