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:
- Spolupráce JavaScriptu [JSImport]/[JSExport] s projektem aplikace WebAssembly Browser.
- JavaScript JSImport/JSExport interop s ASP.NET Core Blazor.
- Jiné platformy .NET WebAssembly, které podporují
[JSImport]
/[JSExport]
interoperabilitu.
Požadavky
Libovolný z následujících typů projektů:
- Projekt aplikace WebAssembly Browser vytvořený v javascriptu [JSImport]/[JSExport] s projektem aplikace WebAssembly Browser.
- Blazor Projekt na straně klienta vytvořený podle interoperability JAVAScript JSImport/JSExport s ASP.NET Core Blazor.
- Projekt vytvořený pro komerční nebo opensourcovou platformu, která podporuje
[JSImport]
/[JSExport]
interoperabilitu (System.Runtime.InteropServices.JavaScript API).
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#, ConsoleLog
která 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.Main
volá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.callAlert
JS 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
sNumber
. - 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
,Exception
Task
aArraySegment
vytvořteGCHandle
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
MemoryView
JS je třída pro modul runtime .NET WebAssembly pro zařazováníSpan
aArraySegment
.- 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á parametrSpan
neboArraySegment
.MemoryView
vytvoření pro voláníSpan
zprostředkovatele je platné pouze po dobu trvání volání zprostředkovatele komunikace. JakSpan
je přiděleno v zásobníku volání, který se nezachovává po volání zprostředkovatele komunikace, není možné exportovat metoduSpan
.NET, která vrací .MemoryView
vytvořeno proArraySegment
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éhoMemoryView
proArraySegment
zlikviduje proxy server a odepnou základní pole .NET. Doporučujeme zavolatdispose()
dotry-finally
bloku proMemoryView
.
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 Date
se 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í, Promise
nebo 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 setTimeout
je například , lze zabalit do před Promise
návratem z JS. Zabalení zpětného volání do Promise
funkce, 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á addEventListener
byla 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: