Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Interoperabilidade de
Observação
Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.
Advertência
Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.
Importante
Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.
Para a versão atual, consulte a versão .NET 9 deste artigo.
Por Aaron Shumaker
Este artigo explica como interagir com JavaScript (JS) no WebAssembly do lado do cliente usando a API de interoperabilidade (System.Runtime.InteropServices.JavaScript) JS[JSImport]
/[JSExport]
.
[JSImport]
/
[JSExport]
interoperabilidade é aplicável ao executar um módulo .NET WebAssembly em um host JS nos seguintes cenários:
- JavaScript '[JSImport]'/'[JSExport]' interoperabilidade com um projeto de Aplicação de Navegador WebAssembly.
- Interoperabilidade JavaScript JSImport/JSExport com ASP.NET Core Blazor.
- Outras plataformas .NET WebAssembly que suportam
[JSImport]
/[JSExport]
interoperabilidade.
Pré-requisitos
SDK do .NET (versão mais recente)
Qualquer um dos seguintes tipos de projeto:
- Um projeto WebAssembly Browser App criado de acordo com JavaScript '[JSImport]'/'[JSExport]' interoperabilidade com um projeto WebAssembly Browser App.
- Um projeto Blazor do lado do cliente criado de acordo com interoperabilidade JSImport/JSExport JavaScript com ASP.NET Core Blazor.
- Um projeto criado para uma plataforma comercial ou de código aberto que suporta interoperabilidade
[JSImport]
/[JSExport]
(System.Runtime.InteropServices.JavaScript API).
Aplicativo de exemplo
Ver ou transferir código de exemplo (como transferir): Selecione uma pasta de versão 8.0 ou posterior que corresponda à versão do .NET que você está adotando. Dentro da pasta 'versão', acede ao exemplo chamado WASMBrowserAppImportExportInterop
.
JS interoperabilidade utilizando atributos [JSImport]
/[JSExport]
O atributo [JSImport]
é aplicado a um método .NET para indicar que um método JS correspondente deve ser chamado quando o método .NET é chamado. Isso permite que os desenvolvedores do .NET definam "importações" que permitem que o código .NET chame JS. Além disso, um Action pode ser passado como parâmetro e JS pode invocar a ação para dar suporte a um retorno de chamada ou padrão de subscrição a eventos.
O atributo [JSExport]
é aplicado a um método .NET para exposição ao código JS. Isso permite que o código JS inicie chamadas para o método .NET.
Importando métodos JS
O exemplo a seguir importa um método JS interno padrão (console.log
) para C#.
[JSImport]
limita-se à importação de métodos de objetos acessíveis globalmente. Por exemplo, log
é um método definido no objeto console
, que é definido no objeto globalmente acessível globalThis
. O método console.log
é mapeado para um método proxy C#, ConsoleLog
, que aceita uma cadeia de caracteres para a mensagem de log:
public partial class GlobalInterop
{
[JSImport("globalThis.console.log")]
public static partial void ConsoleLog(string text);
}
No Program.Main
, ConsoleLog
é chamado com a mensagem para registrar:
GlobalInterop.ConsoleLog("Hello World!");
A saída aparece no console do navegador.
O seguinte demonstra a importação de um método declarado em JS.
O método personalizado seguinte JS (globalThis.callAlert
) cria um diálogo de alerta (window.alert
) com a mensagem passada em text
:
globalThis.callAlert = function (text) {
globalThis.window.alert(text);
}
O método globalThis.callAlert
é mapeado para um método proxy C# (CallAlert
), que aceita uma cadeia de caracteres para a mensagem:
using System.Runtime.InteropServices.JavaScript;
public partial class GlobalInterop
{
[JSImport("globalThis.callAlert")]
public static partial void CallAlert(string text);
}
No Program.Main
, CallAlert
é chamado, passando o texto da mensagem de diálogo de alerta:
GlobalInterop.CallAlert("Hello World");
A classe C# que declara o método [JSImport]
não tem uma implementação. Em tempo de compilação, uma classe parcial gerada pelo código-fonte contém o código .NET que implementa a organização da chamada e os tipos para invocar o método JS correspondente. No Visual Studio, usar as opções Go To Definition ou Go To Implementation navega, respectivamente, para a classe parcial gerada pela fonte ou para a classe parcial definida pelo desenvolvedor.
No exemplo anterior, a declaração de globalThis.callAlert
JS intermediário é usada para encapsular o código JS existente. Este artigo refere-se informalmente à declaração de JS intermédia como um JS shim.
JS ajustes preenchem a diferença entre a implementação do .NET e as capacidades/bibliotecas de JS existentes. Em muitos casos, como no exemplo trivial anterior, o JS shim não é necessário, e os métodos podem ser importados diretamente, como demonstrado no exemplo ConsoleLog
anterior. Como este artigo demonstra nas próximas seções, um JS shim pode:
- Encapsular lógica adicional.
- Mapeie manualmente os tipos.
- Reduza o número de objetos ou chamadas que cruzam o limite de interoperabilidade.
- Mapeie manualmente chamadas estáticas para métodos de instância.
Carregando declarações JavaScript
JS declarações que devem ser importadas com [JSImport]
são normalmente carregadas no contexto da mesma página ou JS host que carregou o .NET WebAssembly. Isto pode ser conseguido com:
- Um bloco
<script>...</script>
declarando JSembutido . - Uma declaração de origem de script (
src
) (<script src="./some.js"></script>
) que carrega um arquivo JS externo (.js
). - Um módulo JS ES6 (
<script type='module' src="./moduleName.js"></script>
). - Um módulo JS ES6 carregado usando JSHost.ImportAsync do .NET WebAssembly.
Exemplos neste artigo usam JSHost.ImportAsync. Ao chamar ImportAsync, o .NET WebAssembly do lado do cliente solicita o arquivo usando o parâmetro moduleUrl
e, portanto, espera que o arquivo seja acessível como um ativo da Web estático, da mesma forma que uma marca <script>
recupera um arquivo com uma URL src
. Por exemplo, o seguinte código C# dentro de um projeto WebAssembly Browser App mantém o arquivo JS (.js
) no caminho /wwwroot/scripts/ExampleShim.js
:
await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");
Dependendo da plataforma que está carregando o WebAssembly, uma URL prefixada por pontos, como ./scripts/
, pode se referir a um subdiretório incorreto, como /_framework/scripts/
, porque o pacote WebAssembly é inicializado por scripts de estrutura em /_framework/
. Nesse caso, prefixar a URL com ../scripts/
refere-se ao caminho correto. O prefixo com /scripts/
funciona se o site estiver hospedado na raiz do domínio. Uma abordagem típica envolve a configuração do caminho base correto para um determinado ambiente com uma marca HTML <base>
e o uso do prefixo /scripts/
para se referir ao caminho relativo ao caminho base. Os prefixos da notação til ~/
não são suportados pelo JSHost.ImportAsync.
Importante
Se JS for carregado de um módulo JavaScript, [JSImport]
atributos deverão incluir o nome do módulo como o segundo parâmetro. Por exemplo, [JSImport("globalThis.callAlert", "ExampleShim")]
indica que o método importado foi declarado em um módulo JavaScript chamado "ExampleShim
".
Mapeamentos de tipo
Parâmetros e tipos de retorno na assinatura do método .NET são convertidos automaticamente para ou a partir de tipos JS apropriados em tempo de execução, se um mapeamento único for suportado. Isso pode resultar em valores convertidos por valor ou referências encapsuladas em um tipo de proxy. Este processo é conhecido como marshalling do tipo . Use JSMarshalAsAttribute<T> para controlar como os parâmetros do método importado e os tipos de retorno são tratados.
Alguns tipos não têm um mapeamento de tipo padrão. Por exemplo, um long
pode ser empacotado como System.Runtime.InteropServices.JavaScript.JSType.Number ou System.Runtime.InteropServices.JavaScript.JSType.BigInt, de modo que o JSMarshalAsAttribute<T> é necessário para evitar um erro em tempo de compilação.
Os seguintes cenários de mapeamento de tipo são suportados:
- Passagem de Action ou Func<TResult> como parâmetros, que são processados como métodos JS chamáveis. Isso permite que o código .NET invoque ouvintes em resposta a retornos de chamada ou eventos JS.
- Passando referências de JS e referências de objetos geridos do .NET em ambas as direções, que são encaminhadas como objetos proxy e mantidas vivas através do limite de interoperabilidade até que o proxy seja recolhido pelo coletor de lixo.
- Marshalização de métodos assíncronos JS ou de um JS
Promise
com um resultado Task, e vice-versa.
A maioria dos tipos organizados funciona em ambas as direções, como parâmetros e como valores de retorno, em métodos importados e exportados.
A tabela a seguir indica os mapeamentos de tipo suportados.
.NET | Javascript | Nullable |
Task
paraPromise |
JSMarshalAs opcional |
Array of |
---|---|---|---|---|---|
Boolean |
Boolean |
Suportado | Suportado | Suportado | Não suportado |
Byte |
Number |
Suportado | Suportado | Suportado | Suportado |
Char |
String |
Suportado | Suportado | Suportado | Não suportado |
Int16 |
Number |
Suportado | Suportado | Suportado | Não suportado |
Int32 |
Number |
Suportado | Suportado | Suportado | Suportado |
Int64 |
Number |
Suportado | Suportado | Não suportado | Não suportado |
Int64 |
BigInt |
Suportado | Suportado | Não suportado | Não suportado |
Single |
Number |
Suportado | Suportado | Suportado | Não suportado |
Double |
Number |
Suportado | Suportado | Suportado | Suportado |
IntPtr |
Number |
Suportado | Suportado | Suportado | Não suportado |
DateTime |
Date |
Suportado | Suportado | Não suportado | Não suportado |
DateTimeOffset |
Date |
Suportado | Suportado | Não suportado | Não suportado |
Exception |
Error |
Não suportado | Suportado | Suportado | Não suportado |
JSObject |
Object |
Não suportado | Suportado | Suportado | Suportado |
String |
String |
Não suportado | Suportado | Suportado | Suportado |
Object |
Any |
Não suportado | Suportado | Não suportado | Suportado |
Span<Byte> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
Span<Int32> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
Span<Double> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
ArraySegment<Byte> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
ArraySegment<Int32> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
ArraySegment<Double> |
MemoryView |
Não suportado | Não suportado | Não suportado | Não suportado |
Task |
Promise |
Não suportado | Não suportado | Suportado | Não suportado |
Action |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Action<T1> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Action<T1, T2> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Action<T1, T2, T3> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Func<TResult> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Func<T1, TResult> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Func<T1, T2, TResult> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Func<T1, T2, T3, TResult> |
Function |
Não suportado | Não suportado | Não suportado | Não suportado |
Aplicam-se as seguintes condições à cartografia de tipo e aos valores agrupados:
- A coluna
Array of
indica se o tipo .NET pode ser empacotado como um JSArray
. Exemplo: C#int[]
(Int32
) mapeado para JSArray
deNumber
s. - Ao passar um valor JS para C# com um valor do tipo errado, o framework lança uma exceção na maioria das vezes. A estrutura não realiza a verificação de tipo em tempo de compilação em JS.
-
JSObject
,Exception
,Task
eArraySegment
criamGCHandle
e um proxy. Você pode acionar o descarte no código do desenvolvedor ou permitir que de coleta de lixo (GC) do .NET elimine os objetos posteriormente. Esses tipos carregam uma sobrecarga de desempenho significativa. -
Array
: A organização de uma matriz cria uma cópia da matriz no JS ou na plataforma .NET. MemoryView
-
MemoryView
é uma classe JS para o tempo de execução do .NET WebAssembly para gerirSpan
eArraySegment
. - Ao contrário de empacotar uma matriz, empacotar uma
Span
ouArraySegment
não cria uma cópia da memória subjacente. -
MemoryView
só pode ser instanciado corretamente pelo runtime do .NET WebAssembly. Portanto, não é possível importar um método JS como um método .NET que tenha um parâmetro deSpan
ouArraySegment
. -
MemoryView
criado para umSpan
só é válido durante a chamada de interoperabilidade. ComoSpan
é alocado na pilha de chamadas, que não persiste após a chamada de interoperabilidade, não é possível exportar um método .NET que retorna umSpan
. -
MemoryView
criado para umArraySegment
sobrevive após a chamada de interoperabilidade e é útil para compartilhar um buffer. Chamardispose()
em umMemoryView
criado para umArraySegment
elimina o proxy e desafixa a matriz .NET subjacente. Recomendamos contactardispose()
num bloco detry-finally
paraMemoryView
.
-
Algumas combinações de mapeamentos de tipo que exigem tipos genéricos aninhados em JSMarshalAs
não são suportadas no momento. Por exemplo, tentar materializar uma matriz a partir de um Promise
como [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
gera um erro em tempo de compilação. Uma solução alternativa apropriada varia dependendo do cenário, mas este cenário específico é mais explorado na seção Limitações de mapeamento de Tipo.
JS primitivos
O exemplo a seguir demonstra [JSImport]
aproveitando mapeamentos de tipo de vários tipos de JS primitivos e o uso de JSMarshalAs
, onde mapeamentos explícitos são necessários em tempo de compilação.
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");
}
}
Em Program.Main
:
await PrimitivesUsage.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
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
JS
Date
objetos
O exemplo nesta seção demonstra a importação de métodos que têm um objeto JS Date
como seu retorno ou parâmetro. As datas são agrupadas através do valor de interoperabilidade, o que significa que são copiadas da mesma forma que JS primitivos.
Um objeto Date
é agnóstico de fuso horário. Um DateTime .NET é ajustado em relação ao seu DateTimeKind quando empacotado para um Date
, mas as informações de fuso horário não são preservadas. Considere inicializar um DateTime com um DateTimeKind.Utc ou DateTimeKind.Local consistente com o valor que ele representa.
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);
}
}
Em Program.Main
:
await DateUsage.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
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)
As informações de fuso horário anteriores (GMT-0500 (Eastern Standard Time)
) dependem do fuso horário local do seu computador/navegador.
JS referências de objeto
Sempre que um método JS retorna uma referência de objeto, ele é representado no .NET como um JSObject. O objeto JS original continua seu tempo de vida dentro do limite JS, enquanto o código .NET pode acessá-lo e modificá-lo por referência através do JSObject. Embora o tipo em si exponha uma API limitada, a capacidade de manter uma referência de objeto JS e retorná-la ou passá-la através do limite de interoperabilidade permite o suporte para vários cenários de interoperabilidade.
O JSObject fornece métodos para acessar propriedades, mas não fornece acesso direto a métodos de instância. Como o método de Summarize
a seguir demonstra, os métodos de instância podem ser acessados indiretamente implementando um método estático que usa a instância como um parâmetro.
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);
}
}
Em Program.Main
:
await JSObjectUsage.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
{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
Interoperabilidade assíncrona
Muitas APIs JS são assíncronas e sinalizam a conclusão por meio de um retorno de chamada, um Promise
ou um método assíncrono. Ignorar os recursos assíncronos muitas vezes não é uma opção, pois o código subsequente pode depender da conclusão da operação assíncrona e deve ser aguardado.
JS métodos usando a palavra-chave async
ou retornando um Promise
podem ser aguardados em C# por um método que retorna um Task. Como demonstrado abaixo, a palavra-chave async
não é usada no método C# com o atributo [JSImport]
porque não usa a palavra-chave await
dentro dele. No entanto, consumir código chamando o método normalmente utilizaria a palavra-chave await
e seria assinalado como async
, como demonstrado no exemplo PromisesUsage
.
JS com um retorno de chamada, como um setTimeout
, pode ser envolvido num Promise
antes de retornar de JS. Envolver um callback em um Promise
, como demonstrado na função atribuída a Wait2Seconds
, é apenas apropriado quando o callback é chamado exatamente uma vez. Caso contrário, um Action C# pode ser passado para monitorizar uma chamada de retorno que pode ser invocada zero ou várias vezes, o que é demonstrado na seção Assinando eventos 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);
});
}
Não use a palavra-chave async
na assinatura do método C#. Devolver Task ou Task<TResult> é suficiente.
Ao chamar métodos JS assíncronos, geralmente queremos esperar até que o método JS conclua a execução. Se estiver carregando um recurso ou fazendo uma solicitação, provavelmente queremos que o código a seguir assuma que a ação foi concluída.
Se o JS shim retornar um Promise
, então o C# pode tratá-lo como um Task/Task<TResult>aguardado.
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}'");
}
}
}
Em Program.Main
:
await PromisesUsage.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
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'
Limitações do mapeamento de tipo
Alguns mapeamentos de tipo que exigem tipos genéricos aninhados na definição de JSMarshalAs
não são atualmente suportados. Por exemplo, retornar um Promise
para uma matriz como [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
gera um erro em tempo de compilação. Uma solução alternativa apropriada varia dependendo do cenário, mas uma opção é representar a matriz como uma referência JSObject. Isso pode ser suficiente se o acesso a elementos individuais no .NET não for necessário e a referência puder ser passada para outros métodos JS que atuam na matriz. Como alternativa, um método dedicado pode tomar a referência JSObject como um parâmetro e retornar a matriz materializada, como demonstrado pelo exemplo de UnwrapJSObjectAsIntArray
a seguir. Nesse caso, o método JS não tem verificação de tipo, e o desenvolvedor tem a responsabilidade de garantir que um JSObject encapsulando o tipo de matriz apropriado seja passado.
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);
//...
Em Program.Main
:
JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);
Considerações sobre desempenho
A administração de chamadas e o acompanhamento de objetos através da fronteira de interoperabilidade são mais dispendiosos do que as operações nativas do .NET, mas ainda devem proporcionar um desempenho aceitável para uma aplicação web típica com demanda moderada.
Proxies de objeto, como JSObject, que mantêm referências através do limite de interoperabilidade, têm sobrecarga de memória adicional e afetam como a coleta de lixo afeta esses objetos. Além disso, a memória disponível pode esgotar-se sem que a coleta de lixo seja acionada em alguns cenários, porque a pressão de memória do JS e do .NET não é compartilhada. Esse risco é significativo quando um número excessivo de objetos grandes são referenciados através do limite de interoperabilidade por objetos JS relativamente pequenos, ou vice-versa, onde objetos .NET grandes são referenciados por proxies JS. Nesses casos, recomendamos seguir padrões de descarte determinísticos com escopos using
aproveitando a interface IDisposable em objetos JS.
Os benchmarks a seguir, que aproveitam o código de exemplo anterior, demonstram que as operações de interoperabilidade são aproximadamente uma ordem de grandeza mais lentas do que aquelas que permanecem dentro do limite do .NET, mas as operações de interoperabilidade permanecem relativamente rápidas. Além disso, considere que os recursos do dispositivo de um usuário afetam o desempenho.
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;
}
}
Em Program.Main
:
JSObjectBenchmark.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
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
Subscrição de eventos JS
O código .NET pode inscrever-se em eventos JS e manipular eventos JS, passando um Action C# para uma função JS para atuar como manipulador. O código shim JS trata da subscrição ao evento.
Advertência
A interação com propriedades individuais do DOM através da interoperabilidade JS, conforme demonstra a orientação nesta seção, é relativamente lenta e pode levar à criação de muitos proxies que geram alta pressão na coleta de lixo. O padrão a seguir geralmente não é recomendado. Utilize o padrão a seguir apenas para alguns elementos. Para obter mais informações, consulte a seção
Uma nuance do removeEventListener
é que ele requer uma referência à função passada anteriormente para addEventListener
. Quando um Action C# é passado através do limite de interoperabilidade, ele é encapsulado em um objeto proxy JS. Portanto, passar o mesmo Action C# para addEventListener
e removeEventListener
resulta na geração de dois objetos proxy JS diferentes que encapsulam o Action. Essas referências são diferentes, portanto, removeEventListener
não consegue encontrar o listener de eventos para remover. Para resolver este problema, os exemplos a seguir encapsulam o Action C# em uma função JS e retornam a referência como um JSObject da chamada de subscrição para posteriormente passá-la para a de cancelamento. Como o Action C# é retornado e passado como um JSObject, a mesma referência é utilizada para ambas as chamadas, e o listener de eventos pode ser removido.
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");
}
}
Em Program.Main
:
await EventsUsage.Run();
O exemplo anterior exibe a seguinte saída no console de depuração do navegador:
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]
cenários de interoperabilidade
Os artigos a seguir se concentram na execução de um módulo .NET WebAssembly em um host JS, como um navegador: