- 在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数Call JavaScript functions from .NET methods in ASP.NET Core Blazor
- IJSRuntimeIJSRuntime
- 调用 void JavaScript 函数Call a void JavaScript function
- 检测 Blazor 服务器应用进行预呈现的时间Detect when a Blazor Server app is prerendering
- 捕获对元素的引用Capture references to elements
- 跨组件引用元素Reference elements across components
- 强化 JS 互操作调用Harden JS interop calls
- 在类库中共享互操作代码Share interop code in a class library
- 其他资源Additional resources
在 ASP.NET Core Blazor 中从 .NET 方法调用 JavaScript 函数Call JavaScript functions from .NET methods in ASP.NET Core Blazor
本文内容
作者:Javier Calvarro Nelson、Daniel Roth 和 Luke Latham
重要
Blazor WebAssembly 为预览版状态
ASP.NET Core 3.0 支持 Blazor Server。Blazor WebAssembly 在 ASP.NET Core 3.1 中为预览版。
Blazor 应用可从 .NET 方法调用 JavaScript 函数,也可从 JavaScript 函数调用 .NET 方法。这被称为 JavaScript 互操作(JS 互操作) 。
本文介绍如何从 .NET 调用 JavaScript 函数。有关如何从 JavaScript 调用 .NET 方法的信息,请参阅 从 ASP.NET Core Blazor 中的 JavaScript 函数调用 .NET 方法。
若要从 .NET 调入 JavaScript,请使用 IJSRuntime
抽象。若要发出 JS 互操作调用,请在组件中注入 IJSRuntime
抽象。InvokeAsync<T>
方法采用要与任意数量的 JSON 可序列化参数一起调用的 JavaScript 函数的标识符。函数标识符相对于全局范围 (window
)。如果要调用 window.someScope.someFunction
,则标识符是 someScope.someFunction
。无需在调用函数之前进行注册。返回类型 T
也必须可进行 JSON 序列化。T
应该与最能映射到所返回 JSON 类型的 .NET 类型匹配。
对于启用了预呈现的 Blazor 服务器应用,初始预呈现期间无法调入 JavaScript。在建立与浏览器的连接之后,必须延迟 JavaScript 互操作调用。有关详细信息,请参阅检测 Blazor 服务器应用进行预呈现的时间部分。
下面的示例基于 TextDecoder(一种基于 JavaScript 的解码器)。该示例演示如何从 C# 方法调用 JavaScript 函数。JavaScript 函数从 C# 方法接受字节数组,对数组进行解码,并将文本返回给组件进行显示。
在 wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor 服务器)的 <head>
元素中,提供了一个 JavaScript 函数,该函数使用 TextDecoder
对传递的数组进行解码并返回解码后的值:
<script>
window.convertArray = (win1251Array) => {
var win1251decoder = new TextDecoder('windows-1251');
var bytes = new Uint8Array(win1251Array);
var decodedArray = win1251decoder.decode(bytes);
console.log(decodedArray);
return decodedArray;
};
</script>
JavaScript 代码(如前面示例中所示的代码)也可以通过对脚本文件的引用,从 JavaScript 文件 (.js ) 加载:
<script src="exampleJsInterop.js"></script>
以下组件:
- 在选择了组件按钮(“转换数组” )时使用
JSRuntime
调用convertArray
JavaScript 函数。 - 调用 JavaScript 函数之后,传递的数组会转换为字符串。该字符串会返回给组件进行显示。
@page "/call-js-example"
@inject IJSRuntime JSRuntime;
<h1>Call JavaScript Function Example</h1>
<button type="button" class="btn btn-primary" @onclick="ConvertArray">
Convert Array
</button>
<p class="mt-2" style="font-size:1.6em">
<span class="badge badge-success">
@_convertedText
</span>
</p>
@code {
// Quote (c)2005 Universal Pictures: Serenity
// https://www.uphe.com/movies/serenity
// David Krumholtz on IMDB: https://www.imdb.com/name/nm0472710/
private MarkupString _convertedText =
new MarkupString("Select the <b>Convert Array</b> button.");
private uint[] _quoteArray = new uint[]
{
60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
105, 118, 101, 114, 115, 101, 10, 10,
};
private async Task ConvertArray()
{
var text =
await JSRuntime.InvokeAsync<string>("convertArray", _quoteArray);
_convertedText = new MarkupString(text);
StateHasChanged();
}
}
IJSRuntimeIJSRuntime
若要使用 IJSRuntime
抽象,请采用以下任何方法:
- 将
IJSRuntime
抽象注入 Razor 组件 (.razor ) 中:
@inject IJSRuntime JSRuntime
@code {
protected override void OnInitialized()
{
StocksService.OnStockTickerUpdated += stockUpdate =>
{
JSRuntime.InvokeVoidAsync("handleTickerChanged",
stockUpdate.symbol, stockUpdate.price);
};
}
}
在 wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor 服务器)的 <head>
元素中,提供了一个 handleTickerChanged
JavaScript 函数。该函数通过 IJSRuntime.InvokeVoidAsync
进行调用,不返回值:
<script>
window.handleTickerChanged = (symbol, price) => {
// ... client-side processing/display code ...
};
</script>
- 将
IJSRuntime
抽象注入一个类 (.cs ):
public class JsInteropClasses
{
private readonly IJSRuntime _jsRuntime;
public JsInteropClasses(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public ValueTask<string> TickerChanged(string data)
{
return _jsRuntime.InvokeAsync<string>(
"handleTickerChanged",
stockUpdate.symbol,
stockUpdate.price);
}
}
在 wwwroot/index.html (Blazor WebAssembly) 或 Pages/_Host.cshtml (Blazor 服务器)的 <head>
元素中,提供了一个 handleTickerChanged
JavaScript 函数。该函数通过 JSRuntime.InvokeAsync
进行调用,会返回值:
<script>
window.handleTickerChanged = (symbol, price) => {
// ... client-side processing/display code ...
return 'Done!';
};
</script>
- 对于使用 BuildRenderTree 的动态内容生成,请使用
[Inject]
属性:
[Inject]
IJSRuntime JSRuntime { get; set; }
在本主题附带的客户端示例应用中,向应用提供了两个 JavaScript 函数,可与 DOM 交互以接收用户输入并显示欢迎消息:
showPrompt
– 生成一个提示,以接受用户输入(用户名)并将名称返回给调用方。displayWelcome
– 将来自调用方的欢迎消息分配给id
为welcome
的 DOM 对象。
wwwroot/exampleJsInterop.js :
window.exampleJsFunctions = {
showPrompt: function (text) {
return prompt(text, 'Type your name here');
},
displayWelcome: function (welcomeMessage) {
document.getElementById('welcome').innerText = welcomeMessage;
},
returnArrayAsyncJs: function () {
DotNet.invokeMethodAsync('BlazorSample', 'ReturnArrayAsync')
.then(data => {
data.push(4);
console.log(data);
});
},
sayHello: function (dotnetHelper) {
return dotnetHelper.invokeMethodAsync('SayHello')
.then(r => console.log(r));
}
};
将引用 JavaScript 文件的 <script>
标记置于 wwwroot/index.html 文件 (Blazor WebAssembly) 或 Pages/_Host.cshtml 文件(Blazor 服务器)中。
wwwroot/index.html (Blazor WebAssembly):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Blazor WebAssembly Sample</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="exampleJsInterop.js"></script>
</body>
</html>
Pages/_Host.cshtml (Blazor 服务器):
@page "/"
@namespace BlazorSample.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Blazor Server Sample</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
<script src="exampleJsInterop.js"></script>
</body>
</html>
请勿将 <script>
标记置于组件文件中,因为 <script>
标记无法动态更新。
.NET 方法通过调用 IJSRuntime.InvokeAsync<T>
与 exampleJsInterop.js 文件中的 JavaScript 函数进行互操作。
IJSRuntime
抽象是异步的,以便可以实现 Blazor 服务器方案。如果应用是 Blazor WebAssembly 应用,并且要同步调用 JavaScript 函数,则向下转换为 IJSInProcessRuntime
并改为调用 Invoke<T>
。建议大多数 JS 互操作库使用异步 API,以确保库在所有方案中都可用。
该示例应用包含一个用于演示 JS 互操作的组件。该组件:
- 通过 JavaScript 提示接收用户输入。
- 将文本返回给组件进行处理。
- 调用第二个 JavaScript 函数,该函数与 DOM 交互以显示欢迎消息。
Pages/JSInterop.razor :
@page "/JSInterop"
@using BlazorSample.JsInteropClasses
@inject IJSRuntime JSRuntime
<h1>JavaScript Interop</h1>
<h2>Invoke JavaScript functions from .NET methods</h2>
<button type="button" class="btn btn-primary" @onclick="TriggerJsPrompt">
Trigger JavaScript Prompt
</button>
<h3 id="welcome" style="color:green;font-style:italic"></h3>
@code {
public async Task TriggerJsPrompt()
{
var name = await JSRuntime.InvokeAsync<string>(
"exampleJsFunctions.showPrompt",
"What's your name?");
await JSRuntime.InvokeVoidAsync(
"exampleJsFunctions.displayWelcome",
$"Hello {name}! Welcome to Blazor!");
}
}
- 通过选择组件的“触发 JavaScript 提示符” 按钮来执行
TriggerJsPrompt
时,则会调用在 wwwroot/exampleJsInterop.js 文件中提供的 JavaScriptshowPrompt
函数。 showPrompt
函数接受进行 HTML 编码并返回给组件的用户输入(用户的名称)。组件将用户的名称存储在本地变量name
中。- 存储在
name
中的字符串会合并为欢迎消息,而该消息会传递给 JavaScript 函数displayWelcome
(它将欢迎消息呈现到标题标记中)。
调用 void JavaScript 函数Call a void JavaScript function
返回 void(0)/void 0 或 undefined 的 JavaScript 函数使用 IJSRuntime.InvokeVoidAsync
进行调用。
检测 Blazor 服务器应用进行预呈现的时间Detect when a Blazor Server app is prerendering
在 Blazor 服务器应用进行预呈现时,由于尚未建立与浏览器的连接,无法执行调用 JavaScript 等特定操作。预呈现时,组件可能需要进行不同的呈现。
要将 JavaScript 互操作调用延迟到与浏览器建立连接之后,可使用 OnAfterRenderAsync 组件生命周期事件。仅在完成呈现应用并与客户端建立连接后,才会调用此事件。
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
<div @ref="divElement">Text during render</div>
@code {
private ElementReference divElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync(
"setElementText", divElement, "Text after render");
}
}
}
对于上述示例代码,请在 wwwroot/index.html<head>
( WebAssembly) 或 Pages/Host.cshtmlBlazor (_ 服务器)的 setElementText
元素中,提供了一个 Blazor JavaScript 函数。该函数通过 IJSRuntime.InvokeVoidAsync
进行调用,不返回值:
<script>
window.setElementText = (element, text) => element.innerText = text;
</script>
警告
上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。大多数情况下,不建议使用 JavaScript 直接修改 DOM,因为 JavaScript 可能会干扰 Blazor 的更改跟踪。
以下组件展示了如何以一种与预呈现兼容的方式将 JavaScript 互操作用作组件初始化逻辑的一部分。该组件显示可从 OnAfterRenderAsync
内部触发呈现更新。开发人员必须避免在此场景中创建无限循环。
如果调用 JSRuntime.InvokeAsync
,则 ElementRef
仅在 OnAfterRenderAsync
中使用,而不在任何更早的生命周期方法中使用,因为呈现组件后才会有 JavaScript 元素。
会调用 StateHasChanged,使用从 JavaScript 互操作调用中获取的新状态重新呈现该组件。此代码不会创建无限循环,因为仅在 infoFromJs
为 null
时才调用 StateHasChanged
。
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
<p>
Get value via JS interop call:
<strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>
Set value via JS interop call:
<div id="val-set-by-interop" @ref="divElement"></div>
@code {
private string infoFromJs;
private ElementReference divElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && infoFromJs == null)
{
infoFromJs = await JSRuntime.InvokeAsync<string>(
"setElementText", divElement, "Hello from interop call!");
StateHasChanged();
}
}
}
对于上述示例代码,请在 wwwroot/index.html<head>
( WebAssembly) 或 Pages/Host.cshtmlBlazor (_ 服务器)的 setElementText
元素中,提供了一个 Blazor JavaScript 函数。该函数通过 IJSRuntime.InvokeAsync
进行调用,会返回值:
<script>
window.setElementText = (element, text) => {
element.innerText = text;
return text;
};
</script>
警告
上述示例直接修改文档对象模型 (DOM),以便仅供演示所用。大多数情况下,不建议使用 JavaScript 直接修改 DOM,因为 JavaScript 可能会干扰 Blazor 的更改跟踪。
捕获对元素的引用Capture references to elements
某些 JS 互操作方案需要引用 HTML 元素。例如,一个 UI 库可能需要用于初始化的元素引用,或者你可能需要对元素调用类似于命令的 API(如 focus
或 play
)。
使用以下方法在组件中捕获对 HTML 元素的引用:
- 向 HTML 元素添加
@ref
属性。 - 定义一个类型为
ElementReference
字段,其名称与@ref
属性的值匹配。
以下示例演示如何捕获对 username
<input>
元素的引用:
<input @ref="username" ... />
@code {
ElementReference username;
}
警告
只使用元素引用改变不与 Blazor 交互的空元素的内容。当第三方 API 向元素提供内容时,此方案十分有用。由于 Blazor 不与元素交互,因此在 Blazor 的元素表示形式与 DOM 之间不可能存在冲突。
在下面的示例中,改变无序列表 (ul
) 的内容具有危险性 ,因为 Blazor 会与 DOM 交互以填充此元素的列表项 (<li>
):
<ul ref="MyList">
@foreach (var item in Todos)
{
<li>@item.Text</li>
}
</ul>
如果 JS 互操作改变元素 MyList
的内容,并且 Blazor 尝试将差异应用于元素,则差异与 DOM 不匹配。
就 .NET 代码而言,ElementReference
是不透明的句柄。可以对 ElementReference
执行的唯一 操作是通过 JS 互操作将它传递给 JavaScript 代码。执行此操作时,JavaScript 端代码会收到一个 HTMLElement
实例,该实例可以与常规 DOM API 一起使用。
例如,以下代码定义一个 .NET 扩展方法,通过该方法可在元素上设置焦点:
exampleJsInterop.js :
window.exampleJsFunctions = {
focusElement : function (element) {
element.focus();
}
}
若要调用不返回值的 JavaScript 函数,请使用 IJSRuntime.InvokeVoidAsync
。下面的代码通过使用捕获的 ElementReference
调用前面的 JavaScript 函数,在用户名输入上设置焦点:
@inject IJSRuntime JSRuntime
<input @ref="_username" />
<button @onclick="SetFocus">Set focus on username</button>
@code {
private ElementReference _username;
public async Task SetFocus()
{
await JSRuntime.InvokeVoidAsync(
"exampleJsFunctions.focusElement", _username);
}
}
若要使用扩展方法,请创建接收 IJSRuntime
实例的静态扩展方法:
public static async Task Focus(this ElementReference elementRef, IJSRuntime jsRuntime)
{
await jsRuntime.InvokeVoidAsync(
"exampleJsFunctions.focusElement", elementRef);
}
Focus
方法在对象上直接调用。下面的示例假设可从 JsInteropClasses
命名空间使用 Focus
方法:
@inject IJSRuntime JSRuntime
@using JsInteropClasses
<input @ref="_username" />
<button @onclick="SetFocus">Set focus on username</button>
@code {
private ElementReference _username;
public async Task SetFocus()
{
await _username.Focus(JSRuntime);
}
}
重要
仅在呈现组件后填充 username
变量。如果将未填充的 ElementReference
传递给 JavaScript 代码,则 JavaScript 代码会收到 null
值。若要在组件完成呈现之后操作元素引用(用于对元素设置初始焦点),请使用 OnAfterRenderAsync 或 OnAfterRender 组件生命周期方法。
使用泛型类型并返回值时,请使用 ValueTask<T>:
public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef,
IJSRuntime jsRuntime)
{
return jsRuntime.InvokeAsync<T>(
"exampleJsFunctions.doSomethingGeneric", elementRef);
}
GenericMethod
在具有类型的对象上直接调用。下面的示例假设可从 JsInteropClasses
命名空间使用 GenericMethod
:
@inject IJSRuntime JSRuntime
@using JsInteropClasses
<input @ref="_username" />
<button @onclick="OnClickMethod">Do something generic</button>
<p>
_returnValue: @_returnValue
</p>
@code {
private ElementReference _username;
private string _returnValue;
private async Task OnClickMethod()
{
_returnValue = await _username.GenericMethod<string>(JSRuntime);
}
}
跨组件引用元素Reference elements across components
ElementReference
仅保证在组件的 OnAfterRender
方法中有效(并且元素引用为 struct
),因此无法在组件之间传递元素引用。
若要使父组件可以向其他组件提供元素引用,父组件可以:
- 允许子组件注册回调。
- 在
OnAfterRender
事件期间,通过传递的元素引用调用注册的回调。此方法间接地允许子组件与父级的元素引用交互。
以下 Blazor WebAssembly 示例演示了该方法。
在 wwwroot/index.html 的 <head>
中:
<style>
.red { color: red }
</style>
在 wwwroot/index.html 的 <body>
中:
<script>
function setElementClass(element, className) {
/** @type {HTMLElement} **/
var myElement = element;
myElement.classList.add(className);
}
</script>
Pages/Index.razor (父组件):
@page "/"
<h1 @ref="_title">Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
Pages/Index.razor.cs :
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
namespace BlazorSample.Pages
{
public partial class Index :
ComponentBase, IObservable<ElementReference>, IDisposable
{
private bool _disposing;
private IList<IObserver<ElementReference>> _subscriptions =
new List<IObserver<ElementReference>>();
private ElementReference _title;
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
foreach (var subscription in _subscriptions)
{
try
{
subscription.OnNext(_title);
}
catch (Exception)
{
throw;
}
}
}
public void Dispose()
{
_disposing = true;
foreach (var subscription in _subscriptions)
{
try
{
subscription.OnCompleted();
}
catch (Exception)
{
}
}
_subscriptions.Clear();
}
public IDisposable Subscribe(IObserver<ElementReference> observer)
{
if (_disposing)
{
throw new InvalidOperationException("Parent being disposed");
}
_subscriptions.Add(observer);
return new Subscription(observer, this);
}
private class Subscription : IDisposable
{
public Subscription(IObserver<ElementReference> observer, Index self)
{
Observer = observer;
Self = self;
}
public IObserver<ElementReference> Observer { get; }
public Index Self { get; }
public void Dispose()
{
Self._subscriptions.Remove(Observer);
}
}
}
}
Shared/SurveyPrompt.razor (子组件):
@inject IJSRuntime JS
<div class="alert alert-secondary mt-4" role="alert">
<span class="oi oi-pencil mr-2" aria-hidden="true"></span>
<strong>@Title</strong>
<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold"
href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
</span>
and tell us what you think.
</div>
@code {
[Parameter]
public string Title { get; set; }
}
Shared/SurveyPrompt.razor.cs :
using System;
using Microsoft.AspNetCore.Components;
namespace BlazorSample.Shared
{
public partial class SurveyPrompt :
ComponentBase, IObserver<ElementReference>, IDisposable
{
private IDisposable _subscription = null;
[Parameter]
public IObservable<ElementReference> Parent { get; set; }
protected override void OnParametersSet()
{
base.OnParametersSet();
if (_subscription != null)
{
_subscription.Dispose();
}
_subscription = Parent.Subscribe(this);
}
public void OnCompleted()
{
_subscription = null;
}
public void OnError(Exception error)
{
_subscription = null;
}
public void OnNext(ElementReference value)
{
JS.InvokeAsync<object>(
"setElementClass", new object[] { value, "red" });
}
public void Dispose()
{
_subscription?.Dispose();
}
}
}
强化 JS 互操作调用Harden JS interop calls
JS 互操作可能会由于网络错误而失败,因此应视为不可靠。默认情况下,Blazor 服务器应用会在一分钟后,使服务器上的 JS 互操作调用超时。如果应用可以容忍更严格的超时(如 10秒),请使用以下方法之一设置超时:
- 在
Startup.ConfigureServices
中全局指定超时:
services.AddServerSideBlazor(
options => options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds({SECONDS}));
- 对于组件代码中的每个调用,单个调用可以指定超时:
var result = await JSRuntime.InvokeAsync<string>("MyJSOperation",
TimeSpan.FromSeconds({SECONDS}), new[] { "Arg1" });
有关资源耗尽的详细信息,请参阅 安全 ASP.NET Core Blazor 服务器应用。
在类库中共享互操作代码Share interop code in a class library
可在类库中包含 JS 互操作代码,以便能在 NuGet 包中共享代码。
类库会处理在生成的程序集中嵌入 JavaScript 资源的操作。JavaScript 文件位于 wwwroot 文件夹中 。工具负责在生成库时嵌入资源。
按引用任何其他 NuGet 包的方式在应用的项目文件中引用生成的 NuGet 包。包还原后,应用代码可如同是 C# 一样调入 JavaScript。
有关详细信息,请参阅 ASP.NET Core Razor 组件类库。