Compartir a través de


Interoperabilidad [JSImport]/[JSExport] de JavaScript

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

En este artículo se explica cómo ejecutar .NET desde JavaScript (JS) mediante la interoperabilidad JS[JSImport]/[JSExport].

Para obtener instrucciones adicionales, consulta la guía Configuración y hospedaje de aplicaciones WebAssembly de .NET en el repositorio de GitHub (dotnet/runtime) en runtime de .NET.

Las aplicacionesJS existentes pueden usar la compatibilidad ampliada con WebAssembly del lado del cliente para reutilizar bibliotecas .NET JS o para crear nuevas aplicaciones y marcos basados en .NET.

Nota:

Este artículo se centra en la ejecución de .NET desde aplicaciones JS sin ninguna dependencia de Blazor. Para obtener instrucciones sobre cómo usar la interoperabilidad [JSImport]/[JSExport] en aplicaciones Blazor WebAssembly, consulta Interoperabilidad JSImport/JSExport de JavaScript con ASP.NET Core Blazor.

Estos enfoques son adecuados cuando solo se espera que se ejecute en WebAssembly (WASM). Las bibliotecas pueden realizar una comprobación en tiempo de ejecución para determinar si la aplicación se ejecuta en WASM mediante una llamada a OperatingSystem.IsBrowser.

Requisitos previos

SDK de .NET (versión más reciente)

Instala la carga de trabajo de wasm-tools en un shell de comandos administrativo, que incluye los destinos de MSBuild relacionados:

dotnet workload install wasm-tools

Las herramientas también se pueden instalar mediante el instalador de Visual Studio en la carga de trabajo ASP.NET y desarrollo web en el instalador de Visual Studio. Selecciona la opción herramientas de compilación WebAssembly de .NET de la lista de componentes opcionales.

Opcionalmente, instala la carga de trabajo wasm-experimental, que contiene plantillas de proyecto experimentales para empezar a trabajar con .NET en WebAssembly en una aplicación de explorador (aplicación de explorador de WebAssembly) o en una aplicación de consola basada en Node.js (aplicación de consola WebAssembly). Esta carga de trabajo no es necesaria si tienes previsto integrar la interoperabilidad JS[JSImport]/[JSExport] en una aplicación JS existente.

dotnet workload install wasm-experimental

Las plantillas también se pueden instalar desde el paquete NuGet de Microsoft.NET.Runtime.WebAssembly.Templates con el siguiente comando:

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

Para obtener más información, consulta la sección: Plantillas de proyecto y carga de trabajo experimentales.

Espacio de nombres

La API de interoperabilidad JS descrita en este artículo se controla mediante atributos del espacio de nombres System.Runtime.InteropServices.JavaScript.

Configuración de proyecto

Para configurar un proyecto (.csproj) para habilitar la interoperabilidad JS:

  • Establece el moniker de la plataforma de destino ({TARGET FRAMEWORK} marcador de posición):

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    Se admite .NET 7 (net7.0) o posterior.

  • Habilita la propiedad AllowUnsafeBlocks, que permite que el generador de código del compilador de Roslyn use punteros para la interoperabilidad JS:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Advertencia

    La API de interoperabilidad JS requiere habilitar AllowUnsafeBlocks. Ten cuidado al implementar tu propio código no seguro en aplicaciones .NET, pues puedes introducir riesgos de seguridad y estabilidad. Para obtener más información, consulta: Código no seguro, tipos de puntero y punteros de función.

A continuación se muestra un archivo de proyecto de ejemplo (.csproj) después de la configuración. El marcador de posición {TARGET FRAMEWORK} es la plataforma de destino:

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

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

</Project>
  • Establece el moniker de la plataforma de destino:

    <TargetFramework>net7.0</TargetFramework>
    

    Se admite .NET 7 (net7.0) o posterior.

  • Especifica browser-wasm para el identificador en tiempo de ejecución:

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • Especifica un tipo de salida ejecutable:

    <OutputType>Exe</OutputType>
    
  • Habilita la propiedad AllowUnsafeBlocks, que permite que el generador de código del compilador de Roslyn use punteros para la interoperabilidad JS:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Advertencia

    La API de interoperabilidad JS requiere habilitar AllowUnsafeBlocks. Ten cuidado al implementar tu propio código no seguro en aplicaciones .NET, pues puedes introducir riesgos de seguridad y estabilidad. Para obtener más información, consulta: Código no seguro, tipos de puntero y punteros de función.

  • Especifica a WasmMainJSPath para que apunte a un archivo en el disco. Este archivo se publica con la aplicación, pero el uso del archivo no es necesario si va a integrar .NET en una aplicación JS existente.

    En el ejemplo siguiente, el archivo JS en el disco es main.js, pero se permite cualquier nombre de archivo JS:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

Archivo de proyecto de ejemplo (.csproj) después de la configuración:

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

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <WasmMainJSPath>main.js</WasmMainJSPath>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Interoperabilidad de JavaScript en WASM

Las API del ejemplo siguiente se importan desde dotnet.js. Estas API permiten configurar módulos con nombre que se pueden importar en el código de C# y llamar a métodos expuestos por el código de .NET, incluido Program.Main.

Importante

Los términos "importar" y "exportar" se definen en este artículo desde la perspectiva de .NET:

  • Una aplicación importa métodos JS para que se puedan llamar desde .NET.
  • La aplicación exporta métodos de .NET para que se puedan llamar desde JS.

En el ejemplo siguiente:

  • El archivo dotnet.js se usa para crear e iniciar el tiempo de ejecución de WebAssembly de .NET. dotnet.js se genera como parte de la salida de compilación de la aplicación.

    Importante

    Para integrar con una aplicación existente, copia el contenido de la carpeta† de salida de publicación en los recursos de implementación de la aplicación existente para que se pueda servir junto con rest de la aplicación. Para las implementaciones de producción, publica la aplicación con el comando dotnet publish -c Release en un shell de comandos e implementa el contenido de la carpeta de salida con la aplicación.

    †La carpeta de salida de publicación es la ubicación de destino del perfil de publicación. El valor predeterminado de un perfil de Release en .NET 8 o posterior es bin/Release/{TARGET FRAMEWORK}/publish, donde el marcador de posición de {TARGET FRAMEWORK} es la plataforma de destino (por ejemplo, net8.0).

  • dotnet.create() configura el tiempo de ejecución de WebAssembly de .NET.

  • setModuleImports asocia un nombre a un módulo de funciones JS para la importación en .NET. El módulo JS contiene una función dom.setInnerText, que acepta y el selector de elementos y la hora para mostrar la hora actual del cronómetro en la interfaz de usuario. El nombre del módulo puede ser cualquier cadena (no es necesario que sea un nombre de archivo), pero debe coincidir con el nombre usado con JSImportAttribute (que se explica más adelante en este artículo). La función dom.setInnerText se importa en C# y la llama el método SetInnerTextde C#. El método SetInnerText se muestra más adelante en esta sección.

  • exports.StopwatchSample.Reset() llama a .NET (StopwatchSample.Reset) desde JS. El método Reset C# reinicia el cronómetro si se está ejecutando o lo restablece si no se está ejecutando. El método Reset se muestra más adelante en esta sección.

  • exports.StopwatchSample.Toggle() llama a .NET (StopwatchSample.Toggle) desde JS. El método Toggle C# inicia o detiene el cronómetro en función de si se está ejecutando o no. El método Toggle se muestra más adelante en esta sección.

  • runMain() ejecuta Program.Main.

  • setModuleImports asocia un nombre a un módulo de funciones JS para la importación en .NET. El módulo JS contiene una función window.location.href, la cual devuelve la dirección de página actual (URL). El nombre del módulo puede ser cualquier cadena (no es necesario que sea un nombre de archivo), pero debe coincidir con el nombre usado con JSImportAttribute (que se explica más adelante en este artículo). La función window.location.href se importa en C# y la llama el método GetHRefde C#. El método GetHRef se muestra más adelante en esta sección.

  • exports.MyClass.Greeting() llama a .NET (MyClass.Greeting) desde JS. El método Greeting de C# devuelve una cadena que incluye el resultado de llamar a la función window.location.href. El método Greeting se muestra más adelante en esta sección.

  • dotnet.run() ejecuta Program.Main.

Módulo JS:

import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig, runMain } = await dotnet
  .withApplicationArguments("start")
  .create();

setModuleImports('main.js', {
  dom: {
    setInnerText: (selector, time) => 
      document.querySelector(selector).innerText = time
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);

document.getElementById('reset').addEventListener('click', e => {
  exports.StopwatchSample.Reset();
  e.preventDefault();
});

const pauseButton = document.getElementById('pause');
pauseButton.addEventListener('click', e => {
  const isRunning = exports.StopwatchSample.Toggle();
  pauseButton.innerText = isRunning ? 'Pause' : 'Start';
  e.preventDefault();
});

await runMain();
import { dotnet } from './_framework/dotnet.js'

const { setModuleImports, getAssemblyExports, getConfig } = await dotnet
  .withDiagnosticTracing(false)
  .withApplicationArgumentsFromQuery()
  .create();

setModuleImports('main.js', {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById('out').innerHTML = text;
await dotnet.run();
import { dotnet } from './dotnet.js'

const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);

const { setModuleImports, getAssemblyExports, getConfig } = 
  await dotnet.create();

setModuleImports("main.js", {
  window: {
    location: {
      href: () => globalThis.window.location.href
    }
  }
});

const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

document.getElementById("out").innerHTML = text;
await dotnet.run();

Para importar una función JS para que se pueda llamar desde C#, usa el nuevo JSImportAttribute en una firma de método coincidente. El primer parámetro para el atributo JSImportAttribute es el nombre de la función JS que se va a importar, y el segundo es el nombre módulo.

En el ejemplo siguiente, se llama a la función dom.setInnerText desde el módulo main.js cuando se llama al método SetInnerText:

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

En el ejemplo siguiente, se llama a la función window.location.href desde el módulo main.js cuando se llama al método GetHRef:

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

En la firma del método importado, puedes usar tipos de .NET para parámetros y valores devueltos, que el tiempo de ejecución serializa automáticamente. Usa JSMarshalAsAttribute<T> para controlar cómo se serializarán los parámetros del método importado. Por ejemplo, puedes optar por serializar long como System.Runtime.InteropServices.JavaScript.JSType.Number o System.Runtime.InteropServices.JavaScript.JSType.BigInt. Puedes pasar devoluciones de llamada Action/Func<TResult> como parámetros, que se serializarán como funciones JS invocables. Puedes pasar JS y las referencias de objetos administrados, y se serializarán como objetos proxy, manteniendo el objeto activo a través del límite hasta que se recopile el proxy como elemento no utilizado. También puedes importar y exportar métodos asincrónicos con un resultado Task, que se serializan como promesas JS. La mayoría de los tipos serializados funcionan en ambas direcciones como parámetros y valores devueltos tanto en métodos importados como exportados.

Las funciones accesibles en el espacio de nombres global se pueden importar mediante el prefijo globalThis en el nombre de la función y mediante el uso del atributo [JSImport] sin proporcionar un nombre de módulo. En el ejemplo siguiente, console.log tiene el prefijo globalThis. El método Log de C# llama a la función importada y acepta un mensaje de cadena de C# (message) y serializa la cadena de C# en un JSString para console.log:

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

Para exportar un método de .NET y que se le pueda llamar desde JS, usa el JSExportAttribute.

En el ejemplo siguiente, cada método se exporta a JS y se puede llamar desde JS funciones:

  • El método Toggle inicia o detiene el cronómetro en función de su estado de ejecución.
  • El método Reset reinicia el cronómetro si se está ejecutando o lo restablece si no se está ejecutando.
  • El método IsRunning indica si se está ejecutando el cronómetro.
[JSExport]
internal static bool Toggle()
{
    if (stopwatch.IsRunning)
    {
        stopwatch.Stop();
        return false;
    }
    else
    {
        stopwatch.Start();
        return true;
    }
}

[JSExport]
internal static void Reset()
{
    if (stopwatch.IsRunning)
        stopwatch.Restart();
    else
        stopwatch.Reset();

    Render();
}

[JSExport]
internal static bool IsRunning() => stopwatch.IsRunning;

En el ejemplo siguiente, el método Greeting devuelve una cadena que incluye el resultado de llamar al método GetHRef. Como se ha mostrado anteriormente, el método GetHref de C# llama a JS para la función window.location.href desde el módulo main.js. window.location.href devuelve la dirección de página actual (URL):

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

Plantillas de proyecto y carga de trabajo experimentales

Para demostrar la funcionalidad de interoperabilidad JS y obtener plantillas de proyecto de interoperabilidad JS, instala la carga de trabajo wasm-experimental:

dotnet workload install wasm-experimental

La carga de trabajo wasm-experimental contiene dos plantillas de proyecto: wasmbrowser y wasmconsole. Estas plantillas son experimentales en este momento, lo que significa que el flujo de trabajo del desarrollador de las plantillas está evolucionando. Sin embargo, las API de .NET y JS que se usan en las plantillas se admiten en .NET 8 y proporcionan una base para usar .NET en WASM desde JS.

Las plantillas también se pueden instalar desde el paquete NuGet de Microsoft.NET.Runtime.WebAssembly.Templates con el siguiente comando:

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

Aplicación de explorador

Puedes crear una aplicación de explorador con la plantilla de wasmbrowser desde la línea de comandos, que crea una aplicación web que muestra el uso de .NET y JS juntos en un explorador:

dotnet new wasmbrowser

Como alternativa, en Visual Studio, puedes crear la aplicación mediante la plantilla de proyecto WebAssembly Browser App.

Compila la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet build

Compila y ejecuta la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet run

Como alternativa, instala y usa el dotnet servecomando:

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

En el ejemplo anterior, el marcador de posición {TARGET FRAMEWORK} es el moniker de la plataforma de destino.

Aplicación de consola de Node.js

Puedes crear una aplicación de consola con la plantilla wasmconsole, que crea una aplicación que se ejecuta en WASM como una aplicación de consola de Node.js o V8:

dotnet new wasmconsole

Como alternativa, en Visual Studio, puedes crear la aplicación mediante la plantilla de proyecto WebAssembly Console App.

Compila la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet build

Compila y ejecuta la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet run

Como alternativa, inicia cualquier servidor de archivos estático desde el directorio de salida de publicación que contenga el archivo main.mjs:

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

En el ejemplo anterior, el marcador de posición {TARGET FRAMEWORK} es el moniker de la plataforma de destino, y el marcador de posición {PATH} es la ruta de acceso al archivo main.mjs.

Recursos adicionales