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进行数据交换。

相关推荐
IT 行者1 小时前
Web逆向工程AI工具:JSHook MCP,80+专业工具让Claude变JS逆向大师
开发语言·javascript·ecmascript·逆向
程序员 沐阳2 小时前
JavaScript 内存与引用:深究深浅拷贝、垃圾回收与 WeakMap/WeakSet
开发语言·javascript·ecmascript
Mr_Xuhhh3 小时前
Java泛型进阶:从基础到高级特性完全指南
开发语言·windows·python
He1955013 小时前
wordpress搭建块
开发语言·wordpress·古腾堡·wordpress块
建行一世4 小时前
【Windows笔记本大模型“傻瓜式”教程】使用LLaMA-Factory工具来完成对Windows笔记本大模型Qwen2.5-3B-Instruct微调
windows·ai·语言模型·llama
老天文学家了4 小时前
蓝桥杯备战Python
开发语言·python
赫瑞4 小时前
数据结构中的排列组合 —— Java实现
java·开发语言·数据结构
初夏睡觉4 小时前
c++1.3(变量与常量,简单数学运算详解),草稿公放
开发语言·c++
升职佳兴5 小时前
C盘爆满自救:3步无损迁移应用数据到E盘(含回滚)
c语言·开发语言
ID_180079054735 小时前
除了 Python,还有哪些语言可以解析 JSON 数据?
开发语言·python·json