Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文)

2022/8/15 1:55:00

本文主要是介绍Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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代码组织方式(以下为推荐方式):

  • 在web根文件夹下新建js文件夹,所有应用级的JS文件都放在这个文件夹下,引用位置为“~/js/***.js”
  • 组件级的JS文件(特定组件专用),在组件同目录下创建同名的JS文件,如Index.razor-->Index.razor.js,VisualStudie会自动将JS文件组织到同名的razor文件下
  • JS函数建议类似命名空间的组织方式,可以有效防止变量名的全局污染,如:var MyApp = MyApp||{}; MyApp.method1=function(){}; MyApp.methond2=function(){};
  • 如果不想引用多个JS文件,也可以将应用级和组件级的JS写到一个文件里。JS代码的组织方式,还有一种是模块化,也是为了防止变量名的全局污染,但易用性较差,不推荐。在Blazor中使用JS函数,遵守的原则还是:能不用,就不用。如果一个项目中,定义的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组件中调用,

  • 注入IJSRuntime对象:@inject IJSRuntime JS
  • 在视图层,定义一件按钮事件来触发“调用JS”的C#方法:<button @click="InvokeJSMethod"></button>
  • 在逻辑层定义“调用JS”的C#方法,在方法调用JS:int a = await InvokeAsync<int>("MyApp.simpleSum",2,3)

 

//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方法有几个重载,如下(无返回值和有返回值可以互换):

  • InvokeAsync<TValue>(IJSRuntime, String, Object[])
  • InvokeAsync<TValue>(IJSRuntime, String, TimeSpan, Object[])
  • InvokeVoidAsync(IJSRuntime, String, CancellationToken, Object[])

 

2)参数说明:

  • IJSRuntime:默认传入
  • String:JS方法名
  • Object[]:参数数组,可序列化的数据类型均可传入
  • TimeSpan:设置调用JS的超时时间
  • CancellationToken:传入CancellationToken对象,在外部停止调用过程

 

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} );

  • AssemblyName:项目/程序集的名称
  • NETMethodID:C#方法的标识符,即可以是方法名称,也可以是[JSInvokeable]特性的参数,如[JSInvokable("ReturnArray")]
  • Arguments:多个参数,以逗号分隔,每个参数都必须是可序列化的

 

2)第二步:定义C#静态方法,并使用JSInvokeableAttribute标注

[JSInvokeable("NETMehtodID")]

public static ......

  • 方法必须标注特性,[JSInvokeable],其中参数可用可不用,如果使用了参数,则NETMethodID就使用参数值,如果不使用,则为方法名
  • 方法必须是public static,公开的、静态的
  • 方法可以是同步方法,也可以是异步方法。异步方法的返回值,可以使用void、Task 或 Task<T>

 

3)第三步:在C#中调用JS函数

  • 可以直接使用HTML原生事件直接调用:<button onclick="JSmethod1()"></button>
  • 也可以如前面小节,注入JSRuntime来调用:<button @onclick="JSgetNetArray">C#中调用JS方法</button>......private void JSgetNetArray(){JS.InvokeVoidAsync("MyApp.getNetArray");}

 

 

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#可以互操作,复杂点,甚至能形成一个调用链,但最好不要循环引用

 



这篇关于Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程