JavaScript [JSImport]
/[JSExport]
互操作
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .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 上运行。
先决条件
在管理命令 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# 调用它,请对匹配的方法签名使用新的 JSImportAttribute。 JSImportAttribute 的第一个参数是要导入的 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.Number 或 System.Runtime.InteropServices.JavaScript.JSType.BigInt。 可以将 Action/Func<TResult> 回调作为参数传递,这些回调被封送为可调用的 JS 函数。 可以同时传递 JS 和托管对象引用,它们被封送为代理对象,使对象在边界上保持活动状态,直到代理被垃圾回收。 还可以导入和导出具有 Task 结果的异步方法,这些结果被封送为 JS 承诺。 大多数封送类型在导入和导出的方法上作为参数和返回值双向工作。
可以通过在函数名称中使用 globalThis
前缀并使用 [JSImport]
属性来导入全局命名空间上可访问的函数,而不提供模块名称。 在下面的示例中,console.log
以 globalThis
为前缀。 导入的函数由 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
工作负载包含两个项目模板:wasmbrowser
和 wasmconsole
。 这些模板目前属于实验性质,这意味着模板的开发人员工作流正在不断发展变化。 但是,模板中使用的 .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.js 或 V8 控制台应用来运行的应用:
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
文件的路径。