10. 【Blazor全栈开发实战指南】--JavaScript调用Blazor

一、DotNetObjectReference:将C#对象暴露给JavaScript

上一章我们完成了"C#调用JavaScript"这个方向,本章转向另一个方向:让JavaScript主动调用C#方法。这在集成事件驱动的JavaScript库时非常关键------当第三方库内部发生某个事件(如图表被点击、编辑器内容变化、定时任务触发),它需要一种机制来通知Blazor组件。

核心工具是DotNetObjectReference<T>,它将一个C#对象包装成一个可被JavaScript引用的句柄,JS侧通过这个句柄调用对象上标记了[JSInvokable]的公共方法。

DotNetObjectReference的典型生命周期是:在OnAfterRenderAsync中创建并传递给JS,在Dispose中释放。释放是必须的------不释放会导致.NET侧的对象被GC Roots所持有,永远无法被垃圾回收:

csharp 复制代码
// 生命周期管理的标准模式
private DotNetObjectReference<MyComponent>? dotNetRef;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // 创建对当前组件实例的引用
        dotNetRef = DotNetObjectReference.Create(this);
        // 将引用传给JS,JS持有这个句柄
        await JS.InvokeVoidAsync("initializeWithCallback", dotNetRef);
    }
}

public async ValueTask DisposeAsync()
{
    // 释放引用,允许C#对象被垃圾回收
    dotNetRef?.Dispose();
}

二、[JSInvokable]:可被JavaScript调用的C#方法

在C#类中,任何需要被JavaScript调用的公共方法 都必须标记[JSInvokable]特性。默认情况下,JavaScript端调用时使用的方法名与C#方法名相同(大小写敏感),也可以通过[JSInvokable("customName")]自定义:

csharp 复制代码
// JavaScript 调用时使用 "UpdateData"
[JSInvokable]
public void UpdateData(string newValue)
{
    currentValue = newValue;
    StateHasChanged(); // 通知Blazor重渲染
}

// JavaScript 调用时使用 "handleResult"(自定义名称)
[JSInvokable("handleResult")]
public async Task<string> ProcessResult(int code)
{
    var result = await SomeService.ProcessAsync(code);
    return result.ToString();
}

[JSInvokable]方法可以有参数(JSON自动反序列化)和返回值(JSON自动序列化),也可以是async Taskasync Task<T>------JavaScript侧调用时返回的是Promise,可以正常await

除了通过DotNetObjectReference调用实例方法,[JSInvokable]也可以标记静态方法,此时不需要传递任何对象引用,JavaScript直接通过程序集名和方法名调用:

csharp 复制代码
// 静态方法:JS调用 DotNet.invokeMethodAsync('MyApp', 'GetAppVersion')
[JSInvokable]
public static string GetAppVersion()
{
    return "1.2.3";
}

三、实战:集成Chart.js构建交互式图表

理论的最佳检验是实践。我们来完整实现一个集成了Chart.js的动态折线图组件,它支持从C#端动态更新数据,并在用户点击图表数据点时,将点击信息回调给Blazor组件:

第一步:在HTML中引入Chart.js

App.razor<head>中引入Chart.js CDN(生产环境建议下载到本地wwwroot):

html 复制代码
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script type="module" src="/js/chartInterop.js"></script>

这里注意type="module"------我们的互操作代码使用ES模块语法,必须以module方式加载。

第二步:编写JavaScript互操作模块

javascript 复制代码
// wwwroot/js/chartInterop.js

// 存储已创建的图表实例,key为canvas元素的id
const charts = new Map();

/**
 * 创建折线图
 * @param {HTMLElement} canvasElement - canvas DOM元素
 * @param {object} chartData - 初始数据 { labels: string[], datasets: object[] }
 * @param {object} dotNetRef - .NET对象引用,用于回调
 */
export function createLineChart(canvasElement, chartData, dotNetRef) {
    const ctx = canvasElement.getContext('2d');

    const chart = new Chart(ctx, {
        type: 'line',
        data: chartData,
        options: {
            responsive: true,
            animation: { duration: 500 },
            plugins: {
                legend: { position: 'top' },
            },
            onClick: async (event, elements) => {
                // 用户点击图表时,回调Blazor组件
                if (elements.length > 0) {
                    const element = elements[0];
                    const datasetIndex = element.datasetIndex;
                    const dataIndex = element.index;
                    const value = chart.data.datasets[datasetIndex].data[dataIndex];
                    const label = chart.data.labels[dataIndex];

                    // invokeMethodAsync 调用 [JSInvokable] 标记的方法
                    // 参数按JSON传递,在C#侧自动反序列化
                    await dotNetRef.invokeMethodAsync('OnChartPointClicked', {
                        datasetIndex,
                        dataIndex,
                        value,
                        label
                    });
                }
            }
        }
    });

    // 使用canvas元素的id作为key存储图表实例
    charts.set(canvasElement.id, chart);
}

/**
 * 更新图表数据(无需重建整个图表)
 * @param {string} canvasId - canvas元素的id
 * @param {number[][]} newData - 每个数据集的新数据数组
 * @param {string[]} newLabels - 新的X轴标签数组
 */
export function updateChartData(canvasId, newData, newLabels) {
    const chart = charts.get(canvasId);
    if (!chart) return;

    chart.data.labels = newLabels;
    newData.forEach((data, index) => {
        if (chart.data.datasets[index]) {
            chart.data.datasets[index].data = data;
        }
    });

    // 触发图表重绘
    chart.update();
}

/**
 * 销毁图表,释放Canvas上下文资源
 * @param {string} canvasId
 */
export function destroyChart(canvasId) {
    const chart = charts.get(canvasId);
    if (chart) {
        chart.destroy();
        charts.delete(canvasId);
    }
}

第三步:编写Blazor组件

csharp 复制代码
@* Components/Shared/LineChart.razor *@
@inject IJSRuntime JS
@implements IAsyncDisposable

<div class="chart-container" style="position: relative; height: 300px;">
    @* @ref 获取canvas元素的引用;id用于JS侧存储图表实例 *@
    <canvas id="@canvasId" @ref="canvasRef"></canvas>
</div>

@if (lastClickedPoint is not null)
{
    <p class="chart-info">
        您点击了:<strong>@lastClickedPoint.Label</strong>,值为 <strong>@lastClickedPoint.Value</strong>
    </p>
}

@code {
    // 接收图表标签和数据集作为参数
    [Parameter]
    public List<string> Labels { get; set; } = [];

    [Parameter]
    public List<ChartDataset> Datasets { get; set; } = [];

    // 当图表内数据点被点击时,通知父组件
    [Parameter]
    public EventCallback<ChartPointInfo> OnPointClicked { get; set; }

    private ElementReference canvasRef;
    // 使用固定的唯一ID,确保多个图表组件共存时不冲突
    private readonly string canvasId = $"chart-{Guid.NewGuid():N}";
    private IJSObjectReference? jsModule;
    private DotNetObjectReference<LineChart>? dotNetRef;
    private ChartPointInfo? lastClickedPoint;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // 加载ES模块
            jsModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "/js/chartInterop.js");

            // 创建当前组件的 .NET 引用,用于JS回调
            dotNetRef = DotNetObjectReference.Create(this);

            // 构建Chart.js所需的数据格式
            var chartData = new
            {
                labels = Labels,
                datasets = Datasets.Select(d => new
                {
                    label = d.Label,
                    data = d.Data,
                    borderColor = d.Color,
                    backgroundColor = d.Color + "33", // 加透明度
                    tension = 0.4  // 曲线平滑度
                })
            };

            // 调用JS模块中的 createLineChart 函数,传入DOM元素、数据和.NET引用
            await jsModule.InvokeVoidAsync("createLineChart", canvasRef, chartData, dotNetRef);
        }
    }

    // 当父组件传入的数据发生变化时,更新图表(无需重建)
    protected override async Task OnParametersSetAsync()
    {
        // jsModule为null说明还没完成首次渲染初始化,跳过
        if (jsModule is null) return;

        var newData = Datasets.Select(d => d.Data).ToList();
        await jsModule.InvokeVoidAsync("updateChartData", canvasId, newData, Labels);
    }

    // 此方法被 JavaScript 端的 onClick 事件通过 dotNetRef 调用
    // 必须是 public,才能被 DotNetObjectReference 反射发现
    [JSInvokable]
    public async Task OnChartPointClicked(ChartPointInfo pointInfo)
    {
        lastClickedPoint = pointInfo;
        // 触发父组件订阅的回调
        await OnPointClicked.InvokeAsync(pointInfo);
        // 手动触发重渲染,更新 lastClickedPoint 的显示
        StateHasChanged();
    }

    public async ValueTask DisposeAsync()
    {
        if (jsModule is not null)
        {
            // 通知JS侧销毁图表,释放Canvas资源
            await jsModule.InvokeVoidAsync("destroyChart", canvasId);
            await jsModule.DisposeAsync();
        }
        // 必须释放,否则 this(组件实例)将被JS侧持有并无法GC
        dotNetRef?.Dispose();
    }
}
csharp 复制代码
// 数据模型
public class ChartDataset
{
    public string Label { get; set; } = string.Empty;
    public List<double> Data { get; set; } = [];
    public string Color { get; set; } = "#0078d4";
}

public class ChartPointInfo
{
    public int DatasetIndex { get; set; }
    public int DataIndex { get; set; }
    public double Value { get; set; }
    public string Label { get; set; } = string.Empty;
}

第四步:在父页面中使用图表组件

csharp 复制代码
@page "/dashboard"

<h1>销售仪表盘</h1>

<LineChart Labels="@months" Datasets="@datasets" OnPointClicked="HandlePointClick" />

<button @onclick="AddRandomData">添加随机数据</button>

@if (clickInfo is not null)
{
    <p>点击详情:@clickInfo.Label 月,@clickInfo.Value 万元</p>
}

@code {
    private List<string> months = ["1月", "2月", "3月", "4月", "5月", "6月"];
    private List<ChartDataset> datasets = [
        new() { Label = "2024年", Data = [85, 92, 78, 110, 95, 130], Color = "#0078d4" },
        new() { Label = "2023年", Data = [72, 80, 65, 88, 76, 105], Color = "#ff6b35" }
    ];
    private ChartPointInfo? clickInfo;
    private readonly Random rng = new();

    private void HandlePointClick(ChartPointInfo info)
    {
        clickInfo = info;
    }

    private void AddRandomData()
    {
        months.Add($"{months.Count + 1}月");
        foreach (var ds in datasets)
        {
            ds.Data.Add(Math.Round(60 + rng.NextDouble() * 80, 1));
        }
    }
}

这个完整示例展示了双向互操作的全貌:C#通过jsModule.InvokeVoidAsync创建和更新图表(C#→JS方向),Chart.js通过dotNetRef.invokeMethodAsync回调OnChartPointClicked(JS→C#方向)。两个方向相互配合,让Razor组件与JavaScript库形成了有机的整体。

四、总结

本章完成了JavaScript互操作的反向链路:DotNetObjectReference.Create(this)将C#对象包装为JS可见的引用;[JSInvokable]标记的公共方法可被JS通过dotNetRef.invokeMethodAsync调用;通过Chart.js集成的完整实战,我们演示了从图表创建、数据动态更新到点击回调的全流程,也强调了dotNetRef.Dispose()jsModule.DisposeAsync()对于防止内存泄漏的重要性。

至此,第三章JavaScript互操作的两个方向均已讲解完毕。一个独立运行的前端离不开数据来源,而数据通常来自后端API。下一章我们进入第四章,探讨如何构建ASP.NET Core Web API服务器,以及Blazor WebAssembly如何通过HttpClient安全、高效地与API进行数据交换。

相关推荐
佩奇大王2 小时前
P1460 路径问题
java·开发语言
云浪2 小时前
5 分钟入门 fetch
前端·javascript
划水的code搬运工小李2 小时前
Origin技巧(五)连接matlab控制台
开发语言·matlab
还是奇怪2 小时前
Python第十课:异常捕获与测试入门
开发语言·python·异常捕获
晓得迷路了2 小时前
栗子前端技术周刊第 120 期 - Vite 8.0、Solid v2.0.0 Beta、TypeScript 6.0 RC...
前端·javascript·vite
自在极意功。2 小时前
ArrayList扩容机制
java·开发语言·算法·集合·arraylist
吃鱼不吐刺.2 小时前
Java线程池
java·开发语言
桌面运维家2 小时前
Windows桌面审计:高效OCR屏幕内容抓取指南
windows·ocr
一个人旅程~2 小时前
win11的ARM版本上能运行X86X64软件吗?哪些软件会存在故障?
windows·经验分享·电脑