Sdílet prostřednictvím


Interoperabilita JavaScriptu [JSImport]/[JSExport] v .NET WebAssembly

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete v tomto článku ve verzi .NET 9.

Upozorňující

Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v zásadách podpory .NET a .NET Core. Aktuální verzi najdete v tomto článku ve verzi .NET 9.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete v tomto článku ve verzi .NET 9.

Autor: Aaron Shumaker

Tento článek vysvětluje, jak pracovat s JavaScriptem (JS) v webAssembly na straně klienta pomocí JS[JSExport][JSImport]/zprostředkovatele komunikace (System.Runtime.InteropServices.JavaScriptAPI).

[JSImport]/[JSExport] Spolupráce se dá použít při spuštění modulu .NET WebAssembly v JS hostiteli v následujících scénářích:

Požadavky

.NET SDK (nejnovější verze)

Libovolný z následujících typů projektů:

Ukázková aplikace

Zobrazení nebo stažení ukázkového kódu (postup stažení): Vyberte složku verze 8.0 nebo novější, která odpovídá verzi .NET, kterou přijímáte. Ve složce verze přejděte k ukázce s názvem WASMBrowserAppImportExportInterop.

JS interoperabilita s využitím [JSImport]/[JSExport] atributů

Atribut [JSImport] se použije na metodu .NET, která označuje, že odpovídající JS metoda by měla být volána při zavolání metody .NET. To umožňuje vývojářům .NET definovat "importy", které umožňují volání JSkódu .NET . Kromě toho Action lze předat jako parametr a JS vyvolat akci, která podporuje vzor zpětného volání nebo odběru událostí.

Atribut [JSExport] se použije na metodu .NET, která ho JS zpřístupní kódu. To umožňuje JS kódu inicializovat volání metody .NET.

Import JS metod

Následující příklad naimportuje standardní integrovanou JS metodu (console.log) do jazyka C#. [JSImport] je omezena na import metod globálně přístupných objektů. Je například log metoda definovaná na objektu console , která je definována v globálně přístupném objektu globalThis. Metoda console.log je namapována na metodu proxy jazyka C#, ConsoleLogkterá přijímá řetězec pro zprávu protokolu:

public partial class GlobalInterop
{
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog(string text);
}

V Program.Main, ConsoleLog je volána se zprávou, která se má protokolovat:

GlobalInterop.ConsoleLog("Hello World!");

Výstup se zobrazí v konzole prohlížeče.

Následující příklad ukazuje import metody deklarované v JS.

Následující vlastní JS metoda (globalThis.callAlert) vytvoří dialogové okno upozornění (window.alert) se zprávou předanou text:

globalThis.callAlert = function (text) {
  globalThis.window.alert(text);
}

Metoda globalThis.callAlert je namapována na metodu proxy v jazyce C# (CallAlert), která přijímá řetězec pro zprávu:

using System.Runtime.InteropServices.JavaScript;

public partial class GlobalInterop
{
	[JSImport("globalThis.callAlert")]
	public static partial void CallAlert(string text);
}

CallAlert Je Program.Mainvolána , předání textu pro zprávu dialogového okna upozornění:

GlobalInterop.CallAlert("Hello World");

Třída jazyka C# deklarující metodu [JSImport] nemá implementaci. V době kompilace obsahuje zdroj generovaná částečná třída kód .NET, který implementuje zařazování volání a typů k vyvolání odpovídající JS metody. V sadě Visual Studio pomocí možností Přejít k definici nebo Přejít na implementaci přejděte buď na zdrojově vygenerovanou částečnou třídu, nebo na částečnou třídu definovanou vývojářem.

V předchozím příkladu se zprostředkující globalThis.callAlertJS deklarace používá k zabalení existujícího JS kódu. Tento článek neformálně odkazuje na průběžnou JS JS deklaraci jako shim. JS Zaplní mezeru mezi implementací .NET a existujícími JS možnostmi a knihovnami. V mnoha případech, například v předchozím triviálním příkladu, JS není překrytí nutné a metody je možné importovat přímo, jak je znázorněno v předchozím ConsoleLog příkladu. Jak tento článek ukazuje v nadcházejících částech, JS může shim:

  • Zapouzdřte další logiku.
  • Ručně mapovat typy.
  • Snižte počet objektů nebo volání překračujících hranici vzájemné spolupráce.
  • Ruční mapování statických volání na metody instance

Načítání deklarací JavaScriptu

JS Deklarace, které mají být importovány s [JSImport] , jsou obvykle načteny v kontextu stejné stránky nebo JS hostitele, který načetl .NET WebAssembly. Můžete toho dosáhnout takto:

  • Blok <script>...</script> deklarující vložený JS.
  • Deklarace zdroje skriptu (src<script src="./some.js"></script>), která načte externí JS soubor (.js).
  • JS Modul ES6 (<script type='module' src="./moduleName.js"></script>).
  • Modul JS ES6 načtený pomocí JSHost.ImportAsync .NET WebAssembly.

Příklady v tomto článku používají JSHost.ImportAsync. Při volání ImportAsync.NET webAssembly na straně klienta požaduje soubor pomocí parametru moduleUrl , a proto očekává, že soubor bude přístupný jako statický webový prostředek, podobně <script> jako značka načte soubor s src adresou URL. Například následující kód jazyka C# v projektu webAssembly Browser App udržuje JS soubor (.js) v cestě /wwwroot/scripts/ExampleShim.js:

await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");

V závislosti na platformě, která načítá WebAssembly, může adresa URL s předponou tečky, například ./scripts/odkazovat na nesprávný podadresář, například /_framework/scripts/, protože balíček WebAssembly je inicializován skripty architektury v části /_framework/. V takovém případě předpona adresy URL ../scripts/ odkazuje na správnou cestu. Předpona s funguje, /scripts/ pokud je web hostovaný v kořenovém adresáři domény. Typický přístup zahrnuje konfiguraci správné základní cesty pro dané prostředí se značkou HTML <base> a použitím /scripts/ předpony odkazující na cestu vzhledem k základní cestě. Předpony zápisu ~/ tilda nejsou podporovány JSHost.ImportAsync.

Důležité

Pokud JS se načte z modulu JavaScriptu, [JSImport] musí atributy jako druhý parametr obsahovat název modulu. Označuje například, [JSImport("globalThis.callAlert", "ExampleShim")] že importovaná metoda byla deklarována v modulu JavaScript s názvem "ExampleShim.

Mapování typů

Parametry a návratové typy v podpisu metody .NET se automaticky převedou na nebo z příslušných JS typů za běhu, pokud je podporováno jedinečné mapování. Výsledkem může být převod hodnot podle hodnoty nebo odkazů zabalených v typu proxy serveru. Tento proces se označuje jako zařazování typů. Slouží JSMarshalAsAttribute<T> k řízení způsobu, jakým se importované parametry metody a návratové typy zařaďují.

Některé typy nemají výchozí mapování typů. Například long lze zařadit jako System.Runtime.InteropServices.JavaScript.JSType.Number nebo System.Runtime.InteropServices.JavaScript.JSType.BigInt, takže JSMarshalAsAttribute<T> se vyžaduje, aby se zabránilo chybě v době kompilace.

Podporují se následující scénáře mapování typů:

  • Předávání Action nebo Func<TResult> jako parametry, které jsou zařazovány jako volatelné JS metody. To umožňuje kódu .NET vyvolat naslouchací procesy v reakci na JS zpětná volání nebo události.
  • Předávání JS odkazů a odkazů na spravované objekty .NET v obou směrech, které se zařaďují jako objekty proxy a uchovávají se v živém rozsahu mezi hranicemi spolupráce, dokud se proxy neshromáždí.
  • Přiřazování asynchronních JS metod nebo výsledku JS Promise Task a naopak

Většina zařazovaných typů funguje v obou směrech jako parametry a jako návratové hodnoty u importovaných i exportovaných metod.

Následující tabulka uvádí podporované mapování typů.

.NET JavaScript Nullable Task➔k Promise JSMarshalAs volitelný Array of
Boolean Boolean Podporuje se Podporuje se Podporuje se Nepodporováno
Byte Number Podporuje se Podporuje se Podporuje se Podporuje se
Char String Podporuje se Podporuje se Podporuje se Nepodporováno
Int16 Number Podporuje se Podporuje se Podporuje se Nepodporováno
Int32 Number Podporuje se Podporuje se Podporuje se Podporuje se
Int64 Number Podporuje se Podporuje se Nepodporováno Nepodporováno
Int64 BigInt Podporuje se Podporuje se Nepodporováno Nepodporováno
Single Number Podporuje se Podporuje se Podporuje se Nepodporováno
Double Number Podporuje se Podporuje se Podporuje se Podporuje se
IntPtr Number Podporuje se Podporuje se Podporuje se Nepodporováno
DateTime Date Podporuje se Podporuje se Nepodporováno Nepodporováno
DateTimeOffset Date Podporuje se Podporuje se Nepodporováno Nepodporováno
Exception Error Nepodporováno Podporuje se Podporuje se Nepodporováno
JSObject Object Nepodporováno Podporuje se Podporuje se Podporuje se
String String Nepodporováno Podporuje se Podporuje se Podporuje se
Object Any Nepodporováno Podporuje se Nepodporováno Podporuje se
Span<Byte> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Span<Int32> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Span<Double> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
ArraySegment<Byte> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
ArraySegment<Int32> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
ArraySegment<Double> MemoryView Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Task Promise Nepodporováno Nepodporováno Podporuje se Nepodporováno
Action Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Action<T1> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Action<T1, T2> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Action<T1, T2, T3> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Func<TResult> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Func<T1, TResult> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Func<T1, T2, TResult> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno
Func<T1, T2, T3, TResult> Function Nepodporováno Nepodporováno Nepodporováno Nepodporováno

Následující podmínky platí pro mapování typů a zařazované hodnoty:

  • Sloupec Array of označuje, zda lze typ .NET zařaďovat jako JSArray. Příklad: C# int[] (Int32) namapovaný na JSArray s Number.
  • Při předávání JS hodnoty do jazyka C# s hodnotou nesprávného typu vyvolá architektura ve většině případů výjimku. Architektura neprovádí kontrolu JStypu kompilace .
  • JSObject, ExceptionTask a ArraySegment vytvořte GCHandle a proxy. Odstranění můžete aktivovat v kódu vývojáře nebo povolit uvolnění paměti .NET (GC) později. Tyto typy mají značné nároky na výkon.
  • Array: Zařazování pole vytvoří kopii pole v JS rozhraní .NET nebo .NET.
  • MemoryView
    • MemoryViewJS je třída pro modul runtime .NET WebAssembly pro zařazování Span a ArraySegment.
    • Na rozdíl od zařazování pole zařazování nebo ArraySegment nezařazování Span kopie podkladové paměti.
    • MemoryView lze správně vytvořit instanci pouze modulem runtime .NET WebAssembly. Proto není možné importovat metodu jako metodu JS .NET, která má parametr Span nebo ArraySegment.
    • MemoryView vytvoření pro volání Span zprostředkovatele je platné pouze po dobu trvání volání zprostředkovatele komunikace. Jak Span je přiděleno v zásobníku volání, který se nezachovává po volání zprostředkovatele komunikace, není možné exportovat metodu Span.NET, která vrací .
    • MemoryView vytvořeno pro ArraySegment přežití po volání zprostředkovatele komunikace a je užitečné pro sdílení vyrovnávací paměti. Volání dispose() vytvořeného MemoryView pro ArraySegment zlikviduje proxy server a odepnou základní pole .NET. Doporučujeme zavolat dispose() do try-finally bloku pro MemoryView.

Některé kombinace mapování typů, které vyžadují vnořené obecné typy, JSMarshalAs se v současné době nepodporují. Například pokus o materializaci pole z Promise například [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] vygeneruje chybu v době kompilace. Vhodné alternativní řešení se liší v závislosti na scénáři, ale tento konkrétní scénář se dále zkoumá v části Omezení mapování typů.

JS primitiva

Následující příklad ukazuje [JSImport] využití mapování typů několika primitivních JS typů a použití JSMarshalAs, kde explicitní mapování jsou vyžadována v době kompilace.

PrimitivesShim.js:

globalThis.counter = 0;

// Takes no parameters and returns nothing.
export function incrementCounter() {
  globalThis.counter += 1;
};

// Returns an int.
export function getCounter() { return globalThis.counter; };

// Takes a parameter and returns nothing. JS doesn't restrict the parameter type, 
// but we can restrict it in the .NET proxy, if desired.
export function logValue(value) { console.log(value); };

// Called for various .NET types to demonstrate mapping to JS primitive types.
export function logValueAndType(value) { console.log(typeof value, value); };

PrimitivesInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PrimitivesInterop
{
    // Importing an existing JS method.
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);

    // Importing static methods from a JS module.
    [JSImport("incrementCounter", "PrimitivesShim")]
    public static partial void IncrementCounter();

    [JSImport("getCounter", "PrimitivesShim")]
    public static partial int GetCounter();

    // The JS shim method name isn't required to match the C# method name.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogInt(int value);

    // A second mapping to the same JS method with compatible type.
    [JSImport("logValue", "PrimitivesShim")]
    public static partial void LogString(string value);

    // Accept any type as parameter. .NET types are mapped to JS types where 
    // possible. Otherwise, they're marshalled as an untyped object reference 
    // to the .NET object proxy. The JS implementation logs to browser console 
    // the JS type and value to demonstrate results of marshalling.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Any>] object value);

    // Some types have multiple mappings and require explicit marshalling to the 
    // desired JS type. A long/Int64 can be mapped as either a Number or BigInt.
    // Passing a long value to the above method generates an error at runtime:
    // "ToJS for System.Int64 is not implemented." ("ToJS" means "to JavaScript")
    // If the parameter declaration `Method(JSMarshalAs<JSType.Any>] long value)` 
    // is used, a compile-time error is generated:
    // "Type long is not supported by source-generated JS interop...."
    // Instead, explicitly map the long parameter to either a JSType.Number or 
    // JSType.BigInt. Note that runtime overflow errors are possible in JS if the 
    // C# value is too large.
    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForNumber(
        [JSMarshalAs<JSType.Number>] long value);

    [JSImport("logValueAndType", "PrimitivesShim")]
    public static partial void LogValueAndTypeForBigInt(
        [JSMarshalAs<JSType.BigInt>] long value);
}

public static class PrimitivesUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("PrimitivesShim", "/PrimitivesShim.js");

        // Call a proxy to a static JS method, console.log().
        PrimitivesInterop.ConsoleLog("Printed from JSImport of console.log()");

        // Basic examples of JS interop with an integer.
        PrimitivesInterop.IncrementCounter();
        int counterValue = PrimitivesInterop.GetCounter();
        PrimitivesInterop.LogInt(counterValue);
        PrimitivesInterop.LogString("I'm a string from .NET in your browser!");

        // Mapping some other .NET types to JS primitives.
        PrimitivesInterop.LogValueAndType(true);
        PrimitivesInterop.LogValueAndType(0x3A); // Byte literal
        PrimitivesInterop.LogValueAndType('C');
        PrimitivesInterop.LogValueAndType((Int16)12);
        // JS Number has a lower max value and can generate overflow errors.
        PrimitivesInterop.LogValueAndTypeForNumber(9007199254740990L); // Int64/Long
        // Next line: Int64/Long, JS BigInt supports larger numbers.
        PrimitivesInterop.LogValueAndTypeForBigInt(1234567890123456789L);// 
        PrimitivesInterop.LogValueAndType(3.14f); // Single floating point literal
        PrimitivesInterop.LogValueAndType(3.14d); // Double floating point literal
        PrimitivesInterop.LogValueAndType("A string");
    }
}

V Program.Main:

await PrimitivesUsage.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

Printed from JSImport of console.log()
1
I'm a string from .NET in your browser!
boolean true
number 58
number 67
number 12
number 9007199254740990
bigint 1234567890123456789n
number 3.140000104904175
number 3.14
string A string

JSDate objekty

Příklad v této části ukazuje metody importu, které mají JS Date objekt jako jeho návrat nebo parametr. Kalendářní data jsou zařazována mezi jednotlivými hodnotami, což znamená, že se kopírují úplně stejně jako JS primitivy.

Objekt Date je nezávislý na časovém pásmu. Technologie .NET DateTime se upraví vzhledem k tomu DateTimeKind , že je přiřazována k objektu , ale informace o časovém pásmu Datese nezachovají. Zvažte inicializaci DateTime s DateTimeKind.Utc hodnotou, kterou představuje, nebo DateTimeKind.Local je konzistentní.

DateShim.js:

export function incrementDay(date) {
  date.setDate(date.getDate() + 1);
  return date;
}

export function logValueAndType(value) {
  console.log("Date:", value)
}

DateInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class DateInterop
{
    [JSImport("incrementDay", "DateShim")]
    [return: JSMarshalAs<JSType.Date>] // Explicit JSMarshalAs for a return type
    public static partial DateTime IncrementDay(
        [JSMarshalAs<JSType.Date>] DateTime date);

    [JSImport("logValueAndType", "DateShim")]
    public static partial void LogValueAndType(
        [JSMarshalAs<JSType.Date>] DateTime value);
}

public static class DateUsage
{
    public static async Task Run()
    {
        // Ensure JS module loaded.
        await JSHost.ImportAsync("DateShim", "/DateShim.js");

        // Basic examples of interop with a C# DateTime and JS Date.
        DateTime date = new(1968, 12, 21, 12, 51, 0, DateTimeKind.Utc);
        DateInterop.LogValueAndType(date);
        date = DateInterop.IncrementDay(date);
        DateInterop.LogValueAndType(date);
    }
}

V Program.Main:

await DateUsage.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

Date: Sat Dec 21 1968 07:51:00 GMT-0500 (Eastern Standard Time)
Date: Sun Dec 22 1968 07:51:00 GMT-0500 (Eastern Standard Time)

Předchozí informace o časovém pásmu (GMT-0500 (Eastern Standard Time)) závisí na místním časovém pásmu počítače nebo prohlížeče.

JS odkazy na objekty

Kdykoli metoda JS vrátí odkaz na objekt, je reprezentován v .NET jako JSObject. Původní JS objekt pokračuje v jeho životnosti JS v rámci hranice, zatímco kód .NET může přistupovat a upravovat pomocí odkazu prostřednictvím JSObject. I když samotný typ zpřístupňuje omezené rozhraní API, schopnost uchovávat JS odkaz na objekt a vracet ho nebo předat přes hranici spolupráce umožňuje podporu několika scénářů spolupráce.

Poskytuje JSObject metody pro přístup k vlastnostem, ale neposkytuje přímý přístup k metodám instance. Jak ukazuje následující Summarize metoda, metody instance mohou být přístupné nepřímo implementací statické metody, která přebírá instanci jako parametr.

JSObjectShim.js:

export function createObject() {
  return {
    name: "Example JS Object",
    answer: 41,
    question: null,
    summarize: function () {
      return `Question: "${this.question}" Answer: ${this.answer}`;
    }
  };
}

export function incrementAnswer(object) {
  object.answer += 1;
  // Don't return the modified object, since the reference is modified.
}

// Proxy an instance method call.
export function summarize(object) {
  return object.summarize();
}

JSObjectInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class JSObjectInterop
{
    [JSImport("createObject", "JSObjectShim")]
    public static partial JSObject CreateObject();

    [JSImport("incrementAnswer", "JSObjectShim")]
    public static partial void IncrementAnswer(JSObject jsObject);

    [JSImport("summarize", "JSObjectShim")]
    public static partial string Summarize(JSObject jsObject);

    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);
}

public static class JSObjectUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("JSObjectShim", "/JSObjectShim.js");

        JSObject jsObject = JSObjectInterop.CreateObject();
        JSObjectInterop.ConsoleLog(jsObject);
        JSObjectInterop.IncrementAnswer(jsObject);
        // An updated object isn't retrieved. The change is reflected in the 
        // existing instance.
        JSObjectInterop.ConsoleLog(jsObject);

        // JSObject exposes several methods for interacting with properties.
        jsObject.SetProperty("question", "What is the answer?");
        JSObjectInterop.ConsoleLog(jsObject);

        // We can't directly JSImport an instance method on the jsObject, but we 
        // can pass the object reference and have the JS shim call the instance 
        // method.
        string summary = JSObjectInterop.Summarize(jsObject);
        Console.WriteLine("Summary: " + summary);
    }
}

V Program.Main:

await JSObjectUsage.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

{name: 'Example JS Object', answer: 41, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: 'What is the answer?', Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
Summary: Question: "What is the answer?" Answer: 42

Asynchronní interoperabilita

Mnoho JS rozhraní API je asynchronní a dokončování signálu prostřednictvím zpětného volání, Promisenebo asynchronní metody. Ignorování asynchronních schopností často není možné, protože následný kód může záviset na dokončení asynchronní operace a musí být očekáván.

JS metody používající async klíčové slovo nebo vrácení Promise lze očekávat v jazyce C# metodou vracející Taskznak . Jak je znázorněno níže, klíčové slovo se v metodě jazyka C# s [JSImport] atributem nepoužívá, async protože v něm nepoužívá await klíčové slovo. Využívání kódu volajícího metodu by však obvykle používalo await klíčové slovo a být označeno jako async, jak je znázorněno v příkladu PromisesUsage .

JS zpětné volání, jako setTimeoutje například , lze zabalit do před Promise návratem z JS. Zabalení zpětného volání do Promisefunkce, jak je znázorněno ve funkci přiřazené Wait2Seconds, je vhodné pouze v případě, že zpětné volání je volána přesně jednou. V opačném případě je možné předat jazyk C# Action pro naslouchání zpětnému volání, které se může volat nulou nebo mnohokrát, což je znázorněno v části Přihlášení k JS odběru událostí.

PromisesShim.js:

export function wait2Seconds() {
  // This also demonstrates wrapping a callback-based API in a promise to
  // make it awaitable.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(); // Resolve promise after 2 seconds
    }, 2000);
  });
}

// Return a value via resolve in a promise.
export function waitGetString() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("String From Resolve"); // Return a string via promise
    }, 500);
  });
}

export function waitGetDate() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date('1988-11-24')); // Return a date via promise
    }, 500);
  });
}

// Demonstrates an awaitable fetch.
export function fetchCurrentUrl() {
  // This method returns the promise returned by .then(*.text())
  // and .NET awaits the returned promise.
  return fetch(globalThis.window.location, { method: 'GET' })
    .then(response => response.text());
}

// .NET can await JS methods using the async/await JS syntax.
export async function asyncFunction() {
  await wait2Seconds();
}

// A Promise.reject can be used to signal failure and is bubbled to .NET code
// as a JSException.
export function conditionalSuccess(shouldSucceed) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed)
        resolve(); // Success
      else
        reject("Reject: ShouldSucceed == false"); // Failure
    }, 500);
  });
}

Nepoužívejte async klíčové slovo v podpisu metody jazyka C#. Task Vrácení nebo Task<TResult> je dostačující.

Při volání asynchronních JS metod často chceme počkat na JS dokončení provádění metody. Pokud načítáte prostředek nebo provedete požadavek, pravděpodobně chceme, aby se akce dokončila, následující kód.

Pokud překrytí vrátí hodnotu Promise, jazyk C# s ním může zacházet jako s očekávanýmTask<TResult>Task/ .JS

PromisesInterop.cs:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class PromisesInterop
{
    // For a promise with void return type, declare a Task return type:
    [JSImport("wait2Seconds", "PromisesShim")]
    public static partial Task Wait2Seconds();

    [JSImport("waitGetString", "PromisesShim")]
    public static partial Task<string> WaitGetString();

    // Some return types require a [return: JSMarshalAs...] declaring the
    // Promise's return type corresponding to Task<T>.
    [JSImport("waitGetDate", "PromisesShim")]
    [return: JSMarshalAs<JSType.Promise<JSType.Date>>()]
    public static partial Task<DateTime> WaitGetDate();

    [JSImport("fetchCurrentUrl", "PromisesShim")]
    public static partial Task<string> FetchCurrentUrl();

    [JSImport("asyncFunction", "PromisesShim")]
    public static partial Task AsyncFunction();

    [JSImport("conditionalSuccess", "PromisesShim")]
    public static partial Task ConditionalSuccess(bool shouldSucceed);
}

public static class PromisesUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("PromisesShim", "/PromisesShim.js");

        Stopwatch sw = new();
        sw.Start();

        await PromisesInterop.Wait2Seconds(); // Await Promise
        Console.WriteLine($"Waited {sw.Elapsed.TotalSeconds:#.0}s.");

        sw.Restart();
        string str =
            await PromisesInterop.WaitGetString(); // Await promise (string return)
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetString: '{str}'");

        sw.Restart();
        // Await promise with string return.
        DateTime date = await PromisesInterop.WaitGetDate();
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetDate: '{date}'");

        // Await a JS fetch.
        string responseText = await PromisesInterop.FetchCurrentUrl();
        Console.WriteLine($"responseText.Length: {responseText.Length}");

        sw.Restart();

        await PromisesInterop.AsyncFunction(); // Await an async JS method
        Console.WriteLine(
            $"Waited {sw.Elapsed.TotalSeconds:#.0}s for AsyncFunction.");

        try
        {
            // Handle a promise rejection. Await an async JS method.
            await PromisesInterop.ConditionalSuccess(shouldSucceed: false);
        }
        catch (JSException ex) // Catch JS exception
        {
            Console.WriteLine($"JS Exception Caught: '{ex.Message}'");
        }
    }
}

V Program.Main:

await PromisesUsage.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

Waited 2.0s.
Waited .5s for WaitGetString: 'String From Resolve'
Waited .5s for WaitGetDate: '11/24/1988 12:00:00 AM'
responseText.Length: 582
Waited 2.0s for AsyncFunction.
JS Exception Caught: 'Reject: ShouldSucceed == false'

Omezení mapování typů

Některá mapování typů vyžadující vnořené obecné typy v JSMarshalAs definici se v současné době nepodporují. Například vrácení Promise pole pro pole, například [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] vygeneruje chybu v době kompilace. Vhodné alternativní řešení se liší v závislosti na scénáři, ale jednou z možností je reprezentovat pole jako JSObject odkaz. To může být dostačující, pokud přístup k jednotlivým prvkům v rozhraní .NET není nutný a odkaz lze předat jiným JS metodám, které se chovají na poli. Alternativně může vyhrazená metoda vzít JSObject odkaz jako parametr a vrátit materializované pole, jak je znázorněno v následujícím UnwrapJSObjectAsIntArray příkladu. V tomto případě JS metoda nemá žádnou kontrolu typů a vývojář má odpovědnost za to, že se předá zabalení JSObject příslušného typu pole.

export function waitGetIntArrayAsObject() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([1, 2, 3, 4, 5]); // Return an array from the Promise
    }, 500);
  });
}

export function unwrapJSObjectAsIntArray(jsObject) {
  return jsObject;
}
// Not supported, generates compile-time error.
// [JSImport("waitGetArray", "PromisesShim")]
// [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
// public static partial Task<int[]> WaitGetIntArray();

// Workaround, take the return the call and pass it to UnwrapJSObjectAsIntArray.
// Return a JSObject reference to a JS number array.
[JSImport("waitGetIntArrayAsObject", "PromisesShim")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>()]
public static partial Task<JSObject> WaitGetIntArrayAsObject();

// Takes a JSObject reference to a JS number array, and returns the array as a C# 
// int array.
[JSImport("unwrapJSObjectAsIntArray", "PromisesShim")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>()]
public static partial int[] UnwrapJSObjectAsIntArray(JSObject intArray);
//...

V Program.Main:

JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);

Důležité informace o výkonu

Zařazování volání a režijní režie sledovacích objektů napříč hranicemi spolupráce je dražší než nativní operace .NET, ale přesto by se měl ukázat přijatelný výkon typické webové aplikace se střední poptávkou.

Proxy objektů, například JSObject, které udržují odkazy napříč hranicemi vzájemné spolupráce, mají další režii paměti a mají vliv na to, jak uvolňování paměti ovlivňuje tyto objekty. Kromě toho může být dostupná paměť vyčerpána bez aktivace uvolňování paměti v některých scénářích, protože zatížení paměti a JS .NET není sdíleno. Toto riziko je významné, pokud se na velký počet velkých objektů odkazuje přes hranice vzájemné spolupráce relativně malými JS objekty nebo naopak, kde na velké objekty .NET odkazují JS proxy servery. Vtakovýchch materiálech v takových případech doporučujeme sledovat deterministické způsoby odstranění s using rozsahy, které využívají IDisposable rozhraní objektů JS .

Následující srovnávací testy, které využívají dřívější ukázkový kód, ukazují, že operace spolupráce jsou zhruba o řád nižší než ty, které zůstávají v rámci hranice .NET, ale operace spolupráce zůstávají poměrně rychlé. Kromě toho vezměte v úvahu, že možnosti zařízení uživatele mají vliv na výkon.

JSObjectBenchmark.cs:

using System;
using System.Diagnostics;

public static class JSObjectBenchmark
{
    public static void Run()
    {
        Stopwatch sw = new();
        var jsObject = JSObjectInterop.CreateObject();

        sw.Start();

        for (int i = 0; i < 1000000; i++)
        {
            JSObjectInterop.IncrementAnswer(jsObject);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        var pocoObject =
            new PocoObject { Question = "What is the answer?", Answer = 41 };
        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            pocoObject.IncrementAnswer();
        }

        sw.Stop();

        Console.WriteLine($".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} " +
            $"seconds at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms " +
            "per operation");

        Console.WriteLine($"Begin Object Creation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var jsObject2 = JSObjectInterop.CreateObject();
            JSObjectInterop.IncrementAnswer(jsObject2);
        }

        sw.Stop();

        Console.WriteLine(
            $"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
            $"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
            "operation");

        sw.Restart();

        for (int i = 0; i < 1000000; i++)
        {
            var pocoObject2 =
                new PocoObject { Question = "What is the answer?", Answer = 0 };
            pocoObject2.IncrementAnswer();
        }

        sw.Stop();
        Console.WriteLine(
            $".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds at " +
            $"{sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per operation");
    }

    public class PocoObject // Plain old CLR object
    {
        public string Question { get; set; }
        public int Answer { get; set; }

        public void IncrementAnswer() => Answer += 1;
    }
}

V Program.Main:

JSObjectBenchmark.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

JS interop elapsed time: .2536 seconds at .000254 ms per operation
.NET elapsed time: .0210 seconds at .000021 ms per operation
Begin Object Creation
JS interop elapsed time: 2.1686 seconds at .002169 ms per operation
.NET elapsed time: .1089 seconds at .000109 ms per operation

Přihlášení k odběru JS událostí

Kód .NET se může přihlásit k odběru JS událostí a zpracovat JS události předáním jazyka C# Action JS funkci, která bude fungovat jako obslužná rutina. JS Kód shim zpracovává přihlášení k odběru události.

Upozorňující

Interakce s jednotlivými vlastnostmi DOM prostřednictvím JS vzájemné spolupráce, jak ukazuje pokyny v této části, je poměrně pomalé a může vést k vytvoření mnoha proxy serverů, které vytvářejí vysoký tlak uvolňování paměti. Následující vzor se obecně nedoporučuje. Pro více než několik prvků použijte následující vzor. Další informace najdete v části Důležité informace o výkonu.

Nuance removeEventListener je, že vyžaduje odkaz na funkci, která addEventListenerbyla dříve předána . Když je jazyk C# Action předán přes hranici spolupráce, je zabalený do objektu JS proxy. Předáním stejného jazyka C# Action do obou addEventListener a removeEventListener výsledkem je vygenerování dvou různých JS objektů proxy, které obtéká Action. Tyto odkazy se liší, takže removeEventListener nedokáže najít naslouchací proces události, který se má odebrat. Pokud chcete tento problém vyřešit, následující příklady zabalí jazyk C# Action do JS funkce a vrátí odkaz jako JSObject odkaz z volání odběru, aby se později předal do volání odhlášení odběru. Vzhledem k tomu, že jazyk C# Action je vrácen a předán jako JSObject, stejný odkaz se používá pro obě volání a naslouchací proces událostí lze odebrat.

EventsShim.js:

export function subscribeEventById(elementId, eventName, listenerFunc) {
  const elementObj = document.getElementById(elementId);

  // Need to wrap the Managed C# action in JS func (only because it is being 
  // returned).
  let handler = function (event) {
    listenerFunc(event.type, event.target.id); // Decompose object to primitives
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  // Return JSObject reference so it can be used for removeEventListener later.
  return handler;
}

// Param listenerHandler must be the JSObject reference returned from the prior 
// SubscribeEvent call.
export function unsubscribeEventById(elementId, eventName, listenerHandler) {
  const elementObj = document.getElementById(elementId);
  elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function triggerClick(elementId) {
  const elementObj = document.getElementById(elementId);
  elementObj.click();
}

export function getElementById(elementId) {
  return document.getElementById(elementId);
}

export function subscribeEvent(elementObj, eventName, listenerFunc) {
  let handler = function (e) {
    listenerFunc(e);
  }.bind(elementObj);

  elementObj.addEventListener(eventName, handler, false);
  return handler;
}

export function unsubscribeEvent(elementObj, eventName, listenerHandler) {
  return elementObj.removeEventListener(eventName, listenerHandler, false);
}

export function subscribeEventFailure(elementObj, eventName, listenerFunc) {
  // It's not strictly required to wrap the C# action listenerFunc in a JS 
  // function.
  elementObj.addEventListener(eventName, listenerFunc, false);
  // If you need to return the wrapped proxy object, you will receive an error 
  // when it tries to wrap the existing proxy in an additional proxy:
  // Error: "JSObject proxy of ManagedObject proxy is not supported."
  return listenerFunc;
}

EventsInterop.cs:

using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;

public partial class EventsInterop
{
    [JSImport("subscribeEventById", "EventsShim")]
    public static partial JSObject SubscribeEventById(string elementId,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.String, JSType.String>>]
        Action<string, string> listenerFunc);

    [JSImport("unsubscribeEventById", "EventsShim")]
    public static partial void UnsubscribeEventById(string elementId,
        string eventName, JSObject listenerHandler);

    [JSImport("triggerClick", "EventsShim")]
    public static partial void TriggerClick(string elementId);

    [JSImport("getElementById", "EventsShim")]
    public static partial JSObject GetElementById(string elementId);

    [JSImport("subscribeEvent", "EventsShim")]
    public static partial JSObject SubscribeEvent(JSObject htmlElement,
        string eventName,
        [JSMarshalAs<JSType.Function<JSType.Object>>]
        Action<JSObject> listenerFunc);

    [JSImport("unsubscribeEvent", "EventsShim")]
    public static partial void UnsubscribeEvent(JSObject htmlElement,
        string eventName, JSObject listenerHandler);
}

public static class EventsUsage
{
    public static async Task Run()
    {
        await JSHost.ImportAsync("EventsShim", "/EventsShim.js");

        Action<string, string> listenerFunc = (eventName, elementId) =>
            Console.WriteLine(
                $"In C# event listener: Event {eventName} from ID {elementId}");

        // Assumes two buttons exist on the page with ids of "btn1" and "btn2"
        JSObject listenerHandler1 =
            EventsInterop.SubscribeEventById("btn1", "click", listenerFunc);
        JSObject listenerHandler2 =
            EventsInterop.SubscribeEventById("btn2", "click", listenerFunc);
        Console.WriteLine("Subscribed to btn1 & 2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2");

        EventsInterop.UnsubscribeEventById("btn2", "click", listenerHandler2);
        Console.WriteLine("Unsubscribed btn2.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.TriggerClick("btn2"); // Doesn't trigger because unsubscribed
        EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler1);
        // Pitfall: Using a different handler for unsubscribe silently fails.
        // EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler2);

        // With JSObject as event target and event object.
        Action<JSObject> listenerFuncForElement = (eventObj) =>
        {
            string eventType = eventObj.GetPropertyAsString("type");
            JSObject target = eventObj.GetPropertyAsJSObject("target");
            Console.WriteLine(
                $"In C# event listener: Event {eventType} from " +
                $"ID {target.GetPropertyAsString("id")}");
        };

        JSObject htmlElement = EventsInterop.GetElementById("btn1");
        JSObject listenerHandler3 = EventsInterop.SubscribeEvent(
            htmlElement, "click", listenerFuncForElement);
        Console.WriteLine("Subscribed to btn1.");
        EventsInterop.TriggerClick("btn1");
        EventsInterop.UnsubscribeEvent(htmlElement, "click", listenerHandler3);
        Console.WriteLine("Unsubscribed btn1.");
        EventsInterop.TriggerClick("btn1");
    }
}

V Program.Main:

await EventsUsage.Run();

V předchozím příkladu se v konzole ladění prohlížeče zobrazí následující výstup:

Subscribed to btn1 & 2.
In C# event listener: Event click from ID btn1
In C# event listener: Event click from ID btn2
Unsubscribed btn2.
In C# event listener: Event click from ID btn1
Subscribed to btn1.
In C# event listener: Event click from ID btn1
Unsubscribed btn1.

JS[JSImport]/[JSExport] scénáře spolupráce

Následující články se zaměřují na spuštění modulu .NET WebAssembly v JS hostiteli, například v prohlížeči: