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


    C#和JS互操作的基本语法是比较简单的,但小知识点特别多,同时,受应用加载顺序、组件生命周期以及参数类型的影响,会有比较多坑,需要耐心的学习。C#中调用JS的场景会比较多,特别是在WASM模式下,由于WebAssembly的限制,很多时候,还是需要借助JS去控制DOMBOM,比如WebStorageWebGLMediaCapture,还比如使用JS的图表库echart.js。反过来,在JS中调用C#的场景,就比较少见。所以,此章节关于"C#中调用JS” 的篇幅会多一些

     

     

    这个章节比较长,目录如下:

    一、C#和JS互操作涉及到的类和接口

    二、HTML/JS和Blazor/C#的加载顺序

    三、在C#中调用JS
    1、使用步骤
    2、继续举些例子来熟悉InvokeVoidAsync和InvokeAsync的使用
    3、InvokeVoidAsync和InvokeAsync方法的参数:引用类型、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,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】的以下代码:

    //开启服务端预渲染

    //关闭服务端预渲染

     

     

    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根目录):

     

     4JS代码组织方式(以下为推荐方式):

    • 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对象,并使用这个对象提供的两个主要方法:InvokeVoidAsyncInvokeAsync,前者无返回值,后者有返回值,在泛型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模式)中,标签中引入JS文件

    ......

       

       

     

     

    (3)第三步:Blazor组件中,调用JS:

    在需要使用这个JS函数的Blazor组件中调用,

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

     

    复制代码
    //MyApp.js
    var MyApp = MyApp || {};
    MyApp.simpleSum = function (a, b) {
        return a + b;
    };
    
    
    //_Layout.cshtml
    
        ......
        
        
        
        ......
    
    
    
    //Index.razor
    @page "/"
    @inject IJSRuntime JS
    
    

    JS返回结果1:@result1

    @code{ private int result1; private async Task invokeSimpleSum() { result1 = await JS.InvokeAsync<int>("MyApp.simpleSum", 1, 2); } }
    复制代码

     

     

    2、继续举些例子来熟悉InvokeVoidAsync和InvokeAsync的使用(多个案例来自Blazor University)

    1)直接在C#中调用JS的Alert,这个函数是JS本身就有的,所以我们不需要再定义JS,直接在Blazor中使用

    复制代码
    //Index.razor
    @page "/"
    @inject IJSRuntime JS
    
    
    
    @code{
    
        private async Task JSAlert()
        {
            await JS.InvokeVoidAsync("alert", "这是弹窗警告信息");
        }
    }
    复制代码

     

    2)继续来调用一个有返回值的JS的confirm对话框,依然用的是JS内置的函数,不需要定义JS

    复制代码
    //Index.razor
    @page "/"
    @inject IJSRuntime JS
    
    

    确定结果为:@result

    @code{ private string? result; private async Task JSConfirm() { bool confirm = await JS.InvokeAsync<bool>("confirm", "您确定吗?"); result = confirm ? "" : ""; } }
    复制代码

     

     

    3、InvokeVoidAsync和InvokeAsync方法的参数

    1)Invoke方法有几个重载,如下(无返回值和有返回值可以互换):

    • InvokeAsync(IJSRuntime, String, Object[])
    • InvokeAsync(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
    
    
    
    @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)
    {
        

    确定结果为:@stuResult.Name

    } @code{ private string? result; private Student? stuResult; private async Task JSGetStudent() { var stu1 = new Student() { Name = "张三", Age = 18 }; stuResult = await JS.InvokeAsync("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
    
    
    "text" id="name"/>
    
    
    "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? 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中,使用自动对焦组件
    //与案例2的Index.razor相比较,不同之处在于,我们传入一个回调,且这个回调返回元素引用
    //为什么要以回调的形式传入?
    //根据父子组件生命周期的顺序,在子组件渲染完成后,父组件才会渲染
    //子组件执行AfterRender时,父组件可能还没有渲染完成,所以此时@ref不一定拿得到
    //所以,我们需要在组件加载完成后,在回调里,把元素引用传入
    @page "/"
    @inject IJSRuntime JS
    
    
    "text" id="name"/>
    
    "text" id="age"/>
    
    "@(()=>{return elementInputName;})">
    
    @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
    
    
    
    @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

     

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

    • 可以直接使用HTML原生事件直接调用:
    • 也可以如前面小节,注入JSRuntime来调用:......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
    
    
    
    @code {
    
        //定义一个DotNetObjectReference类型的字段
        private DotNetObjectReference? 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
    
    
    
    
    
    @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
    
    
    
    @code {
        //使用IJSObjectReference类型,来创建JS模块的对象
        private IJSObjectReference? module;
    
        //组件完成渲染后,获取JS模块对象module
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                module = await JS.InvokeAsync("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#可以互操作,复杂点,甚至能形成一个调用链,但最好不要循环引用

     

  • 相关阅读:
    博途PLC S7-1200/1500 ModbusTcp通信SCL代码篇(轮询)
    基于jeecgboot的主从表改造成抽屉式的字典操作模式
    Spring 从入门到精通 (二十二) 整合持久层框架细节
    makefile之VPATH和vpath的用法
    java面向社区健康核酸预约服务的springboot医疗平台
    __slots__限制类动态增加属性【Python面向对象进阶二】
    SpringBatch 使用过程中遇到的问题
    前端性能优化:页面加载速度慢怎么办?
    复习总结 --- Linux指令
    创维光伏:坚持科技创新,构建中国式现代化光伏生态体系
  • 原文地址:https://www.cnblogs.com/functionMC/p/16552500.html