Compartir vía


Ejecución de .NET desde JavaScript

Nota:

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

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulte 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 .NET 8 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, consulte la guía Configuración y hospedaje de aplicaciones WebAssembly de .NET en el repositorio de GitHub (dotnet/runtime) en tiempo de ejecución 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, consulte 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

Instale la última versión del SDK de .NET.

Instale 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. Seleccione la opción herramientas de compilación WebAssembly de .NET de la lista de componentes opcionales.

Opcionalmente, instale 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 tiene previsto integrar la interoperabilidad JS[JSImport]/[JSExport] en una aplicación existente deJS.

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, consulte 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:

  • Establezca 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.

  • Habilite 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. Tenga cuidado al implementar su propio código no seguro en aplicaciones .NET, pues puede introducir riesgos de seguridad y estabilidad. Para obtener más información, consulte: 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>
  • Establezca el moniker de la plataforma de destino:

    <TargetFramework>net7.0</TargetFramework>
    

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

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

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

    <OutputType>Exe</OutputType>
    
  • Habilite 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. Tenga cuidado al implementar su propio código no seguro en aplicaciones .NET, pues puede introducir riesgos de seguridad y estabilidad. Para obtener más información, consulte: Código no seguro, tipos de puntero y punteros de función.

  • Especifique 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 en el disco JS 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, copie 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 el resto de la aplicación. Para las implementaciones de producción, publique la aplicación con el comando dotnet publish -c Release en un shell de comandos e implemente 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#, use 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, puede usar tipos de .NET para parámetros y valores devueltos, que el tiempo de ejecución serializa automáticamente. Use JSMarshalAsAttribute<T> para controlar cómo se serializarán los parámetros del método importado. Por ejemplo, puede optar por serializar long como System.Runtime.InteropServices.JavaScript.JSType.Number o System.Runtime.InteropServices.JavaScript.JSType.BigInt. Puede pasar devoluciones de llamada Action/Func<TResult> como parámetros, que se serializarán como funciones JS invocables. Puede 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 puede 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.

En la tabla siguiente se indican las asignaciones de tipos admitidas.

.NET JavaScript Nullable Task a Promise JSMarshalAs opcional Array of
Boolean Boolean Compatible Compatible Compatible No compatible
Byte Number Compatible Compatible Compatible Compatible
Char String Compatible Compatible Compatible No compatible
Int16 Number Compatible Compatible Compatible No compatible
Int32 Number Compatible Compatible Compatible Compatible
Int64 Number Compatible Compatible No compatible No compatible
Int64 BigInt Compatible Compatible No compatible No compatible
Single Number Compatible Compatible Compatible No compatible
Double Number Compatible Compatible Compatible Compatible
IntPtr Number Compatible Compatible Compatible No compatible
DateTime Date Compatible Compatible No compatible No compatible
DateTimeOffset Date Compatible Compatible No compatible No compatible
Exception Error No compatible Compatible Compatible No compatible
JSObject Object No compatible Compatible Compatible Compatible
String String No compatible Compatible Compatible Compatible
Object Any No compatible Compatible No compatible Compatible
Span<Byte> MemoryView No compatible No compatible No compatible No compatible
Span<Int32> MemoryView No compatible No compatible No compatible No compatible
Span<Double> MemoryView No compatible No compatible No compatible No compatible
ArraySegment<Byte> MemoryView No compatible No compatible No compatible No compatible
ArraySegment<Int32> MemoryView No compatible No compatible No compatible No compatible
ArraySegment<Double> MemoryView No compatible No compatible No compatible No compatible
Task Promise No compatible No compatible Compatible No compatible
Action Function No compatible No compatible No compatible No compatible
Action<T1> Function No compatible No compatible No compatible No compatible
Action<T1, T2> Function No compatible No compatible No compatible No compatible
Action<T1, T2, T3> Function No compatible No compatible No compatible No compatible
Func<TResult> Function No compatible No compatible No compatible No compatible
Func<T1, TResult> Function No compatible No compatible No compatible No compatible
Func<T1, T2, TResult> Function No compatible No compatible No compatible No compatible
Func<T1, T2, T3, TResult> Function No compatible No compatible No compatible No compatible

Las condiciones siguientes se aplican a la asignación de tipos y a los valores serializados:

  • La columna Array of indica si el tipo de .NET se puede serializar como Array de JS. Ejemplo: C# int[] (Int32) asignado a Array de JS de Numbers.
  • Al pasar un valor JS a C# con un valor del tipo incorrecto, el marco produce una excepción en la mayoría de los casos. El marco de trabajo no realiza la comprobación de tipos en tiempo de compilación en JS.
  • JSObject, Exception, Task y ArraySegment crean GCHandle y un proxy. Puede desencadenar la eliminación en el código de desarrollador o permitir que la recolección de elementos no utilizados de .NET elimine los objetos más adelante. Estos tipos conllevan una sobrecarga de rendimiento significativa.
  • Array: al serializar una matriz se crea una copia de dicha matriz en JS o .NET.
  • MemoryView
    • MemoryView es una clase JS para que el tiempo de ejecución de WebAssembly de .NET serialice Span y ArraySegment.
    • A diferencia de serializar una matriz, serializar Span o ArraySegment no crea una copia de la memoria subyacente.
    • Solo se pueden crear instancias de MemoryView correctamente en el tiempo de ejecución de WebAssembly de .NET. Por lo tanto, no es posible importar una función JS como un método de .NET que tenga un parámetro de Span o ArraySegment.
    • Un elemento MemoryView creado para un elemento Span solo es válido durante la llamada de interoperabilidad. Como Span se asigna en la pila de llamadas, la cual no se conserva después de la llamada de interoperabilidad, no es posible exportar un método de .NET que devuelva un Span.
    • Un elemento MemoryView creado para un elemento ArraySegment sigue disponible después de la llamada de interoperabilidad y es útil para compartir un búfer. La llamada a dispose() en un elemento MemoryView creado para un elemento ArraySegment elimina el proxy y desancla la matriz .NET subyacente. Se recomienda llamar a dispose() en un bloque try-finally para MemoryView.

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, use 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, instale 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

Puede 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, puede crear la aplicación mediante la plantilla de proyecto WebAssembly Browser App.

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

dotnet build

Compile y ejecute la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet run

Como alternativa, instale y use 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

Puede 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, puede crear la aplicación mediante la plantilla de proyecto WebAssembly Console App.

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

dotnet build

Compile y ejecute la aplicación desde Visual Studio o mediante la CLI de .NET:

dotnet run

Como alternativa, inicie 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