JavaScript [JSImport]/[JSExport] 互操作

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

本文介绍如何使用 JS[JSImport]/[JSExport] 互操作从 JavaScript 运行 .NET (JS)。

有关其他指导,请参阅 .NET 运行时 (dotnet/runtime) GitHub 存储库中的配置和托管 .NET WebAssembly 应用程序指南。

现有 JS 应用可以使用扩展的客户端 WebAssembly 支持来重复使用 JS 中的 .NET 库,或生成基于 .NET 的新式应用和框架。

注意

本问重点介绍在不依赖 Blazor 的情况下如何从 JS 应用运行 .NET。 有关在 Blazor WebAssembly 应用中使用 [JSImport]/[JSExport] 互操作的指导,请参阅 JavaScript JSImport/JSExport 与 ASP.NET Core Blazor 互操作

这些方法仅在预计在 WebAssembly (WASM) 上运行时才适用。 库可以通过调用 OperatingSystem.IsBrowser 来进行运行时检查以确定应用是否在 WASM 上运行。

先决条件

.NET SDK(最新版本)

在管理命令 shell 中安装 wasm-tools 工作负载,这将引入相关的 MSBuild 目标:

dotnet workload install wasm-tools

这些工具也可通过 Visual Studio 安装程序安装在 Visual Studio 安装程序中的“ASP.NET 和 web 开发”工作负载下。 从可选组件列表中选择“.NET WebAssembly 生成工具”选项。

(可选)安装 wasm-experimental 工作负载,其中包含用于在浏览器应用(WebAssembly 浏览器应用)或在基于 Node.js 的控制台应用(WebAssembly 控制台应用)中开始使用 WebAssembly 上的 .NET 的实验性项目模板。 如果计划将 JS[JSImport]/[JSExport] 互操作集成到现有 JS 应用中,则不需要此工作负载。

dotnet workload install wasm-experimental

还可以通过以下命令从 Microsoft.NET.Runtime.WebAssembly.Templates NuGet 包安装模板:

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

有关详细信息,请参阅实验性工作负载和项目模板部分。

命名空间

本文中描述的 JS 互操作 API 由 System.Runtime.InteropServices.JavaScript 命名空间中的属性控制。

项目配置

配置项目 (.csproj) 以启用 JS 互操作:

  • 设置目标框架名字对象{TARGET FRAMEWORK} 占位符):

    <TargetFramework>{TARGET FRAMEWORK}</TargetFramework>
    

    支持 .NET 7 (net7.0) 或更高版本。

  • 启用 AllowUnsafeBlocks 属性,这允许 Roslyn 编译器中的代码生成器将指针用于 JS 互操作:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    警告

    JS 互操作 API 需要启用 AllowUnsafeBlocks。 在 .NET 应用中实现自己的不安全代码时要小心,这可能会带来安全和稳定性风险。 有关详细信息,请参阅不安全代码、指针类型和函数指针

下面是配置后的示例项目文件 (.csproj)。 {TARGET FRAMEWORK} 占位符是目标框架:

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

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

</Project>
  • 设置目标框架名字对象

    <TargetFramework>net7.0</TargetFramework>
    

    支持 .NET 7 (net7.0) 或更高版本。

  • 为运行时标识符指定 browser-wasm

    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    
  • 指定可执行输出类型:

    <OutputType>Exe</OutputType>
    
  • 启用 AllowUnsafeBlocks 属性,这允许 Roslyn 编译器中的代码生成器将指针用于 JS 互操作:

    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    

    警告

    JS 互操作 API 需要启用 AllowUnsafeBlocks。 在 .NET 应用中实现自己的不安全代码时要小心,这可能会带来安全和稳定性风险。 有关详细信息,请参阅不安全代码、指针类型和函数指针

  • 指定 WasmMainJSPath 以指向磁盘上的文件。 此文件随应用一起发布,但如果要将 .NET 集成到现有 JS 应用中,则不需要使用此文件。

    在下面的示例中,磁盘上的 JS 文件为 main.js,但任何 JS 文件名都是允许的:

    <WasmMainJSPath>main.js</WasmMainJSPath>
    

配置后的示例项目文件 (.csproj):

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

WASM 上的 JavaScript 互操作

以下示例中的 API 是从 dotnet.js 导入的。 这些 API 使你可以设置可导入到 C# 代码中的命名模块,并调用 .NET 代码公开的方法,包括 Program.Main

重要

本文中的“导入”和“导出”是从 .NET 的角度定义的:

  • 应用导入 JS 方法,以便可以从 .NET 调用它们。
  • 应用导出 .NET 方法,以便可以从 JS 调用它们。

如下示例中:

  • dotnet.js 文件用于创建和启动 .NET WebAssembly 运行时。 dotnet.js 作为应用的生成输出的一部分生成。

    重要

    要与现有应用集成,可将发布输出文件夹†的内容复制到现有应用的部署资产中,以便与应用的 rest 一起提供。 对于生产部署,可在命令 shell 中使用 dotnet publish -c Release 命令发布应用,并使用应用部署输出文件夹的内容。

    †发布输出文件夹是发布配置文件的目标位置。 在 .NET 8 或更高版本中,Release 配置文件的默认值是 bin/Release/{TARGET FRAMEWORK}/publish,其中 {TARGET FRAMEWORK} 占位符是目标框架(例如 net8.0)。

  • dotnet.create() 设置 .NET WebAssembly 运行时。

  • setModuleImports 将名称与要导入到 .NET 中的 JS 函数模块相关联。 JS 模块包含一个 dom.setInnerText 函数,该函数接受元素选择器和时间,以便在 UI 中显示当前秒表时间。 模块的名称可以是任意字符串(它不需要是文件名),但它必须与用于 JSImportAttribute(将在本文的后面进行介绍)的名称相匹配。 dom.setInnerText 函数将导入 C# 中,并由 C# 方法 SetInnerText 调用。 本部分稍后将展示 SetInnerText 方法。

  • exports.StopwatchSample.Reset() 从 JS 调用 .NET (StopwatchSample.Reset)。 Reset C# 方法会在秒表正在运行时,重启秒表,在秒表没有运行时,重置秒表。 本部分稍后将展示 Reset 方法。

  • exports.StopwatchSample.Toggle() 从 JS 调用 .NET (StopwatchSample.Toggle)。 Toggle C# 方法根据秒表当前是否正在运行来启动或停止秒表。 本部分稍后将展示 Toggle 方法。

  • runMain() 运行 Program.Main

  • setModuleImports 将名称与要导入到 .NET 中的 JS 函数模块相关联。 JS 模块包含一个 window.location.href 函数,该函数返回当前页面地址 (URL)。 模块的名称可以是任意字符串(它不需要是文件名),但它必须与用于 JSImportAttribute(将在本文的后面进行介绍)的名称相匹配。 window.location.href 函数将导入 C# 中,并由 C# 方法 GetHRef 调用。 本部分稍后将展示 GetHRef 方法。

  • exports.MyClass.Greeting() 从 JS 调用 .NET (MyClass.Greeting)。 Greeting C# 方法返回一个字符串,其中包含调用 window.location.href 函数的结果。 本部分稍后将展示 Greeting 方法。

  • dotnet.run() 运行 Program.Main

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();

若要导入 JS 函数,以便可以从 C# 调用它,请对匹配的方法签名使用新的 JSImportAttributeJSImportAttribute 的第一个参数是要导入的 JS 函数的名称,第二个参数是模块的名称。

在以下示例中,当调用 SetInnerText 方法时,会从 main.js 模块调用 dom.setInnerText 函数:

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

在以下示例中,当调用 GetHRef 方法时,会从 main.js 模块调用 window.location.href 函数:

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

在导入的方法签名中,可以将 .NET 类型用于参数和返回值,它们由运行时自动封送。 使用 JSMarshalAsAttribute<T> 控制导入的方法参数的封送方式。 例如,可以选择将 long 封送为 System.Runtime.InteropServices.JavaScript.JSType.NumberSystem.Runtime.InteropServices.JavaScript.JSType.BigInt。 可以将 Action/Func<TResult> 回调作为参数传递,这些回调被封送为可调用的 JS 函数。 可以同时传递 JS 和托管对象引用,它们被封送为代理对象,使对象在边界上保持活动状态,直到代理被垃圾回收。 还可以导入和导出具有 Task 结果的异步方法,这些结果被封送为 JS 承诺。 大多数封送类型在导入和导出的方法上作为参数和返回值双向工作。

可以通过在函数名称中使用 globalThis 前缀并使用 [JSImport] 属性来导入全局命名空间上可访问的函数,而不提供模块名称。 在下面的示例中,console.logglobalThis 为前缀。 导入的函数由 C# Log 方法调用,该方法接受 C# 字符串消息 (message) 并将 C# 字符串封送到 console.log 的 JSString

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

若要导出 .NET 方法,以便可以从 JS 调用它,请使用 JSExportAttribute

在下面的示例中,每个方法都导出到 JS 中,可以从 JS 函数调用:

  • Toggle 方法根据秒表的正在运行状态启动或停止秒表。
  • Reset 方法会在秒表正在运行时,重启秒表,在秒表没有运行时,重置秒表。
  • IsRunning 方法指示秒表是否正在运行。
[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;

在下面的示例中,Greeting 方法将返回一个字符串,其中包含调用 GetHRef 方法的结果。 如前所示,GetHref C# 方法会从 main.js 模块调用 window.location.href 函数的 JS。 window.location.href 将返回当前页面地址 (URL):

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

实验性工作负载和项目模板

若要演示 JS 互操作功能并获取 JS 互操作项目模板,请安装 wasm-experimental 工作负载:

dotnet workload install wasm-experimental

wasm-experimental 工作负载包含两个项目模板:wasmbrowserwasmconsole。 这些模板目前属于实验性质,这意味着模板的开发人员工作流正在不断发展变化。 但是,模板中使用的 .NET 和 JS API 在 .NET 8 中受支持,并且为从 JS 使用 WASM 上的 .NET 提供了基础。

还可以通过以下命令从 Microsoft.NET.Runtime.WebAssembly.Templates NuGet 包安装模板:

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

浏览器应用

可以通过命令行使用 wasmbrowser 模板创建浏览器应用,该模板将创建一个 Web 应用,用于演示在浏览器中一起使用 .NET 和 JS:

dotnet new wasmbrowser

或者,在 Visual Studio 中,可以使用 WebAssembly Browser App 项目模板创建应用。

从 Visual Studio 或通过使用 .NET CLI 生成应用:

dotnet build

从 Visual Studio 或通过使用 .NET CLI 生成并运行应用:

dotnet run

或者,安装和使用 dotnet serve 命令

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

在上面的示例中,{TARGET FRAMEWORK} 占位符是目标框架名字对象

Node.js 控制台应用

可以使用 wasmconsole 模板创建一个控制台应用,该模板将创建一个在 WASM 下作为 Node.jsV8 控制台应用来运行的应用:

dotnet new wasmconsole

或者,在 Visual Studio 中,可以使用 WebAssembly Console App 项目模板创建应用。

从 Visual Studio 或通过使用 .NET CLI 生成应用:

dotnet build

从 Visual Studio 或通过使用 .NET CLI 生成并运行应用:

dotnet run

或者,从包含 main.mjs 文件的发布输出目录中启动任何静态文件服务器:

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

在上面的示例中,{TARGET FRAMEWORK} 占位符是目标框架名字对象{PATH} 占位符是 main.mjs 文件的路径。

其他资源