Blazor 》 JS Interop。
Blazor JavaScript interoperability (JS interop)
Interaction with the Document Object Model (DOM)
只有对于不与Blazor交互的标的,才用JS来变更DOM。Blazor内部维护DOM的镜像,如果DOM在Blazor以外被修改,会导致状态不一致。
Location of JavaScipt
可以用以下方式加载JS代码:
- 通过
<head>
标签,不推荐 - 通过
<body>
标签 - 通过加载外部的JS文件
- 在Blazor启动后注入脚本
不要在Blazor组件中使用
<script>
标签
Load a script in markup
示例:
<head>
...
<script>
window.jsMethod = (methodParameter) => {
...
};
</script>
</head>
不推荐的理由:
- JS interop可能会失败,如果它来与Blazor的话
- 提高页面的交互延迟
Load a script in markup
示例:
<body>
...
<script src="_framework/blazor.{webassembly|server}.js"></script>
<script>
window.jsMethod = (methodParameter) => {
...
};
</script>
</body>
Load a script from an external JS file (.js)
示例:
<body>
...
<script src="_framework/blazor.{webassembly|server}.js"></script>
<script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>
</body>
若JS文件由组件库提供,则:
<body>
...
<script src="_framework/blazor.{webassembly|server}.js"></script>
<script src="./_content/{LIBRARY NAME}/{SCRIPT PATH AND FILENAME (.js)}"></script>
</body>
Inject a script after Blazor starts
要将autostart设置成false。然后在head标签中导入目标文件,然后:
<body>
...
<script src="_framework/blazor.{webassembly|server}.js"
autostart="false"></script>
<script>
Blazor.start().then(function () {
var customScript = document.createElement('script');
customScript.setAttribute('src', 'scripts.js');
document.head.appendChild(customScript);
});
</script>
</body>
JavaScript isolation in JavaScript modules
推荐使用JS modules:
- https://developer.mozilla.org/docs/Web/JavaScript/Guide/Modules
- https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-5.0#javascript-isolation-in-javascript-modules
- https://tc39.es/ecma262/#sec-modules
好处:
- 导入的JS不影响全局名字空间
- 不要求组件或者组件库的消费者导入相关的JS
Call JavaScript functions from .NET methods in ASP.NET Core Blazor
需要注入IJSRuntime,并调用以下方法之一:
- IJSRuntime.InvokeAsync
- JSRuntimeExtensions.InvokeAsync
- JSRuntimeExtensions.InvokeVoidAsync
下面是注意点:
- 需要调用的函数标识符用字符串表示,并且仅限于范围(即window)。若要调用
window.someScope.someFunction
,标识符为someScope.someFunction
。调用前不需要注册。 - 以
Object[]
的形式传入任意的可序列化为JSON的实参 - CancellationToken记号用来传播操作需要被取消的通知
- TimeSpan代表JS操作的时限
- 返回值TValue必须可序列化为JSON
- InvokeAsync返回的是JS的Promise,需要拆掉Promise的包装来获取其所等待的值
对于Server,预渲染的时候发生在服务端,因而无法调用JS。
文中举了一个调用https://developer.mozilla.org/docs/Web/API/TextDecoder的例子。
Invoke JavaScript functions without reading a returned value
使用InvokeVoidAsync的场景:
- .NET不需要读取JS返回的内容
- JS函数返回https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/void或者https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
下面是一个示例函数:
<script>
window.displayTickerAlert1 = (symbol, price) => {
alert(`${symbol}: $${price}!`);
};
</script>
Invoke JavaScript functions and read a returned value (InvokeAsync)
InvokeAsync则用在需要读取JS调用的值的场景。
目标函数示例:
<script>
window.displayTickerAlert2 = (symbol, price) => {
if (price < 20) {
alert(`${symbol}: $${price}!`);
return "User alerted in the browser.";
} else {
return "User NOT alerted.";
}
};
</script>
Dynamic content generation scenarios
如果需要在BuildRenderTree的时候动态生成内容,那么需要:
[Inject]
IJSRuntime JS { get; set; }
Detect when a Blazor Server app is prerendering
略。
Location of JavaScipt
似乎重复了。
JavaScript isolation in JavaScript modules
假设有这么一个JS module:
wwwroot/scripts.js
export function showPrompt(message) {
return prompt(message, 'Type anything here');
}
然后使用InvokeAsync导入:
module = await JS.InvokeAsync<IJSObjectReference>("import",
"./scripts.js");
导入的标的类型是 IJSObjectReference。可以在此标的上调用JS的方法:
return await module.InvokeAsync<string>("showPrompt", message);
用完之后需要释放:
await module.DisposeAsync();
IJSInProcessObjectReference则表示到一个JS标的的引用,其上的函数可以以同步的方式调用。
Capture references to elements
一些JS互操作场景需要到HTML元素的引用,比如一个UI库需要请求一个元素的引用用于初始化,或需要调用元素提供的命令式的API,比如click以及play。
组件中,以如下方式获取HTML元素的引用:
- 在生成HTML元素时指定
@ref
属性 - 定义一个类型为ElementReference的字段,用于匹配
@ref
的值
下面是一个例子:
<input @ref="username" ... />
@code {
private ElementReference username;
}
不要变更本由Blazor管理的内容
ElementReference会通过JS interop传给JS代码,后者接收到的是一个HTMLElement现例,可用于普通的DOM API。
下面例子中定义了一个.NET的扩展方法TriggerClickEvent
可以发送一个鼠标点击事件给元素:
window.interopFunctions = {
clickElement : function (element) {
element.click();
}
}
下面是使用示例:
@inject IJSRuntime JS
<button @ref="exampleButton">Example Button</button>
<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>
@code {
private ElementReference exampleButton;
public async Task TriggerClick()
{
await JS.InvokeVoidAsync(
"interopFunctions.clickElement", exampleButton);
}
}
也可以在TriggerClickEvent上加一个额外参数:
public static async Task TriggerClickEvent(this ElementReference elementRef,
IJSRuntime js)
{
await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}
下面的代码假设TriggerClickEvent方法存在于JsInteropClasses:
@inject IJSRuntime JS
@using JsInteropClasses
<button @ref="exampleButton">Example Button</button>
<button @onclick="TriggerClick">
Trigger click event on <code>Example Button</code>
</button>
@code {
private ElementReference exampleButton;
public async Task TriggerClick()
{
await exampleButton.TriggerClickEvent(JS);
}
}
到元素的引用在组件渲染之后才有效,如果需要对其进行操作,需要等到
OnAfterRender*
。
对于泛型,返回的是ValueTask<TResult>
public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef,
IJSRuntime js)
{
return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}
下面的代码假设GenericMethod 来自JsInteropClasses。
Reference elements across components
ElementReference不能在组件间传递,因为:
- 元素现例只有在组件渲染时才可能存在
- ElementReference是struct,无法作为组件参数
上级组件可以获取元素引用,然后:
- 允许子组件注册回调
- 在OnAfterRender对传入的元素引用执行注册的回调
例子有点复杂,略。
Harden JavaScript interop calls
主要用于Server应用。WASM应用或也可以设置一个时限。
Avoid circular object references
具有回环引用的标的无法序列化,无法用于:
- .NET方法调用
- 从C#调用JS,但是返回结果具有回环引用
JavaScript libraries that render UI
可以让UI组件返回空元素给Blazor,让Blazor以为里面内有内容,就不执行diff。
文中给了一个地图的例子。
Size limits on JavaScript interop calls
主要限制于Sever应用。
Unmarshalled JavaScript interop
使用IJSUnmarshalledObjectReference可以以非序列化的方式引用JS标的。
值得多研究研究。
Catch JavaScript exceptions
在.NET中,JS的异常以JsException形式存在。
Additional resources
代码示例:https://github.com/dotnet/AspNetCore/blob/main/src/Components/test/testassets/BasicTestApp/InteropComponent.razor。(主分支上的示例对标最新的.NET版本。若要其他版本,需要切换分支标签)
Call .NET methods from JavaScript functions in ASP.NET Core Blazor
Invoke a static .NET method
对应的方法:
- DotNet.invokeMethod,返回结果
- DotNet.invokeMethodAsync,返回JS Promise
能被调用的.NET方法必须是public,static以及标注为[JSInvokable]
,示例:
@code {
[JSInvokable]
public static Task{<T>} {.NET METHOD ID}()
{
...
}
}
可以指定别名:
[JSInvokable("DifferentMethodName")]
C#中的异步操作,可以参考https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/。
Invoke an instance .NET method
- 需要将.NET现例的引用包裹在DotNetObjectReference里面传给JS,要调用其Create方法
- 在传入的引用上调用invokeMethod或者invokeMethodAsync
- 使用完毕的时候,舍弃引用
例如:
DotNet.invokeMethodAsync('{ASSEMBLY NAME}', '{.NET METHOD ID}', {ARGUMENTS});
Component instance examples
例子略
Class instance examples
例子略
Component instance .NET method helper class
助手类可以将.NET现例的方法作为Action调用,适用于以下场景:
- 同类型的不同组件在当前页面渲染
- 对于Server应用,多用户并发使用同一组件
例子略。
Location of JavaScipt
似乎是重复内容
Avoid circular object references
似乎是重复内容
Size limits on JavaScript interop calls
似乎是重复内容
JavaScript isolation in JavaScript modules
似乎是重复内容
(本篇完)