JavaScript interop

目前,WebAssembly不支持许多功能,因此Blazor不提供对它们的直接访问。这些通常是浏览器API功能,例如:

要访问这些浏览器特性,我们需要使用JavaScript作为Blazor和浏览器之间的中介;这就是下一节要介绍的内容。

JavaScript interop caveats Javascript互操作警告

在使用JSInterop时有一些注意事项。这些内容将添加到下面的列表中,这些内容将在后面的部分中演示。

不要在服务器预呈阶段调用JSInterop。

不要过早使用ElementReference对象。

通过处置资源来避免内存泄漏。

避免调用已处置的.NET引用上的方法。

在Blazor初始化之前,不要调用.NET方法。

JavaScript boot process JavaScript引导过程

在Blazor引导过程中,浏览器将在Blazor初始化之前创建HTML文档,这意味着将立即加载从引导HTML引用的任何JavaScript,并且在这些JavaScript文件中自动执行的任何代码都将在Blazor有机会初始化之前执行。
要观察这一点,请创建一个新的Blazor服务器端应用程序:

  • 编辑 /App.razor
  • 添加以下OnInitialized方法。

    1. @code {
    2. protected override void OnInitialized()
    3. {
    4. System.Diagnostics.Debug.WriteLine("Blazor initialised: " + DateTime.Now.ToString("mm:ss.fff"));
    5. base.OnInitialized();
    6. }
    7. }
  • 在名为script的/wwwroot文件夹下创建一个文件夹

  • 在该文件夹中创建名为JavaScriptBootProcess.js的文件
  • 添加以下脚本
    1. const now = new Date();
    2. console.log('JavaScript initialised: ' + now.getMinutes() + ":" + now.getSeconds() + "." + now.getMilliseconds());
    运行应用程序并查看浏览器的控制台输出和Visual Studio的输出窗口。比较输出,我们将看到类似以下内容:
    1. JavaScript initialised: 15:20.317
    2. Blazor initialised: 15:20.466
    由于这种行为,JavaScript不可能立即调用.NET方法。在使用JavaScript互操作时,如果可能,我建议从Blazor端启动通信。

    ❓ServerPrerendering 服务器预渲染

    如果我们现在编辑相同的项目,并将呈现模式改回ServerPrerended,我们将看到类似以下内容:
    1. Blazor initialised: 42:22.559
    2. JavaScript initialised: 42:22.631
    3. Blazor initialised: 42:22.690
    用户第一次访问我们的应用程序的URL时,Blazor将在浏览器外部呈现App.razor组件,并将生成的HTML发送到浏览器。之后,初始化JavaScript,最后初始化Blazor以便客户端与之交互。
    image.png

    Calling JavaScript from .NET

    JavaScript应该添加到服务器端Blazor应用程序的/Pages/_Host.cshtml中,或者添加到Web Assembly Blazor应用程序的wwwroot/index.html中。
    然后,通过将IJSRuntime服务注入我们的组件,可以从Blazor调用我们的JavaScript。
    1. public interface IJSRuntime
    2. {
    3. ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
    4. ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args);
    5. // Via an extension class
    6. void InvokeVoid(string identifier, params object[] args);
    7. }
    identifier必须是作用域为全局window变量的JavaScript函数,但不必在标识符中包括window。因此,要调用window.alert,我们只需要指定alert作为标识符。 ```csharp @page “/“ @inject IJSRuntime JSRuntime

@code { private async Task ButtonClicked() { await JSRuntime.InvokeVoidAsync(“alert”, “Hello world”); } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/103169/1642693955625-a3d964e6-0834-4031-8560-c9a2d6c300fe.png#clientId=u2835de0d-ca16-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ufcff7223&margin=%5Bobject%20Object%5D&name=image.png&originHeight=174&originWidth=452&originalType=url&ratio=1&rotation=0&showTitle=false&size=4546&status=done&style=none&taskId=u2f70e72b-fda3-4234-a715-59d3df7e632&title=)
  2. <a name="gFFdo"></a>
  3. ### Passing parameters
  4. 上一个示例将字符串“Hello world”作为参数传递给JavaScript`alert`函数。还可以将复杂对象传递给JavaScript。参数被序列化为JSON,然后在JavaScript中反序列化,然后按值将其作为匿名对象类型传递给被调用的函数。<br />传递给JavaScript的所有参数类型必须是基本类型(string/int/etc)或者是JSON可序列化的。<br />创建一个新的Blazor应用程序,并添加以下`Person`类。
  5. ```csharp
  6. using System.Collections.Generic;
  7. namespace PassingComplexObjectsToJavaScript.Models
  8. {
  9. public class Person
  10. {
  11. public string Salutation { get; set; }
  12. public string GivenName { get; set; }
  13. public string FamilyName { get; set; }
  14. public List<KeyValuePair<string, string>> PhoneNumbers { get; set; }
  15. public Person()
  16. {
  17. PhoneNumbers = new List<KeyValuePair<string, string>>();
  18. }
  19. }
  20. }

现在,在我们的Index.razor页面中,创建一个Person实例并将其传递给console.log

  1. @page "/"
  2. @inject IJSRuntime JSRuntime
  3. <button @onclick=ButtonClicked>Perform interop</button>
  4. @code
  5. {
  6. private async Task ButtonClicked()
  7. {
  8. var person = new Models.Person
  9. {
  10. Salutation = "Mr",
  11. GivenName = "Chuck",
  12. FamilyName = "Morris"
  13. };
  14. person.PhoneNumbers.Add(new KeyValuePair<string, string>("Home", "+44 (0)121 999 9999"));
  15. person.PhoneNumbers.Add(new KeyValuePair<string, string>("Work", "+44 (0)161 999 9999"));
  16. await JSRuntime.InvokeVoidAsync("console.log", "Hello", person);
  17. }
  18. }

如果我们查看浏览器的控制台输出,则单击该按钮时应该会看到以下内容。
image.png

Accessing JavaScript return values 访问JavaScript返回值

到目前为止,我们只使用了IJSRuntime扩展方法InvokeVoidAsync。如果我们希望从JavaScript函数接收返回值,则需要使用InvokeAsync<TValue>方法。在下面的示例中,我们将调用标准JavaScript confirm函数(返回布尔值)和prompt函数(返回字符串)。

  1. @page "/"
  2. @inject IJSRuntime JSRuntime
  3. <p>
  4. Status: @Result
  5. </p>
  6. <p>
  7. <button @onclick=ShowConfirm>Confirm popup</button>
  8. </p>
  9. <p>
  10. <button @onclick=ShowPrompt>Prompt popup</button>
  11. </p>
  12. @code
  13. {
  14. private string Result;
  15. private async Task ShowConfirm()
  16. {
  17. bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Are you sure?");
  18. Result = confirmed ? "You clicked OK" : "You clicked Cancel";
  19. }
  20. private async Task ShowPrompt()
  21. {
  22. string name = await JSRuntime.InvokeAsync<string>("prompt", "What is your name?");
  23. Result = "Your name is: " + name;
  24. }
  25. }

Updating the document title

创建Blazor布局一节中,我们看到了Blazor应用程序如何存在于HTML(或cshtml)文档中,并且只能控制主应用程序元素中的内容。
image.png
非单页面应用程序的网站可以通过在<head>部分中添加<title>元素来指定要在浏览器选项卡中显示的文本,但是我们的Blazor应用程序实际上并不导航到不同的服务器页面,因此它们都具有应用程序启动时加载的相同文档标题。
现在,我们将使用一个新的<Document>组件来修复此问题,该组件将使用JavaScript互操作来设置将反映在浏览器选项卡中的document.title。我们将把它创建为Blazor服务器应用程序;它可以很容易地在可重用组件库中创建,但这将留给您作为练习。
创建一个新的Blazor服务器应用程序,然后在wwwroot文件夹中创建一个scripts文件夹,并在该文件夹中使用以下脚本创建一个名为DocumentInterop.js的脚本。

  1. var BlazorUniversity = BlazorUniversity || {};
  2. BlazorUniversity.setDocumentTitle = function (title) {
  3. document.title = title;
  4. };

这将创建一个名为BlazorUniversity的对象,其中包含一个名为setDocumentTitle的函数,该函数采用一个新标题并将其分配给document.title
接下来,编辑/Pages/_Host.cshtml文件并添加对新脚本的引用。

  1. <script src="_framework/blazor.server.js"></script>
  2. <script src="~/scripts/DocumentInterop.js"></script>

最后,我们需要Document组件本身。在/Shared文件夹中,创建一个名为Document.razor的新组件,并输入以下标记。

  1. @inject IJSRuntime JSRuntime
  2. @code {
  3. [Parameter]
  4. public string Title { get; set; }
  5. protected override async Task OnParametersSetAsync()
  6. {
  7. await JSRuntime.InvokeVoidAsync("BlazorUniversity.setDocumentTitle", Title);
  8. }
  9. }

这段代码有一个故意的错误。运行应用程序,您将在调用JSRunime.InvokeVoidAsync的行上看到NullReferenceException
原因是Blazor在将控制权移交给客户端之前在服务器上运行预呈现阶段。此预呈现的目的是从服务器返回有效呈现的HTML,以便

  • 网络爬虫,如谷歌,可以索引我们的网站。
  • 用户可以立即看到结果。

这里的问题是,当预呈现阶段运行时,没有JSRuntime可以与之交互的浏览器。可能的解决方案有

  1. 编辑/Pages/_Host.cshtml并将<component type="typeof(App)" render-mode="ServerPrerended"/>更改为<component type="typeof(App)" render-mode="Server"/>

赞成:简单的解决办法。
注意:谷歌等在访问我们网站的页面时不会看到任何内容。

  1. 不是重写OnParametersSetAsync,而是重写OnAfterRenderAsync

2是解决问题的正确方法。

  1. @inject IJSRuntime JSRuntime
  2. @code {
  3. [Parameter]
  4. public string Title { get; set; }
  5. protected override async Task OnAfterRenderAsync(bool firstRender)
  6. {
  7. if (firstRender)
  8. await JSRuntime.InvokeVoidAsync("BlazorUniversity.setDocumentTitle", Title);
  9. }
  10. }

正如JavaScript引导过程一节中所解释的,当服务器在发送网站之前预先呈现网站时,它将在没有任何JavaScript的情况下呈现App组件。只有在浏览器中呈现HTML后,才会在firstRender设置为true的情况下调用OnAfterRender*方法。

Using the new Document component

编辑/Pages文件夹中的每个页面,并添加我们的新元素<Document Title="Index"/>- 但显然要在浏览器的选项卡中显示正确的文本。

Passing HTML element references

在编写Blazor应用程序时,不鼓励操作文档对象模型(DOM),因为它可能会干扰其增量呈现树,因此对HTML的任何更改都应该在组件内的.NET代码中进行管理。
有时我们可能还是希望继续,让JavaScript与我们生成的HTML交互。实现这一点的标准JavaScript方法是给HTML元素一个id,并让JavaScript使用document.getElementById('someId')找到它。在静态生成的HTML页面中,这非常简单,但是当通过组合许多组件的输出来动态创建页面时,很难确保ID在所有组件中都是唯一的。Blazor使用@ref元素标记和ElementReference结构解决了这个问题。

@ref and ElementReference

当我们需要对HTML元素的引用时,我们应该用@ref修饰该元素(或Blazor组件)。我们通过创建类型为ElementReference的成员来标识组件中的哪个成员将持有对HTML元素的引用,并使用@ref属性在元素上标识它。

  1. @page "/"
  2. <h1 @ref=MyElementReference>Hello, world!</h1>
  3. Welcome to your new app.
  4. @code {
  5. ElementReference MyElementReference;
  6. }
  • Line 3

定义一个HTML元素,并使用@ref指定在引用该元素(MyElementReference)时将使用组件中的哪个成员。

  • Line 7

引用用@ref修饰的元素时将使用的成员。
如果我们更改新Blazor应用程序的Index.razor文件以添加对h1元素的元素引用并运行该应用程序,我们将看到类似以下生成的HTML。

  1. <h1 _bl_bc0f34fa-16bd-4687-a8eb-9e3838b5170d="">Hello, world!</h1>

添加这个特殊格式化的属性就是Blazor在不必劫持元素的id参数的情况下唯一标识元素的方式。现在我们将使用@refElementReference和JavaScript interop来解决一个常见问题。

Case: Auto focusing elements

HTML规范有一个autofocus属性,可以应用于任何可聚焦的元素;当加载页面时,浏览器将找到第一个用autofocus装饰的元素,并给予它焦点。因为Blazor应用程序并不真正导航(HTML只是被重写,浏览器URL被更改),所以当我们导航到新的URL并向用户呈现新内容时,浏览器不会扫描自动聚焦属性。这意味着在输入上设置自动聚焦属性不起作用。这是我们将使用JavaScript Interop、@refElementReference解决的问题。

Observing the autofocus problem

首先创建一个新的Blazor应用程序。
在每个页面中,将内容替换为紧挨着每个@page指令下面的相同标记。

  1. Enter your name: <input autofocus />

运行应用程序并观察<input>元素如何不能自动获得焦点,甚至在第一个页面加载时也不能。

Solving the autofocus problem
  • wwwroot文件夹中创建scripts文件夹。
  • 在该文件夹中创建一个名为AutoFocus.js的新文件,并输入以下脚本。
    1. var BlazorUniversity = BlazorUniversity || {};
    2. BlazorUniversity.setFocus = function (element) {
    3. element.focus();
    4. };
    确保在/Pages/_Host.cshtml(服务器端Blazor应用程序)或/wwwroot/index.html(WebAssembly Blazor应用程序)中添加对此脚本的引用。
    Index.razor页面中,按如下方式更改标记: ```csharp @page “/“ @inject IJSRuntime JSRuntime Enter your name

@code { ElementReference ReferenceToInputControl; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) await JSRuntime.InvokeVoidAsync(“BlazorUniversity.setFocus”, ReferenceToInputControl); } }

  1. - Line 4
  2. 使用`@ref`修饰符为输入指定一个在组件中唯一的标识。
  3. - Line 8
  4. 这是将保存元素标识的成员,该成员必须是`ElementReference`类型。
  5. - Line 12
  6. 如果这是该组件第一次呈现,元素引用将传递给我们的JavaScript,它将焦点放在元素上。<br />现在在页面之间切换应该会使第一个页面上的输入在呈现该特定页面时获得焦点。
  7. <a name="UGZDw"></a>
  8. ##### Componentising our autofocus solution 对我们的自动对焦解决方案进行组件
  9. 添加JavaScript以在每个页面上设置焦点不是很麻烦,但它是重复的。此外,根据显示的选项卡表将自动聚焦设置到选项卡控件中的第一个控件将需要更多的工作。这是我们应该以可重用的形式编写的东西。<br /> 首先,更改其中一个页面的标记,使其使用新的自动聚焦控件。
  10. ```csharp
  11. @page "/"
  12. Enter your name
  13. <input @ref=ReferenceToInputControl />
  14. <AutoFocus Control=ReferenceToInputControl/>
  15. @code {
  16. ElementReference ReferenceToInputControl;
  17. }

/Shared文件夹中,创建一个名为AutoFocus.razor的新组件,并输入以下标记。

  1. @inject IJSRuntime JSRuntime
  2. @code {
  3. [Parameter]
  4. public ElementReference Control { get; set; }
  5. protected override async Task OnAfterRenderAsync(bool firstRender)
  6. {
  7. if (firstRender)
  8. await JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus", Control);
  9. }
  10. }
  • Line 4

为名为Control的组件定义一个参数,该参数接受ElementReference以标识哪个控件应获得焦点。

  • Line 9

执行JavaScript以将焦点设置到指定的控件。
此解决方案的问题在于,组件参数值是在呈现树构建过程中传递的,而元素引用只有在呈现树构建完成并且结果在浏览器中呈现为HTML之后才有效。此解决方案导致element.focus is not a function,因为在将ElementReference的值传递给AutoFoucs组件时,ElementReference是无效的。

Note: Do not use element references too soon!

正如我们在呈现树部分中看到的,在呈现阶段,Blazor根本不更新浏览器DOM。只有在完成所有组件的渲染后,Blazor才会比较新的和以前的渲染树,然后使用尽可能少的更改更新DOM。
这意味着在构建呈现树时,浏览器DOM中可能还不存在使用@ref引用的元素-因此任何通过JavaScript与它们交互的尝试都将失败。因此,我们不应该尝试在OnAfterRenderOnAfterRenderAsync以外的任何组件生命周期方法中使用ElementReference的实例,并且因为组件的参数是在构建呈现树的过程中设置的,所以我们不能将ElementReference作为参数传递,因为它在组件的生命周期中太早。当然,从按钮单击等用户事件访问引用是可以接受的,因为页面已经生成为HTML。
事实上,ElementReference的实例甚至直到OnAfterRender*方法被调用之前才被设置。Blazor流程如下:

  1. 为页面生成虚拟呈现树。
  2. 将更改应用于浏览器的HTML DOM。
  3. 对于每个@ref修饰的元素,更新Blazor组件中的ElementReference成员。
  4. 执行OnAfterRender*生命周期方法。

我们可以通过更改标准Blazor应用程序的Index.razor组件来证明此过程,以便在组件生命周期的不同时间点将ElementReference序列化为字符串,并将序列化的文本呈现到屏幕上。将新项目中的Index.razor更改为以下标记并运行应用程序。

  1. @page "/"
  2. <h1 @ref=MyElementReference>Hello, world!</h1>
  3. <button @onclick=ButtonClicked>Show serialized reference</button>
  4. <code><pre>@Log</pre></code>
  5. Welcome to your new app.
  6. @code {
  7. string Log;
  8. ElementReference MyElementReference;
  9. protected override void OnInitialized()
  10. {
  11. Log += "OnInitialized: ";
  12. ShowSerializedReference();
  13. }
  14. protected override void OnAfterRender(bool firstRender)
  15. {
  16. Log += "OnAfterRender: ";
  17. ShowSerializedReference();
  18. }
  19. private void ButtonClicked()
  20. {
  21. Log += "Button clicked: ";
  22. ShowSerializedReference();
  23. }
  24. private void ShowSerializedReference()
  25. {
  26. Log += System.Text.Json.JsonSerializer.Serialize(MyElementReference) + "\r\n";
  27. }
  28. }
  1. 我们的组件实例已创建。执行OnInitialized(第15行)。
  2. MyElementReference的值被序列化为我们的Log字符串(第33行)。
  3. 将生成渲染树。
  4. 浏览器的DOM将更新
  5. Blazor检查用@ref修饰的元素,并更新它们标识的ElementReference
  6. 在我们的组件上执行OnAfterRender(第21行)。
  7. MyElementReference的值被序列化为我们的Log字符串,但是没有显示-我们必须调用StateHasChanged才能看到它,但是Log的值已经更新。
  8. 用户单击该按钮。
  9. MyElementReference的值被序列化为我们的日志字符串。
  10. Blazor执行StateHasChanged以响应按钮单击。
  11. 我们在屏幕上看到更新的日志

JavaScript interop - 图4

👍Completing the AutoFocus component

我们可以传递一个Func<ElementReference>,而不是传入ElementReference本身,然后我们的AutoFoucs组件可以在其OnAfterRender*生命周期方法中执行该Func-此时返回值将是有效的。
将自动聚焦控件更改为接受Func,并确保设置的值不为空。

  1. @inject IJSRuntime JSRuntime
  2. @code {
  3. [Parameter]
  4. public Func<ElementReference> GetControl { get; set; }
  5. protected override async Task OnAfterRenderAsync(bool firstRender)
  6. {
  7. if (GetControl is null)
  8. throw new ArgumentNullException(nameof(GetControl));
  9. if (firstRender)
  10. await JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus", GetControl());
  11. }
  12. }

现在可以按如下方式使用该组件:

  1. @page "/"
  2. Enter your name
  3. <input @ref=ReferenceToInputControl />
  4. <AutoFocus GetControl=@( () => ReferenceToInputControl)/>
  5. @code {
  6. ElementReference ReferenceToInputControl;
  7. }

注意:有计划让未来的Blazor自动创建ElementReference成员。

Calling .NET from JavaScript

有时,我们的.NET应用程序代码需要从JavaScript执行。Blazor使我们能够异步调用对象实例上的方法,或类上的静电方法。

Identifying invokable .NET code 标识可调用的.NET代码

Blazor不允许javascript调用.NET代码中的任何静态或实例方法。有几个条件

  1. 该方法必须用JsInvokableAttribute修饰。
  2. 该方法必须是public
  3. 该方法的参数必须是Json可序列化的。
  4. 该方法的返回类型必须是Json Serializable、voidTaskTask<T>,其中T是Json可序列化的。
  5. 如果在JsInvokable上指定identifier参数,则每个类层次结构(如果是实例方法)或每个程序集(如果是静态方法)的值必须是唯一的。

    Making .NET code invokable

    要调用.NET对象实例上的方法,我们首先需要将对该对象的引用传递给JavaScript。我们不能直接传递我们的对象,因为我们希望给JavaScript一个对我们的对象的引用,而不是其状态的Json序列化表示。我们通过创建DotNetObjectReference类的实例来实现这一点。为了演示这一点,我们将创建一个简单的Blazor应用程序,它每秒从JavaScript接收随机文本。
    首先,创建一个新的Blazor应用程序,并将Index.razor更改为以下标记。

    警告:以下代码存在内存泄漏,不应在生产中使用。这将在生存期和内存泄漏中突出显示并更正。

  1. @page "/"
  2. @inject IJSRuntime JSRuntime
  3. <h1>Text received</h1>
  4. <ul>
  5. @foreach (string text in TextHistory)
  6. {
  7. <li>@text</li>
  8. }
  9. </ul>
  10. @code
  11. {
  12. List<string> TextHistory = new List<string>();
  13. protected override void OnAfterRender(bool firstRender)
  14. {
  15. base.OnAfterRender(firstRender);
  16. if (firstRender)
  17. {
  18. // See warning about memory above in the article
  19. var dotNetReference = DotNetObjectReference.Create(this);
  20. JSRuntime.InvokeVoidAsync("BlazorUniversity.startRandomGenerator", dotNetReference);
  21. }
  22. }
  23. [JSInvokable("AddText")]
  24. public void AddTextToTextHistory(string text)
  25. {
  26. TextHistory.Add(text.ToString());
  27. while (TextHistory.Count > 10)
  28. TextHistory.RemoveAt(0);
  29. StateHasChanged();
  30. System.Diagnostics.Debug.WriteLine("DotNet: Received " + text);
  31. }
  32. }
  • Line 2

注入IJSRuntime服务。我们使用它来初始化JavaScript,传入对将接收通知的组件的引用。

  • Line 6

迭代List<string>并将其呈现为HTML<li>元素。

  • Line 22

第一次呈现组件时,我们调用一个名为BlazorUniversity.startRandomGenerator的JavaScript函数,通过调用DotNetObjectReference.Create(This)传递对当前组件的引用。

  • Line 26

我们使用JSInvokable装饰我们的回调方法。给出了特定的identifier;这是推荐的做法,否则Blazor将从方法的名称推断出名称,因此将方法重构为新名称可能会破坏任何执行它的JavaScript。

  • Line 27

我们的方法符合Blazor回调要求。它是公共的,有一个void返回类型,并且它唯一的参数是可以从Json序列化的。

  • Line 29-32

将收到的文本添加到我们的List<string>中,确保条目不超过10个,然后调用StateHasChanged,以便Blazor知道它需要重新创建其RenderTree。

  • Line 33

将.NET接收的文本输出到Visual Studio输出窗口。

Invoking .NET code from JavaScript

首先,我们需要编辑/Pages/_Host.cshtml(服务器端)或/wwwroot/index.html(WASM),并添加对即将创建的脚本的引用。

  1. <script src="/scripts/CallingDotNetFromJavaScript.js"></script>

接下来,我们将创建函数BlazorUniversity sity.startRandomGenerator,并让它每秒使用随机数回调.NET对象。

  1. var BlazorUniversity = BlazorUniversity || {};
  2. BlazorUniversity.startRandomGenerator = function(dotNetObject) {
  3. setInterval(function () {
  4. let text = Math.random() * 1000;
  5. console.log("JS: Generated " + text);
  6. dotNetObject.invokeMethodAsync('AddText', text.toString());
  7. }, 1000);
  8. };

现在运行应用程序并按F12查看浏览器工具窗口。查看控制台,我们应该会看到类似以下内容:
image.png
image.png

Lifetimes and memory leaks 生命周期和内存泄漏

如果我们运行我们在从Javascript调用.NET中创建的应用程序,并检查浏览器控制台窗口,我们将看到当我们导航到另一个页面时,JavaScript仍然在回调我们的组件。更糟糕的是,如果我们查看Visual Studio输出窗口,我们将看到我们的组件仍在被调用并输出从JavaScript传递的值,这意味着我们的组件没有被垃圾回收!
当我们创建一个DotNetObjectReference时,Blazor将生成一个唯一的ID(对于WASM为整数,对于服务器端为GUID),并在当前JSRuntime中存储对我们的对象的查找。这意味着除非我们正确处理引用,否则我们的应用程序将会泄漏内存。
DotNetObjectReference类实现IDisposable。要解决内存泄漏问题,我们需要执行以下操作:

  • 我们的组件应该保留对我们创建的DotNetObjectReference的引用。
  • 我们的组件应该实现IDisposable并处置我们的DotNetObjectReference。 ```csharp @page “/“ @inject IJSRuntime JSRuntime @implements IDisposable

Text received

    @foreach (string text in TextHistory) {
  • @text
  • }

@code { List TextHistory = new List(); DotNetObjectReference ObjectReference;

protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (firstRender) { ObjectReference = DotNetObjectReference.Create(this); await JSRuntime.InvokeVoidAsync(“BlazorUniversity.startRandomGenerator”, ObjectReference); } }

[JSInvokable(“AddText”)] public void AddTextToTextHistory(string text) { TextHistory.Add(text.ToString()); while (TextHistory.Count > 10) TextHistory.RemoveAt(0); StateHasChanged(); System.Diagnostics.Debug.WriteLine(“DotNet: Received “ + text); }

public void Dispose() { GC.SuppressFinalize(this);

  1. if (ObjectReference != null)
  2. {
  3. //Now dispose our object reference so our component can be garbage collected
  4. ObjectReference.Dispose();
  5. }

} }

  1. - Line 3
  2. 告诉编译器我们希望我们的组件实现`IDisposable`
  3. - Line 16
  4. 我们现在保留对`DotNetObjectReference`的引用。
  5. - Line 21
  6. 如果这是我们的第一次呈现,我们将创建一个`DotNetObjectReference`并将其传递给我们的JavaScript方法,以便它可以在生成新的随机数时回调我们。
  7. - Line 45
  8. 当我们的组件被释放时,我们在`DotNetObjectReference`上调用`Dispose()`。<br />如果您还记得我们的[JavaScript注意事项](#dsYBH),我们不能过早调用JavaScript,所以我们只在`OnAfterRender*`事件中使用`JSRuntime`,而且只有在**firstRender**为**true**的情况下才使用`JSRuntime`。如果组件从未呈现过(例如,如果在服务器端Blazor应用程序中预先呈现),那么我们的`DotNetObjectReference`将永远不会被创建,因此我们应该只在它不为空时处理它。
  9. > 警告: 避免调用已处置的.NET引用上的方法
  10. 如果我们现在运行我们的应用程序,我们将看到我们的组件不再从JavaScript接收随机数。但是,如果我们查看浏览器的控制台窗口,我们会发现每秒都会引发一个错误。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/103169/1642918829551-62bcc4ca-1885-4a58-b2d0-63f6ab7ed595.png#clientId=u48eb60b6-816a-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ubf95dc76&margin=%5Bobject%20Object%5D&name=image.png&originHeight=923&originWidth=820&originalType=url&ratio=1&rotation=0&showTitle=false&size=120227&status=done&style=none&taskId=u2e39cf58-833e-4737-8e07-440452199b5&title=)<br />一旦我们的`DotNetObjectReference`被释放,它就会从`JSRuntime`中删除,从而允许我们的组件被垃圾收集-因此,该引用不再有效,不应该由JavaScript使用。接下来,我们将调整我们的组件,以便它取消JavaScript `setInterval`,这样一旦我们的组件被销毁,它就不再执行。<br />首先,我们需要更新JavaScript,以便它返回执行`setInterval`时创建的句柄。然后,我们需要添加一个附加函数,该函数将接受该句柄作为参数并取消间隔。
  11. ```csharp
  12. var BlazorUniversity = BlazorUniversity || {};
  13. BlazorUniversity.startRandomGenerator = function (dotNetObject) {
  14. return setInterval(function () {
  15. let text = Math.random() * 1000;
  16. console.log("JS: Generated " + text);
  17. dotNetObject.invokeMethodAsync('AddText', text.toString());
  18. }, 1000);
  19. };
  20. BlazorUniversity.stopRandomGenerator = function (handle) {
  21. clearInterval(handle);
  22. };
  • Line 3

setInteval创建的句柄从启动随机数生成器的函数返回。

  • Line 9

该函数将接受我们创建的时间间隔的句柄,并将其传递给JavaScript clearInterval函数。
最后,我们需要组件跟踪我们创建的JavaScript间隔的句柄,并在组件被释放时调用新的stopRandomGenerator函数。

  1. @page "/"
  2. @inject IJSRuntime JSRuntime
  3. @implements IDisposable
  4. <h1>Text received</h1>
  5. <ul>
  6. @foreach (string text in TextHistory)
  7. {
  8. <li>@text</li>
  9. }
  10. </ul>
  11. @code
  12. {
  13. List<string> TextHistory = new List<string>();
  14. int GeneratorHandle = -1;
  15. DotNetObjectReference<Index> ObjectReference;
  16. protected override async Task OnAfterRenderAsync(bool firstRender)
  17. {
  18. await base.OnAfterRenderAsync(firstRender);
  19. if (firstRender)
  20. {
  21. ObjectReference = DotNetObjectReference.Create(this);
  22. GeneratorHandle = await JSRuntime.InvokeAsync<int>("BlazorUniversity.startRandomGenerator", ObjectReference);
  23. }
  24. }
  25. [JSInvokable("AddText")]
  26. public void AddTextToTextHistory(string text)
  27. {
  28. TextHistory.Add(text.ToString());
  29. while (TextHistory.Count > 10)
  30. TextHistory.RemoveAt(0);
  31. StateHasChanged();
  32. System.Diagnostics.Debug.WriteLine("DotNet: Received " + text);
  33. }
  34. public async void Dispose()
  35. {
  36. GC.SuppressFinalize(this);
  37. if (GeneratorHandle != -1)
  38. {
  39. //Cancel our callback before disposing our object reference
  40. await JSRuntime.InvokeVoidAsync("BlazorUniversity.stopRandomGenerator", GeneratorHandle);
  41. }
  42. if (ObjectReference != null)
  43. {
  44. //Now dispose our object reference so our component can be garbage collected
  45. ObjectReference.Dispose();
  46. }
  47. }
  48. }
  • Line 16

我们创建一个成员来保存对从JavaScript BlazorUniversity.startRandomGenerator函数返回的间隔的引用。

  • Line 25

我们将返回的句柄存储在新成员中。

  • Line 46

如果句柄已经设置,我们调用新的JavaScript BlazoUniversity.stopRandomGenerator函数,传递我们的间隔句柄,以便可以将其传递给clearInterval。
该间隔在我们的DotNetObjectReference被释放之前被取消,这样我们的JavaScript就不会尝试使用无效的对象引用来调用.NET对象上的方法。按照良好的做法,我们在尝试清除GeneratorHandle成员之前检查它是否已设置,以防组件在OnAfterRender*方法执行之前被释放。

Type safety

在从JavaScript调用.NET一节中,您可能已经注意到,在将随机生成的数字传递给.NET之前,JavaScript的第6行调用了toString()

  1. var BlazorUniversity = BlazorUniversity || {};
  2. BlazorUniversity.startRandomGenerator = function(dotNetObject) {
  3. setInterval(function () {
  4. let text = Math.random() * 1000;
  5. console.log("JS: Generated " + text);
  6. dotNetObject.invokeMethodAsync('AddText', text.toString());
  7. }, 1000);
  8. };

尽管对象类型在JavaScript中非常可互换,但当它们传递到我们的.NET可调用方法时,它们就不能互换了。调用.NET时,请确保为传递的变量选择正确的.NET类型。

JavaScript type .NET type
boolean System.Boolean
string System.String
number System.Float / System.Decimal
System.Int32 (etc) if no decimal value
Date System.DateTime or System.String

Enums

当一个JSInvokable.NET方法的参数是enum时,JavaScript应该传递枚举的数值。下面的示例将使用值TestEnum.SecondValue调用我们的.NET方法。

  1. public enum TestEnum
  2. {
  3. FirstValue = 100,
  4. SecondValue = 200
  5. };
  6. [JSInvokable("OurInvokableDotNetMethod")]
  7. public void OurInvokableDotNetMethod(TestEnum enumValue)
  8. {
  9. }
  1. dotNetObject.invokeMethodAsync('OurInvokableDotNetMethod', 200);

但是,如果我们用[System.Text.Json.Serialization.JsonConverter]修饰enum,我们可以让我们的JavaScript传递字符串值。

  1. [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
  2. public enum TestEnum
  3. {
  4. FirstValue = 100,
  5. SecondValue = 200
  6. };

现在,调用JavaScript可以传递枚举值的名称或其数字值。以下两个调用等效。

  1. dotNetObject.invokeMethodAsync('OurInvokableDotNetMethod', 'FirstValue');
  2. dotNetObject.invokeMethodAsync('OurInvokableDotNetMethod', 200);

Calling static .NET methods

除了调用.NET对象实例上的方法之外,Blazor还使我们能够调用静态方法。下一个示例将展示如何从JavaScript调用.NET并检索API调用可能需要的特定设置-例如,对于Google Analytics。
从服务器设置中读取JavaScript设置的好处是,可以在部署过程中根据环境(开发/QA/生产)覆盖这些值,而不必更改JavaScript文件。

警告:不要试图创建一个JavaScript可调用方法,该方法只返回配置中的任何旧值,因为这可能导致客户端能够访问安全密钥等敏感信息。

  • 创建新的Blazor服务器端应用程序
  • 打开/appsettings.json文件并添加名为”JavaScript”的部分

    1. {
    2. "Logging": {
    3. "LogLevel": {
    4. "Default": "Information",
    5. "Microsoft": "Warning",
    6. "Microsoft.Hosting.Lifetime": "Information"
    7. }
    8. },
    9. "JavaScript": {
    10. "SomeApiKey": "123456789"
    11. },
    12. "AllowedHosts": "*"
    13. }

    接下来,我们需要一个类来保存此设置,创建名为Configuration的文件夹
    在该文件夹中,创建名为JavaScriptSettings.cs的文件

    1. public class JavaScriptSettings
    2. {
    3. public string SomeApiKey { get; set; }
    4. }
  • 编辑/Startup.cs文件

  • 在该类的构造函数中,我们将使用注入的IConfiguration实例从/appsettings.json获取”Javascript”部分,并将其存储在静电引用中。

    1. public Startup(IConfiguration configuration)
    2. {
    3. Configuration = configuration;
    4. var javaScriptSettings = configuration
    5. .GetSection("JavaScript")
    6. .Get<JavaScriptSettings>();
    7. JavaScriptConfiguration.SetSettings(javaScriptSettings);
    8. }
  • JavaScriptConfiguration类还不存在,因此接下来我们将在Configuration文件夹中创建它。

    1. public static class JavaScriptConfiguration
    2. {
    3. private static JavaScriptSettings Settings;
    4. internal static void SetSettings(JavaScriptSettings settings)
    5. {
    6. Settings = settings;
    7. }
    8. public static JavaScriptSettings GetSettings() => Settings;
    9. }

    我们现在的配置文件中有了一些新的设置,这是一个在.NET中表示这些设置的类,我们正在读取这些值并将它们存储在静态引用中。接下来,我们需要从JavaScript访问它。
    编辑/Pages/_Host.cshtml文件,并在现有的<script>标记下面添加以下内容

    1. <script src="~/scripts/CallingStaticDotNetMethods.js"></script>

    接下来,在/wwwroot文件夹下创建名为scripts的文件夹
    在该文件夹中,创建一个名为CallingDotNetStaticMethods.js的新文件,并添加以下脚本

    1. setTimeout(async function () {
    2. const settings = await DotNet.invokeMethodAsync("CallingStaticDotNetMethods", "GetSettings");
    3. alert('API key: ' + settings.someApiKey);
    4. }, 1000);

    DotNet.invkeMethodAsync至少需要两个参数。可以传递两个以上的参数,第二个之后的任何参数都被视为要作为其参数传递给该方法的值。

  • 方法所在的二进制文件的全名(不包括文件扩展名

  • 要执行的方法的标识符

难题的最后一块是用[JSInvokable]属性装饰该方法,传入标识符-在本例中,标识符将是GetSettings
编辑/Configuration/JavaScriptConfiguration类,并更改GetSettings方法:

  1. [JSInvokable("GetSettings")]
  2. public static JavaScriptSettings GetSettings() => Settings;

传递给[JSInvokable]的标识符不必与方法名相同。

Qualifying methods for JavaScript invocation JavaScript调用的限定方法

要限定为可通过这种方式调用的候选.NET方法,该方法必 须满足以下条件:

  1. 拥有该方法的类必须是公共的
  2. 该方法必须是公共的
  3. 一定是静态的方法
  4. 返回类型必须为void或可序列化为JSON-或者必须为TaskTask<T>,其中T可序列化为JSON
  5. 所有参数必须可序列化为JSON
  6. 该方法必须用[JSInvokable]修饰
  7. JSInvokable属性中使用的同一标识符不能在单个程序集中多次使用。

    注意:不要立即从JavaScript调用.NET静电方法

如果您重新阅读关于JavaScript引导过程的部分,您会记得在Blazor初始化之前,JavaScript是在浏览器中初始化的。
image.png
正是由于这个原因,我们只在初始超时之后调用.NET静态方法-在本例中,我选择了一秒钟。

  1. setTimeout(async function () {
  2. const settings = await DotNet.invokeMethodAsync("CallingStaticDotNetMethods", "GetSettings");
  3. alert('API key: ' + settings.someApiKey);
  4. }, 1000);

在编写本文时,无法从JavaScript检查Blazor是否已准备好在不尝试调用但失败的情况下被调用。

  1. window.someInitialization = async function () {
  2. try {
  3. const settings = await DotNet.invokeMethodAsync("CallingStaticDotNetMethods", "GetSettings");
  4. alert('API key: ' + settings.someApiKey);
  5. }
  6. catch {
  7. // Try again
  8. this.setTimeout(someInitialization, 10);
  9. }
  10. }
  11. window.someInitialization();

Hooking into Blazor.start 连接到Blazor.start

可以通过调用Blazor.start函数在Blazor初始化时调用我们的JavaScript。
首先,编辑/Pages/_Host.cshtml并更改引用Blazor脚本的<script>标记,然后添加一个名为autostart、值为false的新属性。

  1. <script src="_framework/blazor.server.js" autostart="false"></script>

接下来,我们需要更改我们的JavaScript,使其调用Blazor.start-它将返回一个Promise<void>,一旦Blazor被初始化,我们就可以用它来执行我们自己的代码。

  1. Blazor.start({})
  2. .then(async function () {
  3. const settings = await DotNet.invokeMethodAsync("CallingStaticDotNetMethods", "GetSettings");
  4. alert('API key: ' + settings.someApiKey);
  5. });

这种方法的问题是您只能使用一次。因此,如果我们在不同的脚本上有多个入口点,那么我们必须创建自己的钩点来缓存Blazor.start的结果,并将其返回给任何调用脚本。我已经提出了一个特性请求,以提供在Blazor初始化完成时注册多个回调的能力-在这里