C#和JS互操作的基本语法是比较简单的,但小知识点特别多,同时,受应用加载顺序、组件生命周期以及参数类型的影响,会有比较多坑,需要耐心的学习。在C#中调用JS的场景会比较多,特别是在WASM模式下,由于WebAssembly的限制,很多时候,还是需要借助JS去控制DOM或BOM,比如WebStorage、WebGL、MediaCapture,还比如使用JS的图表库echart.js。反过来,在JS中调用C#的场景,就比较少见。所以,此章节关于"C#中调用JS” 的篇幅会多一些。
这个章节比较长,目录如下:
一、C#和JS互操作涉及到的类和接口
二、HTML/JS和Blazor/C#的加载顺序
三、在C#中调用JS
1、使用步骤
2、继续举些例子来熟悉InvokeVoidAsync和InvokeAsync<T>的使用
3、InvokeVoidAsync和InvokeAsync<T>方法的参数:引用类型、Blazor组织树节点引用、流式数据
四、在JS中调用C#
1、互操作的发起端
2、调用C#的静态方法
3、调用C#的实例方法及使用实例引用的注意事项
五、其它知识点
1、调用JS的异常处理、超时处理、中止执行
2、调用JS的模块化module方式,引用一个JS库
3、WASM模式下同步调用JS函数
4、Server模式下传输数据的大小限制
5、不要循环引用
一、C#和JS互操作涉及到的类、接口和方法
ASP.NET Core框架,在Microsoft.JSInterop命名空间下,定义了将近20个类和接口,用于C#和JS的互操作。虽然比较繁杂,但大体上可以分为:
1、C#调JS:JSRuntime / JSRuntimeExtensions / IJSRuntime,这是核心的几个类,用于在C#中异步调用JS,最常用。相对应,有一套同步操作类,仅限于WASM模式,比较少用
2、在C#中使用JS实例,JSObjectReference / JSObjectReferenceExtensions / IJSObjectReference
3、JS调C#:为C#方法进行特性标注的JSInvokableAttribute,如果是C#的静态方法,就可以直接在JS中调用
4、在JS中使用C#实例,DotNetObjectReference / DotNetObjectReference<T>,JS调用C#的实例方法时,需要将组件实例传入JS
5、其它:处理异常的JSException/JSDisconnectedException、参数中传输流式数据的DotNetStreamReference
二、JS和Blazor的加载顺序
1、HTML/JS和Blazor组件加载顺序,受预渲染、Blazor组件生命周期的等影响,是坑最多的地方,需要特别注意。如下图所示,注意4个坑的说明:
补充说明:
(1){NAME}.lib.module.js,称为JS初始值。其中NAME为项目的名称,要遵循命名约定。JS初始值主要有两个回调函数可以使用,如下图所示,可以看一下,在Server和WASM模式下,回调什么时候执行,以及框架调用时,传入的参数options、extensions和blazor分别是什么?
(2)Server模式下,修改预渲染的方式。修改文档【Pages/_Host.cshtml】的以下代码:
<component type="typeof(App)" render-mode="ServerPrerendered" /> //开启服务端预渲染
<component type="typeof(App)" render-mode="Server"/> //关闭服务端预渲染
2、如何引用JS代码
(1)添加JS的位置,Pages/_Layout.cshtml(Server模式);wwwroot/index.html(WASM模式)
(2)引用JS代码方式主要有两种,一是直接在script标签里写代码,二是以JS文件的方式,推荐JS文件方式,和HTML中使用JS差不多,此处不详述
(3)引用JS的具体位置也有很多,推荐在body标签里,【blazor.server|webassembly.js】之后。如下所示(前缀波浪号~指WEB根目录):
(4)JS代码组织方式(以下为推荐方式):
三、在C#中调用JS
1、使用步骤:C#调用JS,主要使用JSRuntime类,我们面向IJSRuntime接口,以依赖注入的方式来创建JSRuntime对象,并使用这个对象提供的两个主要方法:InvokeVoidAsync和InvokeAsync<T>,前者无返回值,后者有返回值,在泛型T中定义具体的返回值类型。使用非常简单,三步走:
(1)第一步:WEB根目录下,创建JS:
在www/js文件夹下,新建MyApp.js,增加一个simpleSum函数。本例创建一个应用级的JS函数。
var MyApp = MyApp || {};
MyApp.simpleSum = function (a, b) { return a + b; }
(2)第二步:HTML入口页面中,引用JS:
在_Layout.cshtml(Server模式),或index.html(WASM模式)中,<body>标签中引入JS文件
<body>......
<script>_framework/blazor.server.js</script>
<script>~/js/MyApp.js</script>
</body>
(3)第三步:Blazor组件中,调用JS:
在需要使用这个JS函数的Blazor组件中调用,
//MyApp.js var MyApp = MyApp || {}; MyApp.simpleSum = function (a, b) { return a + b; }; //_Layout.cshtml <body> ...... <script src="_framework/blazor.server.js"></script> <script src="~/js/MyApp.js"></script> <script src="../Pages/Index.razor.js"></script> ...... </body> //Index.razor @page "/" @inject IJSRuntime JS <h1>JS返回结果1:@result1</h1> <button @onclick="invokeSimpleSum">调用JS函数simpleSum</button> @code{ private int result1; private async Task invokeSimpleSum() { result1 = await JS.InvokeAsync<int>("MyApp.simpleSum", 1, 2); } }
2、继续举些例子来熟悉InvokeVoidAsync和InvokeAsync<T>的使用(多个案例来自Blazor University)
1)直接在C#中调用JS的Alert,这个函数是JS本身就有的,所以我们不需要再定义JS,直接在Blazor中使用
//Index.razor @page "/" @inject IJSRuntime JS <button @onclick="JSAlert">弹窗</button> @code{ private async Task JSAlert() { await JS.InvokeVoidAsync("alert", "这是弹窗警告信息"); } }
2)继续来调用一个有返回值的JS的confirm对话框,依然用的是JS内置的函数,不需要定义JS
//Index.razor @page "/" @inject IJSRuntime JS <h1>确定结果为:@result</h1> <button @onclick="JSConfirm">弹窗确认</button> @code{ private string? result; private async Task JSConfirm() { bool confirm = await JS.InvokeAsync<bool>("confirm", "您确定吗?"); result = confirm ? "是" : "否"; } }
3、InvokeVoidAsync和InvokeAsync<T>方法的参数
1)Invoke方法有几个重载,如下(无返回值和有返回值可以互换):
2)参数说明:
3)重点说明输入参数,Object[]
①参数:引用类型:
Object[]参数传入Invoke方法时,会自动序列化为Json,JS收到参数后,自动进行反序列化。参数不仅可以传入普通的值类型,也可以传入引用类型。以下案例,我们在C#的Invoke方法中,传入一个Student对象,首先自动序列化为Json,JS接收后自动反序列化为JS对象。JS如果返回JS对象,C#中可以使用相同结构的对象来直接接收。
//JS函数Index.razor.js var Index = Index || {}; Index.getStudent = function (student) { console.log(student); } //Blazor组件index.razor @page "/" @inject IJSRuntime JS <button @onclick="JSGetStudent">点击执行</button> @code{ private async Task JSGetStudent() { var stu1 = new Student() { Name = "张三", Age = 18 }; await JS.InvokeVoidAsync("Index.getStudent", stu1); } public class Student { public string? Name { get; set; } public int Age { get; set; } } }
//例子延伸,JS返回结果是一个JS对象,C#中有相同结构的对象来接收 //JS函数Index.razor.js var Index = Index || {}; Index.getStudent = function (student) { return student; } //Blazor组件index.razor @page "/" @inject IJSRuntime JS @if (stuResult != null) { <h1>确定结果为:@stuResult.Name</h1> } <button @onclick="JSGetStudent">点击执行</button> @code{ private string? result; private Student? stuResult; private async Task JSGetStudent() { var stu1 = new Student() { Name = "张三", Age = 18 }; stuResult = await JS.InvokeAsync<Student>("Index.getStudent", stu1); } public class Student { public string? Name { get; set; } public int Age { get; set; } } }
②参数:Blazor组件树元素引用
Blazor不建议我们直接操作DOM,有可能会破坏组件树的Diff更新,但有时候确实需要操作DOM,Blazor给我们提供了@ref属性,相当于可以为一个组件节点申明了一个变量,我们可以将这个变量作为参数传入JS。需要特别注意这里有一个坑,请回顾本章节的第一幅图,因为JS和Blazor的加载顺序问题,我们要在组件渲染后(OnAfterRender),DOM都加载完了,才能获得正确的ref引用。下面直接来两个非常好的案例:
//案例1:设置页面标题 //JS函数,MyApp.js var MyApp = MyApp || {}; MyApp.setTitle = function (title) { document.title = title; } //Blazor组件,Index.razor @page "/" @inject IJSRuntime JS @code{ //必须在OnAfterRender生命周期函数中调用 //根据firstRender判断,只在首次加载时调用JS函数 protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("MyApp.setTitle", "这是Index页面"); } } }
//案例2:设置自动对焦 //JS函数:MyApp.js var MyApp = MyApp || {}; MyApp.setFocus = function (element) { element.focus(); } //Blazor组件,Index.razor @page "/" @inject IJSRuntime JS <label for="name" @ref="elementInputName">姓名:</label> <input type="text" id="name"/> <label for="age">年龄:</label> <input type="text" id="age"/> @code{ //ElementReference是组件节点的引用类型 private ElementReference elementInputName; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("MyApp.setFocus", elementInputName); } } }
//案例3:紧接案例2,我们将设置自动对焦包装成一个组件 //JS函数没有变化,MyApp.js var MyApp = MyApp || {}; MyApp.setFocus = function (element) { element.focus(); } //新建一个Focus组件,Focus.razor //稍后解释,为什么父传子不是直接传ElementReference,而是一个Func委托? @inject IJSRuntime JS @code { [Parameter] public Func<ElementReference>? GetElement { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (GetElement is null) { throw new ArgumentException(); } if (firstRender) { await JS.InvokeVoidAsync("MyApp.setFocus", GetElement()); } } } //在Index.razor中,使用自动对焦组件<Focus> //与案例2的Index.razor相比较,不同之处在于,我们传入一个回调,且这个回调返回元素引用 //为什么要以回调的形式传入? //根据父子组件生命周期的顺序,在子组件渲染完成后,父组件才会渲染 //子组件执行AfterRender时,父组件可能还没有渲染完成,所以此时@ref不一定拿得到 //所以,我们需要在组件加载完成后,在回调里,把元素引用传入 @page "/" @inject IJSRuntime JS <label for="name" @ref="elementInputName">姓名:</label> <input type="text" id="name"/> <label for="age">年龄:</label> <input type="text" id="age"/> <Focus GetElement="@(()=>{return elementInputName;})"></Focus> @code{ private ElementReference elementInputName; }
四、在JS中调用C#
1、互操作的发起端
在JS中,使用 DotNet.invokeMethodAsync(异步,在Server和WASM模式下均可用,推荐),或者 DotNet.invokeMethod(同步,仅在WASM模式下可用),直接调用C#中使用JsInvokableAttribute特性标注的方法。调用静态方法和实例方法,稍有不同,后文详解。在JS中使用 DotNet.invokeMethodAsync 时,如果要获取异步结果,promise和async的语法均可以使用,推荐使用async。
由于加载JS时,Blazor可能还未加载,所以我们一般在C#端先发起JS的调用,然后才在JS端调用C#。多绕了一圈,肯定有性能损耗,但这样更安全。当然,也可以直接在视图层,直接发起原生的HTML事件上,直接调用JS函数,如下例所示:
//MyApp.js //使用 DotNet.invokeMethodAsync 调用C#的一个静态方法 //方法的第1个参数为项目名称;第2个参数为C#静态方法名称 //使用async语法执行异步函数,并获得返回结果 var MyApp = MyApp || {}; MyApp.getNetArray = async function() { const result = await DotNet.invokeMethodAsync("JSRuntimeServer", "ReturnArrayAsync"); console.log(result); } //Index.razor //注意button使用的是HTML原生的onclick事件,直接调用JS函数 //C#方法是公共的、静态的,且使用[JSInvokable]标注 @page "/" @inject IJSRuntime JS <button onclick="MyApp.getNetArray()">原生调用JS方法</button> @code{ [JSInvokable] public static Task<int[]> ReturnArrayAsync() { return Task.FromResult(new int[] { 1, 2, 3 }); } } //当然,C#中的ReturnArrayAsync也可以使用同步方法,如下: @code{ [JSInvokable] public static int[] ReturnArray() { return new int[] { 1, 2, 3 }; } }
2、调用C#的静态方法
调用C#静态方法的步骤比较简单,基本步骤如下所示,不再举例
1)第一步:定义JS函数
函数体中调用C#静态方法:DotNet.invokeMethodAsync( "{AssemblyName}", "{NETMethodID}", {Arguments} );
2)第二步:定义C#静态方法,并使用JSInvokeableAttribute标注
[JSInvokeable("NETMehtodID")]
public static ......
3)第三步:在C#中调用JS函数
3、调用C#的实例方法及使用实例引用的注意事项
调用C#的实例方法,会相对复杂一些。上节中,我们看到,调用静态方法时,我们使用框架提供的DotNet对象,就可以直接通过程序集,调用静态方法。而调用实例方法,需要将Blazor的组件实例传入到JS中,这是两者的主要区别。静态方法中我们通过DotNet对象的invokeMethodAsync来调用,而在实例方法中,我们需要通过使用实例的invokeMehtonAsync来调用。我们直接通过案例来学习使用方法。
1)第一步:定义JS函数,在函数中调用C#的实例方法
//MyApp.js //JS函数接受一个Blazor组件的实例对象 //通过这个实例对象的invokeMethod方法,调用组件的方法 var MyApp = MyApp || {}; MyApp.getNetArray = async function (blazorObject) { const result = await blazorObject.invokeMethodAsync("ReturnArrayID"); console.log(result); }
2)第二步:在Blaozr组件中,定义被JS调用的方法,并调用JS函数(将第2节的两步合并为这里的一步)
//Index.razor @page "/" @implements IDisposable @inject IJSRuntime JS <button @onclick="GetNetArrayJS">调用JS函数,该函数反过来调用C#方法</button> @code { //定义一个DotNetObjectReference<T>类型的字段 private DotNetObjectReference<Index>? blazorObject; private async Task GetNetArrayJS() { //使用DotNetObjectReference.Create()方法,创建当前组件的实例对象 blazorObject = DotNetObjectReference.Create(this); //调用JS函数时,传入当前组件的实例对象 await JS.InvokeVoidAsync("MyApp.getNetArray", blazorObject); } //使用JSInvokableAttribute特性标注对象,并使用参数设置方法标识符 [JSInvokable("ReturnArrayID")] public async Task<int[]> ReturnArray() { //异步返回一个数组给JS return await Task.FromResult(new int[] { 1, 2, 3 }); } //千万记得,在组件销毁生命周期函数中,销毁组件实例 //因为JS引用着组件实例,所以即使走了Dispose生命周期,组件不会被自动回收,会一直占有内存 public void Dispose() { blazorObject?.Dispose(); } }
五、其它知识点
1、调用JS的异常处理、超时处理、中止执行
1)调用JS的异常处理
//如果JS没有定义method1方法,将捕获一个JS执行异常 //使用JSException类型参数来接收异常信息 @code { private string? errorMessage; private string? result; private async Task CatchJSMethod1) { try { result = await JS.InvokeAsync<string>("method1"); } catch (JSException e) { errorMessage = $"Error Message: {e.Message}"; } } }
2)超时处理
//调用JS函数的默认超时时间为1分钟,可以在注册服务时配置默认时间 //设置超时的时间,主要在Server模式下应用,但WASM模式也可以使用 builder.Services.AddServerSideBlazor( options => options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(120)); //调用JS方法时,也可以设置超时的时间,如果设置,则覆盖默认时间 var result = await JS.InvokeAsync<string>("MyApp.method1", TimeSpan.FromSeconds(120), param1);
3)中止执行
//和很多异步方法一样,可以传入CancellationTokenSource对象 //实现在异步方法的外部,中止异步方法执行 //需要注意,在组件的Dispose生命周期函数中,记得回收CancellationTokenSource对象 @inject IJSRuntime JS <button @onclick="StartTask">开始执行</button> <button @onclick="CancelTask">中止执行</button> @code { private CancellationTokenSource? cts; private async Task StartTask() { cts = new CancellationTokenSource(); cts.Token.Register(() => JS.InvokeVoidAsync("MyApp.method1")); } private void CancelTask() { cts?.Cancel(); } public void Dispose() { cts?.Cancel(); cts?.Dispose(); } }
2、调用JS的模块化module方式,引用一个JS库
当需要引入一个JS库时,推荐使用module的方式,可以有效防止变量名的全局污染。基本的使用步骤如下(案例来自官方文档,做了简化):
1)我们假定自己做了一个简单的JS库,在 wwwroot/js 目录下,创建 MyLibrary.js ,里面只export一个函数,如下:
function alertMessage(message) { alert(message); } export { alertMessage };
2)在C#中,以模块化的方式引入
@page "/" @implements IAsyncDisposable @inject IJSRuntime JS <button @onclick="AlertMessageJS">调用MyLibrary的JS函数</button> @code { //使用IJSObjectReference类型,来创建JS模块的对象 private IJSObjectReference? module; //组件完成渲染后,获取JS模块对象module protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>("import", "../js/MyLibrary.js"); Console.WriteLine(module); } } //使用JS模块对象的InvokeVoidAsync方法,调用JS函数 private async Task AlertMessageJS() { if (module is not null) { await module.InvokeVoidAsync("alertMessage", "这是需要显示的信息"); } } //记得组件销毁时,要回收JS模块对象,否则GC不会主动回收,造成内存损耗 //当然,这里也可以使用同步的IDisposable public async ValueTask DisposeAsync() { if (module is not null) { await module.DisposeAsync(); } } }
3、WASM模式下同步调用JS函数
WASM模式下,在C#中可以同步调用JS,会带来一点点性能上的提升,案例如代码所示(官网案例)。Server模式下,仅可使用异步方法。推荐无论是WASM,还是Server,都永远使用异步方法。
//同步调用JS @inject IJSRuntime JS @code { protected override void HandleSomeEvent() { var JSIP = (IJSInProcessRuntime)JS; var value = JSIP.Invoke<string>("JSMethod1"); } }
4、Server模式下传输数据的大小限制
Server模式下,JS 到C #的SignalR有大小限制,默认情况下是32K,可以在服务容器中进行配置,如案例代码所示。但不建议调整太大,否则会占用更多服务器资源,引起性能损耗。如果数据很大,可以考虑式用流失数据。WASM模式下,没有大小限制。
//设置为64K builder.Services.AddServerSideBlazor() .AddHubOptions(options =>options.MaximumReceiveMessageSize = 64 * 1024);
5、不要循环调用
JS和C#可以互操作,复杂点,甚至能形成一个调用链,但最好不要循环引用