news 2026/8/16 18:12:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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所持有,永远无法被垃圾回收:

// 生命周期管理的标准模式privateDotNetObjectReference<MyComponent>?dotNetRef;protectedoverrideasyncTaskOnAfterRenderAsync(boolfirstRender){if(firstRender){// 创建对当前组件实例的引用dotNetRef=DotNetObjectReference.Create(this);// 将引用传给JS,JS持有这个句柄awaitJS.InvokeVoidAsync("initializeWithCallback",dotNetRef);}}publicasyncValueTaskDisposeAsync(){// 释放引用,允许C#对象被垃圾回收dotNetRef?.Dispose();}

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

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

// JavaScript 调用时使用 "UpdateData"[JSInvokable]publicvoidUpdateData(stringnewValue){currentValue=newValue;StateHasChanged();// 通知Blazor重渲染}// JavaScript 调用时使用 "handleResult"(自定义名称)[JSInvokable("handleResult")]publicasyncTask<string>ProcessResult(intcode){varresult=awaitSomeService.ProcessAsync(code);returnresult.ToString();}

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

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

// 静态方法:JS调用 DotNet.invokeMethodAsync('MyApp', 'GetAppVersion')[JSInvokable]publicstaticstringGetAppVersion(){return"1.2.3";}

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

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

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

<scriptsrc="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script><scripttype="module"src="/js/chartInterop.js"></script>

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

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

// wwwroot/js/chartInterop.js// 存储已创建的图表实例,key为canvas元素的idconstcharts=newMap();/** * 创建折线图 * @param {HTMLElement} canvasElement - canvas DOM元素 * @param {object} chartData - 初始数据 { labels: string[], datasets: object[] } * @param {object} dotNetRef - .NET对象引用,用于回调 */exportfunctioncreateLineChart(canvasElement,chartData,dotNetRef){constctx=canvasElement.getContext('2d');constchart=newChart(ctx,{type:'line',data:chartData,options:{responsive:true,animation:{duration:500},plugins:{legend:{position:'top'},},onClick:async(event,elements)=>{// 用户点击图表时,回调Blazor组件if(elements.length>0){constelement=elements[0];constdatasetIndex=element.datasetIndex;constdataIndex=element.index;constvalue=chart.data.datasets[datasetIndex].data[dataIndex];constlabel=chart.data.labels[dataIndex];// invokeMethodAsync 调用 [JSInvokable] 标记的方法// 参数按JSON传递,在C#侧自动反序列化awaitdotNetRef.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轴标签数组 */exportfunctionupdateChartData(canvasId,newData,newLabels){constchart=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 */exportfunctiondestroyChart(canvasId){constchart=charts.get(canvasId);if(chart){chart.destroy();charts.delete(canvasId);}}

第三步:编写Blazor组件

@*Components/Shared/LineChart.razor*@ @inject IJSRuntime JS @implements IAsyncDisposable<divclass="chart-container"style="position: relative; height: 300px;">@*@ref获取canvas元素的引用;id用于JS侧存储图表实例*@<canvasid="@canvasId"@ref="canvasRef"></canvas></div>@if(lastClickedPointisnotnull){<pclass="chart-info">您点击了:<strong>@lastClickedPoint.Label</strong>,值为<strong>@lastClickedPoint.Value</strong></p>}@code{// 接收图表标签和数据集作为参数[Parameter]publicList<string>Labels{get;set;}=[];[Parameter]publicList<ChartDataset>Datasets{get;set;}=[];// 当图表内数据点被点击时,通知父组件[Parameter]publicEventCallback<ChartPointInfo>OnPointClicked{get;set;}privateElementReferencecanvasRef;// 使用固定的唯一ID,确保多个图表组件共存时不冲突privatereadonlystringcanvasId=$"chart-{Guid.NewGuid():N}";privateIJSObjectReference?jsModule;privateDotNetObjectReference<LineChart>?dotNetRef;privateChartPointInfo?lastClickedPoint;protectedoverrideasyncTaskOnAfterRenderAsync(boolfirstRender){if(firstRender){// 加载ES模块jsModule=awaitJS.InvokeAsync<IJSObjectReference>("import","/js/chartInterop.js");// 创建当前组件的 .NET 引用,用于JS回调dotNetRef=DotNetObjectReference.Create(this);// 构建Chart.js所需的数据格式varchartData=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引用awaitjsModule.InvokeVoidAsync("createLineChart",canvasRef,chartData,dotNetRef);}}// 当父组件传入的数据发生变化时,更新图表(无需重建)protectedoverrideasyncTaskOnParametersSetAsync(){// jsModule为null说明还没完成首次渲染初始化,跳过if(jsModuleisnull)return;varnewData=Datasets.Select(d=>d.Data).ToList();awaitjsModule.InvokeVoidAsync("updateChartData",canvasId,newData,Labels);}// 此方法被 JavaScript 端的 onClick 事件通过 dotNetRef 调用// 必须是 public,才能被 DotNetObjectReference 反射发现[JSInvokable]publicasyncTaskOnChartPointClicked(ChartPointInfopointInfo){lastClickedPoint=pointInfo;// 触发父组件订阅的回调awaitOnPointClicked.InvokeAsync(pointInfo);// 手动触发重渲染,更新 lastClickedPoint 的显示StateHasChanged();}publicasyncValueTaskDisposeAsync(){if(jsModuleisnotnull){// 通知JS侧销毁图表,释放Canvas资源awaitjsModule.InvokeVoidAsync("destroyChart",canvasId);awaitjsModule.DisposeAsync();}// 必须释放,否则 this(组件实例)将被JS侧持有并无法GCdotNetRef?.Dispose();}}
// 数据模型publicclassChartDataset{publicstringLabel{get;set;}=string.Empty;publicList<double>Data{get;set;}=[];publicstringColor{get;set;}="#0078d4";}publicclassChartPointInfo{publicintDatasetIndex{get;set;}publicintDataIndex{get;set;}publicdoubleValue{get;set;}publicstringLabel{get;set;}=string.Empty;}

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

@page"/dashboard"<h1>销售仪表盘</h1><LineChartLabels="@months"Datasets="@datasets"OnPointClicked="HandlePointClick"/><button@onclick="AddRandomData">添加随机数据</button>@if(clickInfoisnotnull){<p>点击详情:@clickInfo.Label 月,@clickInfo.Value 万元</p>}@code{privateList<string>months=["1月","2月","3月","4月","5月","6月"];privateList<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"}];privateChartPointInfo?clickInfo;privatereadonlyRandomrng=new();privatevoidHandlePointClick(ChartPointInfoinfo){clickInfo=info;}privatevoidAddRandomData(){months.Add($"{months.Count+1}月");foreach(vardsindatasets){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进行数据交换。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 16:08:35

Android自由窗口Freeform模式的深度解析与实战应用

1. Android自由窗口Freeform模式初探 第一次在Android设备上看到自由窗口时&#xff0c;那种感觉就像发现了新大陆。想象一下&#xff0c;你的手机屏幕上同时运行着微信、浏览器和笔记应用&#xff0c;每个应用窗口都可以自由调整大小和位置&#xff0c;就像在电脑上操作一样流…

作者头像 李华
网站建设 2026/7/14 16:08:37

湿式离合器KP点自学习:从原理到工程实践

1. 湿式离合器KP点到底是什么&#xff1f; 第一次听到"KP点"这个词时&#xff0c;我也是一头雾水。后来在变速箱厂跟产线老师傅蹲了半个月才明白&#xff0c;这其实就是离合器开始"干活"的起跑线。想象一下你骑自行车时捏刹车的手感——刚开始捏刹车把时感…

作者头像 李华
网站建设 2026/7/14 16:08:35

图解FC存储网络中的RAID与LUN分配实战

1. FC存储网络基础入门 第一次接触FC存储网络时&#xff0c;我被那一堆专业术语搞得头晕眼花。后来发现&#xff0c;这东西其实就像管理一个大仓库——磁盘是货架&#xff0c;RAID是货架的组合方式&#xff0c;LUN就是划分出来的储物间。FC&#xff08;光纤通道&#xff09;则相…

作者头像 李华
网站建设 2026/7/14 16:08:38

Windows 环境下快速搭建个人网站并实现公网访问(内网穿透实战)

1. 从零开始&#xff1a;在Windows上搭建你的第一个网站 很多朋友可能觉得&#xff0c;搭建一个网站是件很复杂、很高深的事情&#xff0c;得租服务器、买域名、配置一大堆看不懂的东西。其实&#xff0c;完全不是这样&#xff01;尤其是在Windows电脑上&#xff0c;你完全可以…

作者头像 李华
网站建设 2026/7/14 16:08:49

DS4Windows:终结PS手柄PC兼容性难题的全能映射工具

DS4Windows&#xff1a;终结PS手柄PC兼容性难题的全能映射工具 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows 问题发现&#xff1a;PS手柄的PC兼容性困境 当你兴致勃勃地将PS4/PS5手柄连…

作者头像 李华
网站建设 2026/7/14 16:08:37

AI 模型推理框架性能对比

AI模型推理框架性能对比&#xff1a;关键维度解析 随着AI技术的快速发展&#xff0c;模型推理框架的性能成为影响实际应用效率的核心因素。无论是云端部署还是边缘计算&#xff0c;选择高效的推理框架能显著降低延迟、提升吞吐量&#xff0c;并优化资源利用率。本文将从计算效…

作者头像 李华