Взаимодействие JavaScript [JSImport]
/[JSExport]
в .NET WebAssembly
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 9 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
В этой статье объясняется, как взаимодействовать с JavaScript (JS) на стороне клиента WebAssembly с помощью/[JSExport]
JS[JSImport]
взаимодействия (System.Runtime.InteropServices.JavaScriptAPI).
[JSImport]
/[JSExport]
Взаимодействие применимо при запуске модуля WebAssembly .NET в узле в JS следующих сценариях:
- Взаимодействие JavaScript "[JSImport]/[JSExport]" с проектом приложения браузера WebAssembly.
- Взаимодействие JavaScript JSImport/JSExport с ASP.NET Core Blazor.
- Другие платформы .NET WebAssembly, поддерживающие
[JSImport]
/[JSExport]
взаимодействие.
Необходимые компоненты
Пакет SDK для .NET (последняя версия)
Любой из следующих типов проектов:
- Проект приложения браузера WebAssembly, созданный в соответствии с проектом приложения браузера WebAssembly, созданным согласно JavaScript "[JSImport]"/[JSExport].
- Клиентский Blazor проект, созданный в соответствии с взаимодействием JavaScript JSImport/JSExport с ASP.NET Core Blazor.
- Проект, созданный для коммерческой или открытой платформы с открытым кодом, поддерживающей
[JSImport]
/[JSExport]
взаимодействие (System.Runtime.InteropServices.JavaScript API).
Пример приложения
Просмотр или скачивание примера кода (как скачать): выберите папку версии 8.0 или более поздней версии, соответствующую используемой версии .NET. В папке версии перейдите к примеру с именем WASMBrowserAppImportExportInterop
.
JS взаимодействие с помощью [JSImport]
/[JSExport]
атрибутов
Атрибут [JSImport]
применяется к методу .NET, чтобы указать, что соответствующий JS метод должен вызываться при вызове метода .NET. Это позволяет разработчикам .NET определять "импорты", позволяющие коду .NET вызываться JS. Кроме того, Action можно передать как параметр и JS вызвать действие для поддержки шаблона обратного вызова или подписки на события.
Атрибут [JSExport]
применяется к методу .NET, чтобы предоставить его коду JS . Это позволяет JS коду инициировать вызовы метода .NET.
JS Импорт методов
В следующем примере импортируется стандартный встроенный JS метод (console.log
) в C#. [JSImport]
ограничен импортом методов глобально доступных объектов. Например, это метод, log
определенный для console
объекта, который определяется для глобально доступного объекта globalThis
. Этот console.log
метод сопоставляется с прокси-методом C#, ConsoleLog
который принимает строку для сообщения журнала:
public partial class GlobalInterop
{
[JSImport("globalThis.console.log")]
public static partial void ConsoleLog(string text);
}
ConsoleLog
В Program.Main
вызывается сообщение для записи:
GlobalInterop.ConsoleLog("Hello World!");
Выходные данные отображаются в консоли браузера.
Ниже показано импорт метода, объявленного в JS.
Следующий настраиваемый JS метод (globalThis.callAlert
) создает диалоговое окно оповещений (window.alert
) с переданным сообщением:text
globalThis.callAlert = function (text) {
globalThis.window.alert(text);
}
Этот globalThis.callAlert
метод сопоставляется с прокси-методом C# (CallAlert
), который принимает строку для сообщения:
using System.Runtime.InteropServices.JavaScript;
public partial class GlobalInterop
{
[JSImport("globalThis.callAlert")]
public static partial void CallAlert(string text);
}
CallAlert
ВызываетсяProgram.Main
, передавая текст для сообщения диалогового окна генерации оповещений:
GlobalInterop.CallAlert("Hello World");
Класс C#, объявляющий [JSImport]
метод, не имеет реализации. Во время компиляции исходный разделяемый класс содержит код .NET, реализующий маршаллирование вызова и типов для вызова соответствующего JS метода. В Visual Studio с помощью параметров "Перейти к определению" или "Перейти к реализации" соответственно перемещается к исходному или определенному разработчиком частичному классу.
В предыдущем примере промежуточное globalThis.callAlert
JS объявление используется для упаковки существующего JS кода. Эта статья неофициально относится к промежуточному JS объявлению как схимуJS. JS дермы заполняют пробел между реализацией .NET и существующими JS возможностями и библиотеками. Во многих случаях, таких как предыдущий тривиальный пример, JS не требуется, а методы можно импортировать напрямую, как показано в предыдущем ConsoleLog
примере. Как показано в следующих разделах, JS схим может:
- Инкапсулировать дополнительную логику.
- Типы сопоставления вручную.
- Уменьшите количество объектов или вызовов, пересекающих границу взаимодействия.
- Вручную сопоставлять статические вызовы с методами экземпляра.
Загрузка объявлений JavaScript
JS объявления, которые должны быть импортированы, [JSImport]
обычно загружаются в контексте той же страницы или JS узла, который загружал .NET WebAssembly. Это можно сделать с помощью:
- Блок, объявляющий встроенный
<script>...</script>
JS. - Объявление источника скрипта (
src
<script src="./some.js"></script>
) с загрузкой внешнего JS файла (.js
). - JS Модуль ES6 (
<script type='module' src="./moduleName.js"></script>
). - Модуль ES6, загруженный JS из JSHost.ImportAsync .NET WebAssembly.
Примеры, приведенные в этой статье JSHost.ImportAsync. При вызове ImportAsyncклиентского веб-ресурса .NET WebAssembly запрашивает файл с помощью moduleUrl
параметра, поэтому он ожидает, что файл будет доступен как статический веб-ресурс, так же, как <script>
тег получает файл с URL-адресом src
. Например, следующий код C# в проекте приложения браузера WebAssembly поддерживает JS файл (.js
) по пути /wwwroot/scripts/ExampleShim.js
:
await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");
В зависимости от платформы, загружающей WebAssembly, URL-адрес с точками, например ./scripts/
, может ссылаться на неправильный подкаталог, например /_framework/scripts/
, так как пакет WebAssembly инициализирован скриптами платформы /_framework/
. В этом случае префикс URL-адреса ../scripts/
ссылается на правильный путь. Префикс с работой, /scripts/
если сайт размещен в корне домена. Типичный подход включает настройку правильного базового пути для данной среды с тегом HTML <base>
и использование /scripts/
префикса для ссылки на путь относительно базового пути. Префиксы нотации ~/
Тильды не поддерживаются JSHost.ImportAsync.
Внимание
Если JS загружается из модуля JavaScript, [JSImport]
атрибуты должны включать имя модуля в качестве второго параметра. Например, указывает, [JSImport("globalThis.callAlert", "ExampleShim")]
что импортированный метод объявлен в модуле JavaScript с именем "ExampleShim
".
Сопоставления типов
Параметры и возвращаемые типы в сигнатуре метода .NET автоматически преобразуются в соответствующие JS типы во время выполнения, если поддерживается уникальное сопоставление. Это может привести к преобразованию значений или ссылок, упакованных в тип прокси-сервера. Этот процесс называется маршалингом типа. Используется JSMarshalAsAttribute<T> для управления тем, как импортированные параметры метода и типы возвращаемых значений маршалируются.
Некоторые типы не имеют сопоставления типов по умолчанию. Например, long
можно маршалировать как System.Runtime.InteropServices.JavaScript.JSType.Number или System.Runtime.InteropServices.JavaScript.JSType.BigInt, чтобы JSMarshalAsAttribute<T> избежать ошибки во время компиляции.
Поддерживаются следующие сценарии сопоставления типов:
- Передача Action или Func<TResult> как параметры, которые маршаллируются как вызываемые JS методы. Это позволяет коду .NET вызывать прослушиватели в ответ на JS обратные вызовы или события.
- Передача JS ссылок и управляемых объектов .NET в любом направлении, которые маршалируются как прокси-объекты и сохраняются в живых по границе взаимодействия до тех пор, пока прокси-сервер не собирает мусор.
- Маршаллинг асинхронных JS методов или JS
Promise
с результатом Task , и наоборот.
Большинство маршаллированных типов работают в обоих направлениях в качестве параметров и возвращаемых значений в импортированных и экспортированных методах.
В следующей таблице указаны поддерживаемые сопоставления типов.
.NET | JavaScript | Nullable |
Task ➔Кому Promise |
JSMarshalAs необязательный |
Array of |
---|---|---|---|---|---|
Boolean |
Boolean |
Поддерживается | Поддерживается | Поддерживается | Не поддерживаются |
Byte |
Number |
Поддерживается | Поддерживается | Поддерживается | Поддерживается |
Char |
String |
Поддерживается | Поддерживается | Поддерживается | Не поддерживаются |
Int16 |
Number |
Поддерживается | Поддерживается | Поддерживается | Не поддерживаются |
Int32 |
Number |
Поддерживается | Поддерживается | Поддерживается | Поддерживается |
Int64 |
Number |
Поддерживается | Поддерживается | Не поддерживаются | Не поддерживаются |
Int64 |
BigInt |
Поддерживается | Поддерживается | Не поддерживаются | Не поддерживаются |
Single |
Number |
Поддерживается | Поддерживается | Поддерживается | Не поддерживаются |
Double |
Number |
Поддерживается | Поддерживается | Поддерживается | Поддерживается |
IntPtr |
Number |
Поддерживается | Поддерживается | Поддерживается | Не поддерживаются |
DateTime |
Date |
Поддерживается | Поддерживается | Не поддерживаются | Не поддерживаются |
DateTimeOffset |
Date |
Поддерживается | Поддерживается | Не поддерживаются | Не поддерживаются |
Exception |
Error |
Не поддерживаются | Поддерживается | Поддерживается | Не поддерживаются |
JSObject |
Object |
Не поддерживаются | Поддерживается | Поддерживается | Поддерживается |
String |
String |
Не поддерживаются | Поддерживается | Поддерживается | Поддерживается |
Object |
Any |
Не поддерживаются | Поддерживается | Не поддерживаются | Поддерживается |
Span<Byte> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Span<Int32> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Span<Double> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
ArraySegment<Byte> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
ArraySegment<Int32> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
ArraySegment<Double> |
MemoryView |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Task |
Promise |
Не поддерживаются | Не поддерживаются | Поддерживается | Не поддерживаются |
Action |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Action<T1> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Action<T1, T2> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Action<T1, T2, T3> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Func<TResult> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Func<T1, TResult> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Func<T1, T2, TResult> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Func<T1, T2, T3, TResult> |
Function |
Не поддерживаются | Не поддерживаются | Не поддерживаются | Не поддерживаются |
Следующие условия применяются к сопоставлению типов и маршализированным значениям:
- Столбец
Array of
указывает, можно ли маршалировать тип .NET в виде JSArray
. Пример: C#int[]
(Int32
) сопоставлен с JSArray
Number
s. - При передаче JS значения в C# со значением неправильного типа платформа создает исключение в большинстве случаев. Платформа не выполняет проверку JSтипа во время компиляции.
JSObject
Task
,Exception
и созданиеGCHandle
иArraySegment
прокси-сервер. Вы можете активировать удаление в коде разработчика или разрешить сборку мусора .NET (GC) удалять объекты позже. Эти типы несут значительные затраты на производительность.Array
: маршалинг массива создает копию массива в JS или .NET.MemoryView
MemoryView
JS— это класс среды выполнения .NET WebAssembly для маршалированияSpan
иArraySegment
.- В отличие от маршалинга массива, маршалинг или
Span
ArraySegment
не создает копию базовой памяти. MemoryView
может быть правильно создано средой выполнения .NET WebAssembly. Поэтому невозможно импортировать JS метод как метод .NET, имеющий параметрSpan
илиArraySegment
.MemoryView
создается только дляSpan
срока вызова взаимодействия. КакSpan
и в стеке вызовов, который не сохраняется после вызова взаимодействия, невозможно экспортировать метод .NET, который возвращаетSpan
.MemoryView
создано дляArraySegment
выживания после вызова взаимодействия и полезно для совместного использования буфера. Вызовdispose()
созданногоMemoryView
ArraySegment
для удаления прокси-сервера и открепляет базовый массив .NET. Рекомендуется вызыватьdispose()
блокtry-finally
дляMemoryView
.
Некоторые сочетания сопоставлений типов, требующих вложенных универсальных типов, JSMarshalAs
в настоящее время не поддерживаются. Например, попытка материализации массива из Promise
такого вида, как [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
создание ошибки во время компиляции. Соответствующее решение зависит от сценария, но этот конкретный сценарий рассматривается далее в разделе ограничений сопоставления типов.
JS Примитивы
В следующем примере показано [JSImport]
использование сопоставлений типов нескольких примитивных JS типов и использования JSMarshalAs
, где явные сопоставления требуются во время компиляции.
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");
}
}
В Program.Main
:
await PrimitivesUsage.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
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
В этом разделе показано, как импортировать методы, имеющие объект в качестве возвращаемого JS Date
или параметра. Даты маршалируются по значению взаимодействия, что означает, что они копируются так же, как JS и примитивы.
Объект Date
не зависит от часового пояса. .NET DateTime корректируется относительно его DateTimeKind при маршале до Date
значения, но сведения о часовом поясе не сохраняются. Рассмотрите возможность инициализации DateTime значения или DateTimeKind.Utc DateTimeKind.Local соответствия значению, которое он представляет.
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);
}
}
В Program.Main
:
await DateUsage.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
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)
Предыдущие сведения часового пояса (GMT-0500 (Eastern Standard Time)
) зависят от локального часового пояса компьютера или браузера.
JS Ссылки на объекты
Каждый раз, когда JS метод возвращает ссылку на объект, он представлен в .NET как объект JSObject. Исходный JS объект продолжает свое время существования в JS пределах границы, в то время как код .NET может получить доступ и изменить его по ссылке через JSObject. Хотя сам тип предоставляет ограниченный API, возможность хранения JS ссылки на объект и возврата или передачи его через границу взаимодействия обеспечивает поддержку нескольких сценариев взаимодействия.
Предоставляет JSObject методы для доступа к свойствам, но он не предоставляет прямой доступ к методам экземпляра. Как показано в следующем Summarize
методе, методы экземпляра можно получить косвенно, реализуя статический метод, который принимает экземпляр в качестве параметра.
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);
}
}
В Program.Main
:
await JSObjectUsage.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
{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
Асинхронное взаимодействие
Многие JS API являются асинхронным и сигнальным завершением через обратный вызов, метод Promise
или асинхронный метод. Игнорировать асинхронные возможности часто не являются вариантом, так как последующий код может зависеть от завершения асинхронной операции и должен ожидаться.
JS методы, использующие ключевое async
слово или возвращающие объект Promise
, можно ожидать в C# методом, возвращающим объект Task. Как показано ниже, ключевое слово не используется в методе C# с [JSImport]
атрибутом, async
так как оно не использует await
ключевое слово в нем. Однако использование кода, вызывающего метод, обычно использует await
ключевое слово и помечается как async
показано в PromisesUsage
примере.
JS с обратным вызовом, например setTimeout
, можно упаковать в оболочку Promise
перед возвратом из JS. Упаковка обратного вызова в объект Promise
, как показано в функции, назначенной Wait2Seconds
функции, подходит только в том случае, если обратный вызов вызывается ровно один раз. В противном случае C# Action можно передать для прослушивания обратного вызова, который может вызываться нулевым или много раз, который демонстрируется в разделе "Подписка на JS события ".
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);
});
}
Не используйте ключевое async
слово в сигнатуре метода C#. Task Возврат или Task<TResult> достаточно.
При вызове асинхронных JS методов часто требуется ждать завершения JS выполнения метода. При загрузке ресурса или выполнении запроса, скорее всего, требуется, чтобы следующий код предположил, что действие завершено.
JS Если схим возвращаетсяPromise
, C# может рассматривать его как ожидающее/TaskTask<TResult>.
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}'");
}
}
}
В Program.Main
:
await PromisesUsage.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
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'
Ограничения сопоставления типов
Некоторые сопоставления типов, требующие вложенных универсальных типов в JSMarshalAs
определении, в настоящее время не поддерживаются. Например, возврат Promise
массива, [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
например, создает ошибку во время компиляции. Соответствующее решение зависит от сценария, но один из вариантов состоит в представлении массива JSObject в качестве ссылки. Это может быть достаточно, если доступ к отдельным элементам в .NET не нужен, и ссылка может быть передана другим JS методам, которые действуют в массиве. Кроме того, выделенный метод может принимать ссылку JSObject в качестве параметра и возвращать материализованный массив, как показано в следующем UnwrapJSObjectAsIntArray
примере. В этом случае метод не имеет проверки типов, и разработчик несет ответственность за то, JS чтобы JSObject упаковать соответствующий тип массива.
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);
//...
В Program.Main
:
JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);
Замечания, связанные с быстродействием
Маршаллирование вызовов и затраты на отслеживание объектов через границу взаимодействия дороже, чем собственные операции .NET, но по-прежнему должны продемонстрировать допустимую производительность для типичного веб-приложения с умеренным спросом.
Прокси-серверы объектов, такие как JSObject, которые поддерживают ссылки на границу взаимодействия, имеют дополнительные затраты на память и влияют на то, как сборка мусора влияет на эти объекты. Кроме того, доступная память может быть исчерпана без активации сборки мусора в некоторых сценариях, так как давление на память из JS .NET не является общим. Этот риск является значительным, если чрезмерное количество больших объектов ссылается на границу взаимодействия относительно небольшими JS объектами или наоборот, где на крупные объекты .NET ссылаются JS прокси-серверы. В таких случаях рекомендуется использовать интерфейс на JS объектахIDisposable, using
используя области детерминированного удаления.
Следующие тесты, которые используют предыдущий пример кода, демонстрируют, что операции взаимодействия примерно ниже порядка медленнее, чем те, которые остаются в пределах границы .NET, но операции взаимодействия остаются относительно быстрыми. Кроме того, следует учитывать, что возможности устройства пользователя влияют на производительность.
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;
}
}
В Program.Main
:
JSObjectBenchmark.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
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
Подписка на JS события
Код .NET может подписаться на JS события и обрабатывать JS события путем передачи C# JS Action функции в качестве обработчика. Код JS схима обрабатывает подписку на событие.
Предупреждение
Взаимодействие с отдельными свойствами DOM через JS взаимодействие, как показано в руководстве в этом разделе, относительно медленно и может привести к созданию многих прокси-серверов, которые создают высокое давление сборки мусора. Обычно не рекомендуется использовать следующий шаблон. Используйте следующий шаблон не более чем для нескольких элементов. Дополнительные сведения см. в разделе "Рекомендации по производительности".
removeEventListener
Нюанс заключается в том, что для нее требуется ссылка на функцию, переданную addEventListener
ранее. Когда C# Action передается через границу взаимодействия, он упаковывается в JS прокси-объект. Таким образом, передача одного и того же C# Action addEventListener
в оба и removeEventListener
приводит к созданию двух разных JS прокси-объектов, обтекающих Action. Эти ссылки отличаются, поэтому removeEventListener
не удается найти прослушиватель событий для удаления. Чтобы устранить эту проблему, приведенные ниже примеры упаковывают C# Action в JS функцию и возвращают ссылку в качестве JSObject вызова подписки, который будет передаваться позже в вызов отмены подписки. Так как C# Action возвращается и передается в виде JSObject, для обоих вызовов используется одна и та же ссылка, а прослушиватель событий можно удалить.
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");
}
}
В Program.Main
:
await EventsUsage.Run();
В предыдущем примере отображаются следующие выходные данные в консоли отладки браузера:
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]
Сценарии взаимодействия
В следующих статьях основное внимание уделяется запуску модуля WebAssembly .NET в JS узле, например в браузере:
ASP.NET Core