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:

好处:

  • 导入的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的场景:

下面是一个示例函数:

<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

似乎是重复内容

(本篇完)