第六章 JavaScript 互操(2).NET调用JS

发布于:2025-07-29 ⋅ 阅读:(12) ⋅ 点赞:(0)

IJSRuntime服务

Blazor框架中为我们注册了IJSRuntime服务,如果想要从.NET调用JS函数,需要注入IJSRuntime服务,此外JSRuntimeExtensions类中还为了IJSRuntime服务扩展了一些方法。

一、常用方法

ValueTask<TValue> InvokeAsync(string identifier, [CancellationToken cancellationToken/TimeSpan timeout,] params object?[]? args)

ValueTask InvokeVoidAsync(string identifier, [CancellationToken cancellationToken/TimeSpan timeout,] params object?[]? args)

  • identifier:要调用的JS函数名,相对于JS全局范围,基于window,因此如果要调用JS的是window.someScope.someFunction函数,则此参数为someScope.someFunction
  • params:为传入JS函数的参数
  • cancellationToken:取消JS调用的取消令牌对象
  • TimeSpan:表示JS函数调用的超时时间,如果在超时时间内未收到结果,将会超时并返回一个异常或错误信息
  • 对于具有返回值TValue的方法,返回值TValue必须可JSON序列化。
  • 在JS函数中返回JS PromiseInvokeAsync 会将 Promise 解包并返回 Promise 所等待的值。

示例-有返回结果

  • App.razor

    JS 函数从 C# 方法接受字节数组,对数组进行解码,并将文本返回给组件进行显示

    ......
    <body>
        ......
        <script src="_framework/blazor.web.js"></script>
        <script>
            window.convertArray = (win1251Array) => {
                var utf8decoder = new TextDecoder('utf-8');
                var bytes = new Uint8Array(win1251Array);
                var decodedArray = utf8decoder.decode(bytes);
                return decodedArray;
            };
        </script>
    </body>
    ......
    
  • CallJs1.razor

    @page "/call-js-1"
    @inject IJSRuntime JS
    
    <PageTitle>Call JS 1</PageTitle>
    
    <h1>Call JS Example 1</h1>
    
    <p>
        <button @onclick="SetStock">Set Stock</button>
    </p>
    
    @if (data is not null)
    {
        <p>@data</p>
    }
    
    @code {
        private string? data;
    
        private async Task SetStock()
        {
            byte[] temp = System.Text.Encoding.Default.GetBytes("测试一下");
            data = await JS.InvokeAsync<string>("convertArray", temp);
        }
    }
    

示例-无返回结果

  • App.razor

    ......
    <body>
        ......
        <script src="_framework/blazor.web.js"></script>
        <script>
            window.displayTickerAlert1 = (symbol, price) => {
                alert(`${symbol}: $${price}!`);
            };
        </script>
    </body>
    ......
    
  • CallJs2.razor

    @page "/call-js-2"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <PageTitle>Call JS 2</PageTitle>
    
    <h1>Call JS Example 2</h1>
    
    <p>
        <button @onclick="SetStock">Set Stock</button>
    </p>
    
    @if (stockSymbol is not null)
    {
        <p>@stockSymbol price: @price.ToString("c")</p>
    }
    
    @code {
        private string? stockSymbol;
        private decimal price;
    
        private async Task SetStock()
        {
            stockSymbol = $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
        }
    }
    

示例-通过C#类调用JS函数(无返回结果)

  • App.razor

    ......
    <body>
        ......
        <script src="_framework/blazor.web.js"></script>
        <script>
            window.displayTickerAlert1 = (symbol, price) => {
                alert(`${symbol}: $${price}!`);
            };
        </script>
    </body>
    ......
    
  • JsInteropClasses1.cs

    public class JsInteropClasses1(IJSRuntime js) : IDisposable
    {
        private readonly IJSRuntime js = js;
    
        public async ValueTask TickerChanged(string symbol, decimal price)
        {
            await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
        }
    
        public void Dispose()
        {
            // The following prevents derived types that introduce a
            // finalizer from needing to re-implement IDisposable.
            GC.SuppressFinalize(this);
        }
    }
    
  • CallJs3.razor

    @page "/call-js-3"
    @rendermode InteractiveServer
    @implements IDisposable
    @inject IJSRuntime JS
    
    <PageTitle>Call JS 3</PageTitle>
    
    <h1>Call JS Example 3</h1>
    
    <p>
        <button @onclick="SetStock">Set Stock</button>
    </p>
    
    @if (stockSymbol is not null)
    {
        <p>@stockSymbol price: @price.ToString("c")</p>
    }
    
    @code {
        private string? stockSymbol;
        private decimal price;
        private JsInteropClasses1? jsClass;
    
        protected override void OnInitialized()
        {
            jsClass = new(JS);
        }
    
        private async Task SetStock()
        {
            if (jsClass is not null)
            {
                stockSymbol = $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
                price = Random.Shared.Next(1, 101);
                await jsClass.TickerChanged(stockSymbol, price);
            }
        }
    
        public void Dispose() => jsClass?.Dispose();
    }
    

示例-通过C#类调用JS函数(有返回结果)

  • App.razor

    ......
    <body>
        ......
        <script src="_framework/blazor.web.js"></script>
        <script>
            window.displayTickerAlert2 = (symbol, price) => {
                if (price < 20) {
                    alert(`${symbol}: $${price}!`);
                    return "User alerted in the browser.";
                } else {
                    return "User NOT alerted.";
                }
            };
        </script>
    </body>
    ......
    
  • JsInteropClasses2.cs

    public class JsInteropClasses2(IJSRuntime js) : IDisposable
    {
        private readonly IJSRuntime js = js;
    
        public async ValueTask<string> TickerChanged(string symbol, decimal price)
        {
            return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
        }
    
        public void Dispose()
        {
            // The following prevents derived types that introduce a
            // finalizer from needing to re-implement IDisposable.
            GC.SuppressFinalize(this);
        }
    }
    
    
  • CallJs4.razor

    @page "/call-js-4"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <PageTitle>Call JS 4</PageTitle>
    
    <h1>Call JS Example 4</h1>
    
    <p>
        <button @onclick="SetStock">Set Stock</button>
    </p>
    
    @if (stockSymbol is not null)
    {
        <p>@stockSymbol price: @price.ToString("c")</p>
    }
    
    @if (result is not null)
    {
        <p>@result</p>
    }
    
    @code {
        private string? stockSymbol;
        private decimal price;
        private string? result;
    
        private async Task SetStock()
        {
            stockSymbol = $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
            result = $"Result of TickerChanged call for {stockSymbol} at " + $"{price.ToString("c")}: {interopResult}";
        }
    }
    

二、预渲染处理

注意,预渲染期间无法执行调用JS函数等特定操作

由于服务器上的预渲染过程中,不会调用OnAfterRender()生命周期函数,因此可以将调用JS函数的操作放在OnAfterRender()函数中,以拖延到组件进行交互式渲染之后再调用。

  • 需要注意的是,如果在OnAfterRender()中调用了JS函数,且由于JS函数的调用更改了组件内的某个变量,需要重新渲染组件(调用StateHasChanged()),则要注意防止无限循环。

  • App.razor

    ......
    <body>
        ......
        <script src="_framework/blazor.web.js"></script>
        <script>
    			  window.setElementText1 = (element, text) => element.innerText = text;
    		</script>
    </body>
    ......
    
  • PrerenderedInterop.razor

    @page "/prerendered-interop"
    @rendermode InteractiveServer
    @using Microsoft.AspNetCore.Components
    @using Microsoft.JSInterop
    @inject IJSRuntime JS
    
    <PageTitle>Prerendered Interop</PageTitle>
    
    <h1>Prerendered Interop Example</h1>
    
    <p>
        Get value via JS interop call:
        <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
    </p>
    
    <p>
        Set value via JS interop call:
        <strong id="val-set-by-interop" @ref="divElement"></strong>
    </p>
    
    @code {
        private string? infoFromJs;
        private ElementReference divElement;
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender && infoFromJs == null)
            {
                infoFromJs = await JS.InvokeAsync<string>("setElementText", divElement, "Hello from interop call!");
                StateHasChanged();
            }
        }
    }
    

客户端组件中的同步 JS 互操作

默认情况下,JS 互操作调用是异步的,无论执行的JS代码是同步还是异步。 这是为了确保组件在服务器端和客户端渲染模式之间都兼容。

在服务器上,所有 JS 互操作调用都必须是异步的,因为它们通过网络连接发送。而在WebAssembly渲染模式上,则允许同步的JS调用,这比进行异步调用的开销略少,并且由于等待结果时没有中间状态,可能会导致渲染周期更少。

若想要在客户端(WebAssembly渲染模式)组件中进行从.NET到JavaScript的同步调用,需要将 IJSRuntime 强制转换为 IJSInProcessRuntime 以进行 JS 互操作调用,IJSInProcessRuntime 提供了同步调用的Invoke方法。

  • 示例

    @inject IJSRuntime JS
    
    ...
    
    @code {
        protected override void HandleSomeEvent()
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
        }
    }
    

如果使用模块加载,那么可以将IJSObjectReference 时,可以改为IJSInProcessObjectReference来实现同步调用。

  • IJSInProcessObjectReference也实现了 IAsyncDisposable/IDisposable并应释放以进行垃圾回收,防止内存泄漏

  • 示例

    @inject IJSRuntime JS
    @implements IAsyncDisposable
    
    ......
    
    @code {
        ......
        private IJSInProcessObjectReference? module;
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                module = await JS.InvokeAsync<IJSInProcessObjectReference>("import",  "./scripts.js");
            }
        }
    
        ......
    
        async ValueTask IAsyncDisposable.DisposeAsync()
        {
            if (module is not null)
            {
                await module.DisposeAsync();
            }
        }
    }
    

捕获元素引用

有些 JavaScript 互操作方案需要在代码中引用 HTML 元素,这样才能对这些元素进行操作或调用它们的方法。例如,一个 UI 库可能需要获取某个特定元素的引用来初始化界面,或者需要对某个元素调用特定的方法(比如clickplay)。

  • 需要注意的是,应该尽量避免直接通过JavaScript来修改DOM,获取HTML元素引用更多的是为了方便调用常规的 DOM API,而不是修改其结构。

在组件中想要获取HTML元素的引用,可以使用属性指令@ref,并定义ElementRederence对象来接收:

<input @ref="username" ...... />

@code {
    private ElementReference username;
}

通过 JS 互操作将 ElementReference 传递给 JS 代码, JS 代码会收到一个 HTMLElement 实例,该实例可以与常规 DOM API 一起使用。

下面的示例中,通过将Button元素应用传递给JS方法,在JS方法中调用Button元素的click方法。

  • ElementReferenceEx.cs

    如果元素的API操作较多,可以统一写成扩展方法

    public static class ElementReferenceEx
    {
        public static async Task TriggerClickEvent(this ElementReference elementRef, IJSRuntime js)
        {
            await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
        }
    }
    
  • ElementReferenceTest.razor

    @page "/element-reference"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <h3>ElementReferenceTest</h3>
    
    <HeadContent>
        @*这里为了方便,直接在这样写入JS脚本*@
        <script>
            window.interopFunctions = {
                clickElement: function (element) {
                    element.click();
                }
            }
        </script>
    </HeadContent>
    
    <button @ref="exampleButton" @onclick="ExampleClick">Example Button</button>
    
    <button @onclick="TriggerClick">
        Trigger click event on <code>Example Button</code>
    </button>
    
    @if (!string.IsNullOrEmpty(clickConten))
    {
        <div>
            @clickConten
        </div>
    }
    
    @code {
        private ElementReference exampleButton;
    
        private string? clickConten;
    
        public async Task TriggerClick()
        {
            await exampleButton.TriggerClickEvent(JS);
        }
    
        private void ExampleClick()
        {
            clickConten = "按钮被点击了";
        }
    }
    

在使用元素引用时,要注意,仅在组件渲染后才填充 ElementReference 变量。 如果将未填充的 ElementReference 传递给 JS 代码,则 JS 代码会收到 null 值。 若要在组件完成渲染后立即操作元素引用,可以使用 OnAfterRenderAsyncOnAfterRender 组件生命周期方法。

跨组件引用元素

注意,不能在组件之间传递ElementReference,原因如下:

  • 仅在组件渲染完成之后(即在执行组件的 OnAfterRender/OnAfterRenderAsync 方法期间或之后),才能保证实例存在。
  • ElementReferencestruct,不能作为ElementReference直接传递。

虽然不能在组件之间传递 ElementReference ,但是如果想要让父组件中的某个元素引用提供给子组件使用,那么可以在子组件中注册回调函数,并且通过在 OnAfterRender 事件期间,通过传递的元素引用调用注册的回调。 此方法间接地允许子组件与父级的元素引用交互。

  • SurveyPrompt.razor

    <HeadContent>
        <style>
            .red { color: red }
        </style>
        <script>
            function setElementClass(element, className) {
                var myElement = element;
                myElement.classList.add(className);
            }
        </script>
    </HeadContent>
    
    <div class="alert alert-secondary mt-4">
        <span class="oi oi-pencil me-2" aria-hidden="true"></span>
        <strong>@Title</strong>
        <span class="text-nowrap">
            Please take our
            <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186158">brief survey</a>
        </span>
            and tell us what you think.
    </div>
    
    @code {
        [Parameter]
        public string? Title { get; set; }
    }
    
  • SurveyPrompt.razor.cs

    public partial class SurveyPrompt : ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable? subscription = null;
    
        [Parameter]
        public IObservable<ElementReference>? Parent { get; set; }
    
        [Inject]
        public IJSRuntime? JS { get; set; }
    
        protected override void OnParametersSet()
        {
            base.OnParametersSet();
            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", [value, "red"]));
        }
    
        public void Dispose()
        {
            subscription?.Dispose();
            // The following prevents derived types that introduce a
            // finalizer from needing to re-implement IDisposable.
            GC.SuppressFinalize(this);
        }
    }
    
    
  • CallJsTest.razor

    @page "/call-js-test"
    @rendermode InteractiveServer
    
    <PageTitle>Call JS 7</PageTitle>
    
    <h1>Call JS Example 7</h1>
    
    <h2 @ref="title">Hello, world!</h2>
    
    Welcome to your new app.
    
    <SurveyPrompt Parent="this" Title="How is Blazor working for you?" />
    
  • CallJsTest.razor.cs

    public partial class CallJsTest : ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private readonly List<IObserver<ElementReference>> subscriptions = [];
        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();
            // The following prevents derived types that introduce a
            // finalizer from needing to re-implement IDisposable.
            GC.SuppressFinalize(this);
        }
    
        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(IObserver<ElementReference> observer, CallJsTest self) : IDisposable
        {
            public IObserver<ElementReference> Observer { get; } = observer;
            public CallJsTest Self { get; } = self;
            public void Dispose() => Self.subscriptions.Remove(Observer);
        }
    }
    
    

JS互操的超时设置

在服务器交互式渲染(Blazor Server)的Blazor应用中,JS互操可能会由于网络错误而失败,因此需要有一个调用时间限制。默认情况下,Blazor中的JS互操超时时间为1分钟。

如果需要对JS互操的超时时间进行设置,可以进行全局或在调用JS函数时指定。

  • 客户端交互式渲染(Blazor WebAssembly)在条件允许的情况下也可以设置,这里就不展开了。

全局设置超时时间

如果想要设置全局的超时时间,可以通过在Program.cs文件中在添加交互式组件服务时传入选项CircuitOptions.JSInteropDefaultCallTimeout进行设置

  • Program.cs

    ......
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents(options => 
            options.JSInteropDefaultCallTimeout = {TIMEOUT});
    ......
    

调用JS函数时设置

也可以在调用JS函数时候进行超时时间的设置,优先级高于全局超时

  • 示例

    var result = await JS.InvokeAsync<string>("funcTionName", TimeSpan.FromSeconds(30), new[] { "Arg1" });
    

嵌入JavaScript组件

有时我们可能需要使用JavaScript库来在网页上生成可见的用户界面元素,例如按钮、表单、图表等。这些库可以帮助我们更轻松地操作DOM(文档对象模型),并且提供了一些方便的方法和功能来创建和管理用户界面元素。这似乎很难,因为 Blazor 的 diffing 系统依赖于对 DOM 元素树的控制,并且如果某个外部代码使 DOM 树发生变化并为了应用 diff 而使其机制失效,就会产生错误。

  • 实际上,并不止Blazor,任何基于 diff 的UI框架都会面临同样的问题

幸运的是,将外部生成的 UI 嵌入到 Razor 组件 UI 非常简单。 推荐的方法是让组件的代码(.razor 文件)生成一个空元素。 就 Blazor 的 diffing 系统而言,该元素始终为空,这样呈现器就不会递归到元素中,而是保留元素内容不变。 这样就可以安全地用外部托管的任意内容来填充元素。

在下面示例中,当 firstRender 为 true 时,使用 JS 互操作与 Blazor 外的 unmanagedElement 进行交互。 例如,调用某个外部 JS 库来填充元素。 Blazor 保留元素内容不变,直到此组件被删除。 删除组件时,会同时删除组件的整个 DOM 子树。

  • 示例

    <h1>Hello! This is a Razor component rendered at @DateTime.Now</h1>
    
    <div @ref="unmanagedElement"></div>
    
    @code {
        private ElementReference unmanagedElement;
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                ...
            }
        }
    }
    

JS生成地图

这里使用国外的JavaScript库Mapbox来进行地图的渲染,并嵌入到组件的空白元素中。

  • Mapbox库的使用是需要使用访问令牌的,可以通过登录Mapbox获取

  • wwwroot/mapComponent.js

    import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';
    
    mapboxgl.accessToken = '{ACCESS TOKEN}';
    
    export function addMapToElement(element) {
      return new mapboxgl.Map({
        container: element,
        style: 'mapbox://styles/mapbox/streets-v11',
        center: [-74.5, 40],
        zoom: 9
      });
    }
    
    export function setMapCenter(map, latitude, longitude) {
      map.setCenter([longitude, latitude]);
    }
    
  • JSMapTest.razor

    @page "/js-map-test"
    @render InteractiveServer
    @implements IAsyncDisposable
    @inject IJSRuntime JS
    
    <PageTitle>js map test</PageTitle>
    
    <HeadContent>
        <link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet" />
    </HeadContent>
    
    <h1>js map test</h1>
    
    <div @ref="mapElement" style='width:400px;height:300px'></div>
    
    <button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
    <button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>
    
    @code
    {
        private ElementReference mapElement;
        private IJSObjectReference? mapModule;
        private IJSObjectReference? mapInstance;
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                mapModule = await JS.InvokeAsync<IJSObjectReference>("import", "./mapComponent.js");
                mapInstance = await mapModule.InvokeAsync<IJSObjectReference>("addMapToElement", mapElement);
            }
        }
    
        private async Task ShowAsync(double latitude, double longitude)
        {
            if (mapModule is not null && mapInstance is not null)
            {
                await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude, longitude).AsTask();
            }
        }
    
        async ValueTask IAsyncDisposable.DisposeAsync()
        {
            if (mapInstance is not null)
            {
                await mapInstance.DisposeAsync();
            }
    
            if (mapModule is not null)
            {
                await mapModule.DisposeAsync();
            }
        }
    }
    

在这里插入图片描述

在Blazor中,具有 @ref="mapElement"<div> 元素始终保留为空。只要JS脚本不会尝试访问和改变页面的其他部分,就可以安全的填充元素并修改其内容,

需要注意的是,使用此方法时,切记遵守 Blazor 保留或销毁 DOM 元素的方式的规则。组件之所以能够安全处理按钮单击事件并更新现有的地图实例,是因为Blazor默认会尽可能的保留 DOM 元素。 如果要从 @foreach 循环内渲染地图元素的列表,则需要使用 @key 来确保保留组件实例。 否则,列表数据的更改可能导致组件实例以不合适的方式保留以前实例的状态。

传递.NET引用到JavaScript

一、传递对象

如果想要将.NET的某个对象传递到JS,需要通过静态方法DotNetObjectReference.Create<T>(T t)将指定的对象再次封装后再传递给JS方法。

DotNetObjectReference<T> DotNetObjectReference.Create<T>(T t):创建可以用于传递给JS的DotNet对象

  • 示例

    @page "/pass-obj-to-js"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <HeadContent>
        <script>
            window.getDotNetObj = (obj) => {
                obj.invokeMethodAsync("GetData").then(data => {
                    console.log(data);
                });
            }
        </script>
    </HeadContent>
    
    <h3>JSInvokeObjectMethod</h3>
    
    <button @onclick="PassObj">
        传递Net引用
    </button>
    
    @code {
        private Student student = new Student { Age = 23, Name = "Schuyler"};
    
        private async Task PassObj()
        {
            using DotNetObjectReference<Student> dotNetObj = DotNetObjectReference.Create(student);
            await JS.InvokeVoidAsync("getDotNetObj", dotNetObj);
        }
    
        public class Student
        {
            public int Age { get; set; }
            public string? Name { get; set; }
    
            [JSInvokable]
            public string GetData()
            {
                return $"Age:{Age},Name:{Name}";
            }
        }
    }
    
    

注意释放DotNetObjectReference.Create<T>(T t)所创建的对象,上述例子中使用了using进行了资源的释放,除此之外也可以通过Dispose()方法进行释放,如果没有在类或组件中释放 DotNetObjectReference 对象,则可以通过在JS中对传递的DotNetObjectReference对象调用 dispose() 从客户端对其进行释放。

二、传递流

Blazor 支持将数据直接从 .NET 流式传输到 JavaScript,通过DotNetStreamReference流对象实现。

DotNetStreamReference(Stream stream, bool leaveOpen = false)DotNetStreamReference的构造函数。

  • stream:要引用的流对象

  • leaveOpen:bool类型,指示在释放DotNetStreamReference对象时是否保持流打开。默认值为false,即在释放DotNetStreamReference对象时关闭流

  • 示例

    @page "/js-stream-test"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <h3>JsStreamTest</h3>
    
    <button @onclick="SendDatas">传输数据</button>
    
    <HeadContent>
        <script>
            @* 使用ReadableStream *@
            async function streamToJavaScriptUseStream(streamRef) {
                const stream = await streamRef.stream();
            }
            @* 使用ArrayBuffer *@
            async function streamToJavaScriptUseArryBuffer(streamRef) {
                const data = await streamRef.arrayBuffer();
            }
        </script>
    </HeadContent>
    
    @code {
    
        private async Task SendDatas()
        {
            byte[] buffer = new byte[]
            {
                0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
                0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
                0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
                0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e
            };
            MemoryStream memoryStream = new MemoryStream(buffer);
            using var streamRef = new DotNetStreamReference(stream: memoryStream, leaveOpen: false);
            await JS.InvokeVoidAsync("streamToJavaScriptUseStream", streamRef);
        }
    }
    
    

三、字节数组支持

Blazor支持一种更为优化的方式来进行JavaScript和C#之间的字节数组交互操作。在这种优化方式下,不需要将字节数组编码为Base64格式,而是直接传递字节数组本身,从而避免了编码和解码的开销。

  • 示例

    @page "/bytes-test"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <HeadContent>
        <script>
            window.receiveByteArray = (bytes) => {
              let utf8decoder = new TextDecoder();
              let str = utf8decoder.decode(bytes);
              return str;
            };
        </script>
    </HeadContent>
    
    <h1>Bytes Test</h1>
    
    <p>
        <button @onclick="SendByteArray">Send Bytes</button>
    </p>
    
    <p>
        @result
    </p>
    
    <p>
        Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
        <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
        <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
    </p>
    
    @code {
        private string? result;
    
        private async Task SendByteArray()
        {
            var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
                0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
                0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
                0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };
    
            result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
        }
    }
    

中止JavaScript函数

想要从C#代码中终止一个长时间运行的JavaScript函数,可以配合使用JavaScript中的AbortController与组件中的CancellationTokenSource

  • 示例

    @page "/abort-js-test"
    @rendermode InteractiveServer
    @inject IJSRuntime JS
    
    <HeadContent>
        <script>
            class Helpers {
                static #controller = new AbortController();
    
                static async #sleep(ms) {
                    return new Promise(resolve => setTimeout(resolve, ms));
                }
                static async longRunningFn() {
                    var i = 0;
                    while (!this.#controller.signal.aborted) {
                        i++;
                        console.log(`longRunningFn: ${i}`);
                        await this.#sleep(1000);
                    }
                }
                static stopFn() {
                    this.#controller.abort();
                    console.log('longRunningFn aborted!');
                }
            }
            window.Helpers = Helpers;
        </script>
    </HeadContent>
    
    <h1>Cancel long-running JS interop</h1>
    
    <p>
        <button @onclick="StartTask">Start Task</button>
        <button @onclick="CancelTask">Cancel Task</button>
    </p>
    
    @code {
        private CancellationTokenSource? cts;
    
        private async Task StartTask()
        {
            cts = new CancellationTokenSource();
            cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));
    
            await JS.InvokeVoidAsync("Helpers.longRunningFn");
        }
    
        private void CancelTask()
        {
            cts?.Cancel();
        }
    
        public void Dispose()
        {
            cts?.Cancel();
            cts?.Dispose();
        }
    }
    

JavaScript 互操作对象引用的释放

在进行JavaScript互操时,要注意引用对象的释放

从 .NET 调用 JS 时,要注意释放从 .NET 或从 JS 创建的 IJSObjectReference/IJSInProcessObjectReference/JSObjectReference,以避免泄漏 JS 内存。

从 JS 调用 .NET 时,要注意释放从 .NET 或从 JS 创建的 DotNetObjectReference,以避免泄漏 .NET 内存。

在Blazor中,当通过JS互操作引用创建对象时,Blazor会创建一个映射,将JS调用端的标识符作为键来控制这个映射。当从.NET或JS端释放对象时,Blazor会从这个映射中删除对应的条目。只要没有其他强引用指向这个对象,对象就可以被垃圾回收。换句话说,当对象不再被引用时,Blazor会自动将其从映射中移除,并允许垃圾回收器回收该对象的内存空间。

至少应始终释放在 .NET 端创建的对象,避免泄漏 .NET 托管内存。


网站公告

今日签到

点亮在社区的每一天
去签到