Compartilhar via


Interop JavaScript [JSImport]/[JSExport] com um projeto de aplicativo de navegador WebAssembly

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.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica como configurar um projeto de aplicativo de navegador WebAssembly para executar o .NET a partir do JavaScript (JS) usando interop JS[JSImport]/[JSExport]. Para obter informações adicionais e exemplos, consulte Interop de `[JSImport]`/`[JSExport]` do JavaScript no .NET WebAssembly.

Para obter orientações adicionais, confira as diretrizes de Configuração e hospedagem de aplicativos WebAssembly do .NET no repositório GitHub do .NET Runtime (dotnet/runtime).

Os aplicativos existentes JS podem usar o suporte do WebAssembly do lado do cliente expandido para reutilizar bibliotecas .NET de JS ou para compilar novas estruturas e aplicativos baseados em .NET.

Observação

Esse artigo se concentra na execução do .NET de aplicativos JS sem nenhuma dependência no Blazor. Para obter diretrizes sobre como usar interoperabilidade [JSImport]/[JSExport] em aplicativos Blazor WebAssembly, consulte interop de JSImport/JSExport do JavaScript com ASP.NET Core Blazor.

Essas abordagens são apropriadas quando você espera apenas que o aplicativo Blazor execute no WebAssembly (WASM). As bibliotecas podem fazer uma verificação de runtime para determinar se o aplicativo está em execução no WASM chamando OperatingSystem.IsBrowser.

Pré-requisitos

SDK do .NET (versão mais recente)

Instale a carga de trabalho wasm-tools em um shell de comando administrativo, que traz os destinos do MSBuild relacionados:

dotnet workload install wasm-tools

As ferramentas também podem ser instaladas por meio do instalador do Visual Studio na carga de trabalho de ASP.NET e desenvolvimento da Web no instalador do Visual Studio. Selecione a opção ferramentas de build do .NET WebAssembly na lista de componentes opcionais.

Opcionalmente, instale a carga de trabalho wasm-experimental, que adiciona os seguintes modelos de projeto experimental:

  • Aplicativo de navegador WebAssembly para começar a usar o .NET no WebAssembly em um aplicativo de navegador.
  • Aplicativo de Console WebAssembly para introdução em um aplicativo de console baseado em Node.js.

Depois de instalar a carga de trabalho, esses novos modelos podem ser selecionados ao criar um novo projeto. Essa carga de trabalho não será necessária se você planeja integrar JS[JSImport]/[JSExport] a interoperabilidade a um aplicativo existente JS.

dotnet workload install wasm-experimental

Os modelos também podem ser instalados no pacote NuGet Microsoft.NET.Runtime.WebAssembly.Templates com o seguinte comando:

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

Para obter mais informações, consulte a seção Modelos experimentais de carga de trabalho e projeto.

Namespace

A API de interoperabilidade JS descrita neste artigo é controlada por atributos no namespace System.Runtime.InteropServices.JavaScript.

Configuração do projeto

Para configurar um projeto (.csproj) para habilitar a JS interoperabilidade:

  • Defina o moniker da estrutura de destino (espaço reservado{TARGET FRAMEWORK}):

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    Há suporte para .NET 7 (net7.0) ou posterior.

  • Habilite a propriedade AllowUnsafeBlocks, que permite que o gerador de código no compilador Roslyn use ponteiros para JS interoperabilidade:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Aviso

    A API de interoperabilidade JS requer a habilitação de AllowUnsafeBlocks. Tenha cuidado ao implementar seu próprio código não seguro em aplicativos .NET, o que pode introduzir riscos de segurança e estabilidade. Para obter mais informações, consulte Código não seguro, tipos de ponteiro e ponteiros de função.

A seguir há um exemplo de arquivo de projeto (.csproj) após a configuração. O espaço reservado {TARGET FRAMEWORK} é a estrutura de destino:

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

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

</Project>
  • Defina o moniker da estrutura de destino:

    <TargetFramework>net7.0</TargetFramework>
    

    Há suporte para .NET 7 (net7.0) ou posterior.

  • Especifique browser-wasm para o identificador de runtime:

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • Especifique um tipo de saída executável:

    <OutputType>Exe</OutputType>
    
  • Habilite a propriedade AllowUnsafeBlocks, que permite que o gerador de código no compilador Roslyn use ponteiros para JS interoperabilidade:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    Aviso

    A API de interoperabilidade JS requer a habilitação de AllowUnsafeBlocks. Tenha cuidado ao implementar seu próprio código não seguro em aplicativos .NET, o que pode introduzir riscos de segurança e estabilidade. Para obter mais informações, consulte Código não seguro, tipos de ponteiro e ponteiros de função.

  • Especifique WasmMainJSPath para apontar para um arquivo no disco. Esse arquivo é publicado com o aplicativo, mas o uso do arquivo não é necessário se você estiver integrando o .NET a um aplicativo existente JS.

    No exemplo a seguir, o arquivo JS no disco é main.js, mas qualquer nome de arquivo JS é permitido:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

Exemplo de arquivo de projeto (.csproj) após a configuração:

<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>

Interoperabilidade do JavaScript em WASM

As APIs no exemplo a seguir são importadas de dotnet.js. Essas APIs permitem configurar módulos nomeados que podem ser importados para o código C# e chamar em métodos expostos pelo código .NET, incluindo Program.Main.

Importante

"Importar" e "exportar" ao longo deste artigo são definidos da perspectiva do .NET:

  • Um aplicativo importa métodos JS para que eles possam ser chamados do .NET.
  • O aplicativo exporta métodos .NET para que eles possam ser chamados do JS.

No exemplo a seguir:

  • O arquivo dotnet.js é usado para criar e iniciar o runtime do WebAssembly do .NET. dotnet.js é gerado como parte da saída de build do aplicativo.

    Importante

    Para se integrar a um aplicativo existente, copie o conteúdo da pasta de saída† de publicação para os ativos de implantação do aplicativo existente para que ele possa ser atendido com o restante do aplicativo. Para implantações de produção, publique o aplicativo com o comando dotnet publish -c Release em um shell de comando e implante o conteúdo da pasta de saída com o aplicativo.

    †A pasta de saída de publicação é o local de destino do seu perfil de publicação. O padrão para um perfil de Release no .NET 8 ou posterior é bin/Release/{TARGET FRAMEWORK}/publish, em que o espaço reservado {TARGET FRAMEWORK} é a estrutura de destino (por exemplo, net8.0).

  • dotnet.create() configura o runtime do WebAssembly do .NET.

  • setModuleImports associa um nome a um módulo de funções JS para importação para o .NET. O módulo JS contém uma função dom.setInnerText, que aceita um seletor de elemento e tempo para exibir o tempo atual do cronômetro na interface do usuário. O nome do módulo pode ser qualquer cadeia de caracteres (ele não precisa ser um nome de arquivo), mas deve corresponder ao nome usado com o JSImportAttribute (explicado posteriormente neste artigo). A função dom.setInnerText é importada para C# e chamada pelo método C# SetInnerText. O método SetInnerText é mostrado posteriormente nesta seção.

  • exports.StopwatchSample.Reset() chama o .NET (StopwatchSample.Reset) de JS. O método C# Reset reiniciará o cronômetro se ele estiver em execução ou redefini-lo se ele não estiver em execução. O método Reset é mostrado posteriormente nesta seção.

  • exports.StopwatchSample.Toggle() chama o .NET (StopwatchSample.Toggle) de JS. O método C# Toggle inicia ou interrompe o cronômetro, dependendo se ele estiver em execução ou não. O método Toggle é mostrado posteriormente nesta seção.

  • runMain() executa Program.Main.

  • setModuleImports associa um nome a um módulo de funções JS para importação para o .NET. O módulo JS contém uma função window.location.href, que retorna o endereço da página atual (URL). O nome do módulo pode ser qualquer cadeia de caracteres (ele não precisa ser um nome de arquivo), mas deve corresponder ao nome usado com o JSImportAttribute (explicado posteriormente neste artigo). A função window.location.href é importada para C# e chamada pelo método C# GetHRef. O método GetHRef é mostrado posteriormente nesta seção.

  • exports.MyClass.Greeting() chama o .NET (MyClass.Greeting) de JS. O método C# Greeting retorna uma cadeia de caracteres que inclui o resultado da chamada da função window.location.href. O método Greeting é mostrado posteriormente nesta seção.

  • dotnet.run() executa 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 uma função JS para que ela possa ser chamada de C#, use o novo JSImportAttribute em uma assinatura de método correspondente. O primeiro parâmetro para o JSImportAttribute é o nome da função JS a ser importada e o segundo parâmetro é o nome do módulo.

No exemplo a seguir, a função dom.setInnerText é chamada do módulo main.js quando o método SetInnerText é chamado:

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

No exemplo a seguir, a função window.location.href é chamada do módulo main.js quando o método GetHRef é chamado:

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

Na assinatura do método importado, você pode usar tipos .NET para parâmetros e valores retornados, que sofrem realização de marshal automaticamente pelo runtime. Use JSMarshalAsAttribute<T> para controlar como os parâmetros do método importado sofrem realização de marshal. Por exemplo, você pode optar por realizar marshaling de um long como System.Runtime.InteropServices.JavaScript.JSType.Number ou System.Runtime.InteropServices.JavaScript.JSType.BigInt. Você pode passar Action/Func<TResult> retornos de chamada como parâmetros, que sofrem realização de marshal como funções chamáveis JS. Você pode passar referências de JS e objeto gerenciado e elas sofrem realização de marshal como objetos proxy, mantendo o objeto ativo no limite até que o proxy seja coletado. Você também pode importar e exportar métodos assíncronos com um resultado Task, que sofrem realização de marshal como JS promessas. A maioria dos tipos que sofrem realização de marshal funciona em ambas as direções, como parâmetros e como valores retornados, em métodos importados e exportados.

Para obter informações e exemplos adicionais de mapeamento de tipo, consulte Interop de `[JSImport]`/`[JSExport]` do JavaScript no .NET WebAssembly.

As funções acessíveis no namespace global podem ser importadas usando o prefixo globalThis no nome da função e o atributo [JSImport] sem fornecer um nome de módulo. No exemplo a seguir, console.log é prefixado com globalThis. A função importada é chamada pelo método C# Log, que aceita uma mensagem de cadeia de caracteres C# (message) e realiza marshaling da cadeia de caracteres C# em um JSString para console.log:

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

Para exportar um método .NET para que ele possa ser chamado do JS, use o JSExportAttribute.

No exemplo a seguir, cada método é exportado para JS e pode ser chamado de funções JS:

  • O método Toggle inicia ou interrompe o cronômetro dependendo de seu estado de execução.
  • O método Reset reiniciará o cronômetro se ele estiver em execução ou redefini-lo se ele não estiver em execução.
  • O método IsRunning indica se o cronômetro está em execução.
[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;

No exemplo a seguir, o método Greeting retorna uma cadeia de caracteres que inclui o resultado da chamada do método GetHRef. Conforme mostrado anteriormente, o método C# GetHref chama JS para a função window.location.href do módulo main.js. window.location.href retorna o endereço da página atual (URL):

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

Modelos experimentais de carga de trabalho e projeto

Para demonstrar a funcionalidade de interoperabilidade JS e obter modelos JS de projeto de interoperabilidade, instale a carga de trabalho wasm-experimental:

dotnet workload install wasm-experimental

A carga de trabalho wasm-experimental contém dois modelos de projeto: wasmbrowser e wasmconsole. Esses modelos são experimentais no momento, o que significa que o fluxo de trabalho do desenvolvedor para os modelos está evoluindo. No entanto, o .NET e as APIs JS usadas nos modelos têm suporte no .NET 8 e fornecem uma base para o uso do .NET em WASM do JS.

Os modelos também podem ser instalados no pacote NuGet Microsoft.NET.Runtime.WebAssembly.Templates com o seguinte comando:

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

Aplicativo de navegador

Você pode criar um aplicativo de navegador com o modelo wasmbrowser na linha de comando, que cria um aplicativo Web que demonstra como usar o .NET e JS juntos em um navegador:

dotnet new wasmbrowser

Como alternativa no Visual Studio, você pode criar o aplicativo usando o modelo de projeto WebAssembly Browser App.

Compile o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet build

Compile e execute o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet run

Como alternativa, instale e use o dotnet serve comando:

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

No exemplo anterior, o espaço reservado {TARGET FRAMEWORK} é o moniker da estrutura de destino.

Aplicativo de console do Node.js

Você pode criar um aplicativo de console com o modelo wasmconsole, que cria um aplicativo executado em WASM como um aplicativo de console Node.js ou V8:

dotnet new wasmconsole

Como alternativa no Visual Studio, você pode criar o aplicativo usando o modelo de projeto WebAssembly Console App.

Compile o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet build

Compile e execute o aplicativo do Visual Studio ou use a CLI do .NET:

dotnet run

Como alternativa, inicie qualquer servidor de arquivos estático no diretório de saída de publicação que contenha o arquivo main.mjs:

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

No exemplo anterior, o espaço reservado {TARGET FRAMEWORK} é o moniker da estrutura de destino, e o espaço reservado {PATH} é o caminho para o arquivo main.mjs.

Recursos adicionais