做Web开发肯定离不开Javascript,Blazor虽然一定程度上可以用c#来替换Javascript的功能,但是完全抛弃Javascript肯定是不可能的,因此必然需要一种机制让C#可以和Javascript互相调用,也可以称之为互操作。

从调用的方向来看可以是C#调用Javascript,也可以是Javascript调用C#。

1. C#调用Javascript

做Web开发的都知道Javascript脚本都是通过<script>标签来引入的,不过Blazor不能直接在Razor组件里面添加该标签,你只能添加到文件"wwwroot/index.html (Blazor WebAssembly)" 或者 "Pages/_Host.cshtml (Blazor Server)"中。

1.1 IJSRuntime

Blazor提供了IJSRuntime接口让开发者可以方便的进行Javascript调用。

以下是她的声明:

namespace Microsoft.JSInterop
{
    public interface IJSRuntime
    {
        ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
        ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args);
    }
}

同时还有一组扩展方法

namespace Microsoft.JSInterop
{
    public static class JSRuntimeExtensions
    {
        public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, params object[] args);
        public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args);
        public static ValueTask<TValue> InvokeAsync<TValue>(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args);
        public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object[] args);
        public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args);
        public static ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args);
    }
}

其实主要还是用扩展方法,她比原始方法更灵活,如果需要取返回值,则使用InvokeAsync方法,不需要返回值则使用InvokeVoidAsync。

使用之前必选先在Razor组件里面声明注入:

@inject IJSRuntime JSRuntime;

1.2 identifier

identifier作为每个函数的第一个必选参数,表示JS里面函数的标识符,如果是全局函数,直接写函数名就行,如果包含范围(命名空间或实例),则使用"{范围}.{函数名}"的形式。

比如如下js:

//全局
function sayHello() {
    alert('Hello');
}

//带范围
window.jsUtils = {
    sayHello: function () {
        alert('jsUtils:Hello');
    }
};

可以这样调用:

<!--不带参数调用-->
<button @onclick="SayHello">全局函数</button>
<button @onclick="JsUtilSayHello">带范围的函数</button>
@code {
    void SayHello()
    {
        this.JSRuntime.InvokeVoidAsync("sayHello");
    }
    void JsUtilSayHello()
    {
        this.JSRuntime.InvokeVoidAsync("jsUtils.sayHello");
    }
}

1.3 args

args,顾名思义,就是参数了,这个是可选的,而且可以是一个,也可以是多个,可以是简单的值,也可以是对象(要求能够使用JSON序列化)。

示例如下:

// javascript 函数
//全局
function sayHelloPerson(person) {
    alert('Hello:' + person.name);
}

//带范围
window.jsUtils = {
    sayHelloPerson: function (person) {
        alert('jsUtils:Hello:' + person.name);
    }
};
<!--Razor 带参数调用-->
<button @onclick="SayHelloPerson">全局函数</button>
<button @onclick="JsUtilSayHelloPerson">带范围函数</button>
@code {
    void SayHelloPerson()
    {
        this.JSRuntime.InvokeVoidAsync("sayHelloPerson",new { Name="六十五号腕" });
    }
    void JsUtilSayHelloPerson()
    {
        this.JSRuntime.InvokeVoidAsync("jsUtils.sayHelloPerson", new { Name = "六十五号腕" });
    }
}

需要注意的是C#的JSON序列化会将属性名的首字母转成小写,所以js里面要使用小写的属性名。
还有如果有日期类型,不管是作为对象的属性,还是单独作为参数传递给js的时候都会变成字符串,并不是原始的Js Date类型,格式参考:2020-08-08T08:08:08.798+08:00

1.4 获取Js的返回值

获取返回值需要使用InvokeAsync方法,参数和前面的InvokeVoidAsync基本一致,但是需要声明返回值的类型,返回的值可以是简单值,也可以是一个对象。

下面的例子就使用InvokeAsync方法从js函数里面返回了一个对象:

// javascript 函数
function getPerson() {
    return {
        name: '六十五号腕',
        actionDate: new Date()
    };
}
// c#
public class Person
{
    public string Name { get; set; }
    public DateTime ActionDate { get; set; }
}

async void GetPerson()
{
    var person= await this.JSRuntime.InvokeAsync<Person>("getPerson");
}

可以看到需要先在c#里面声明一个与js返回值同结构的类用来序列化,测试发现js里面的Date是可以正常序列化成c#里面的DateTime的

1.5 IJSInProcessRuntime

前面的方法都属于异步方法,如果使用的是WebAssembly模式,Blazor提供了一个额外的接口IJSInProcessRuntime用来进行js的同步调用,这样会节省一些异步的开销,同样是上面的例子,可以写成:

void GetPerson()
{
    var person=  ((IJSInProcessRuntime)this.JSRuntime).Invoke<Person>("getPerson");
}

需要注意的是这个接口只能从IJSRuntime转换过来,不能直接使用inject注入。

2. Javascript调用C#方法

2.1 JSInvokable

如果想从Javascript里面调用C#的方法,首先就得把C#的方法暴露出去,我们需要在每个C#方法上面加上[JSInvokable]属性,如下所示:

[JSInvokable]
void CSharpMethod1()
{
}
[JSInvokable("Method2")]
void CSharpMethod2()
{
}

该属性有两种构造,默认构造会将C#的函数名暴露给Javascript(这里并不会进行首字母大小写转换,所以如果C#里面是首字母大写,Javascript里面的方法名也必须保持首字母大写。), 另一个构造则可以指定其他的暴露名称。

2.2 调用静态方法

如果想从Javascript调用C#的静态方法,可以使用DotNet.invokeMethod 或 DotNet.invokeMethodAsync这两个方法,一个用于同步方法,另一个用于异步。

这两个方法的参数是一样的,第一个参数是程序集的名称,比如Bigname65.Blazor,第二个是使用JSInvokable属性暴露的方法名,请看下面的例子:

//c# 静态方法
[JSInvokable]
public static string GetName()
{
    return "六十五号腕";
}
//javascript 调用
function getName() {
    var name = DotNet.invokeMethod("你的程序集名称","GetName");
    alert(name);
}

需要注意的是第一个参数是你的程序集名称,但是同一个程序集下面难免会出现重名的静态方法,这时候就要考虑使用[JSInvokable]属性指定额外的调用名称了。

以上是同步调用的例子,异步的情况也差不多,如下所示:

//c# 静态方法
[JSInvokable]
public static Task<string> GetNameAsync()
{
    return Task.FromResult<string>("六十五号腕");
}
//javascript 调用
function getNameAsync() {
    DotNet.invokeMethodAsync('你的程序集名称', 'GetName')
        .then(name => {
            alert(name)
        });
}

额外参数

如果C#函数带有参数,同样也是支持的,从DotNet.invokeMethod(或Asyc)的第三个参数开始按顺序排列就可以了。

2.3 组件内的实例方法

上一节调用的静态方法,可以是组件内的,也可以是组件外的。如果想调用当前组件内的非静态方法应该如何操作呢?

我们可以声明一个静态的委托类型,在组件初始化的时候给其赋值,然后在Javascript调用的时候直接使用委托就可以了。

针对上面的例子我们稍微做一下修改就可以了:

//Razor组件代码
//1. 声明一个静态委托变量
static Func<string> _funcGetInputName;

protected override void OnInitialized()
{
    //2. 组件初始化的时候将实例方法赋值给静态委托变量
    _funcGetInputName = GetInputName;

    base.OnInitialized();
}

private string GetInputName()
{
    return "六十五号腕";
}

// 暴露方法给Js
[JSInvokable]
public static string GetName()
{
    //3. Js调用该方法的时候直接使用委托
    return _funcGetInputName.Invoke();
}

这里只是一个用来获取值的Func委托,你也可以用Action或者其他自定义的委托。

2.4 组件外的实例方法

调用组件外的非静态方法其实可以单独作为一节,因为她就是一种混合操作,具体过程为:

  • 创建C#类,并使用[JSInvokable]属性修饰需要暴露的方法。
  • 创建对应的实例,并使用[IJSRuntime]接口传递给Javascript。
  • 在Javascript中调用C#实例的方法。

以下是参考代码:

//Razor组件代码
//1. 创建Person类
public class Person
{
    string _name = "";

    public Person(string name)
    {
        _name = name;
    }

    //2. 暴露GetName方法
    [JSInvokable]
    public string GetName()
    {
        return _name;
    }
}

DotNetObjectReference<Person> _personRef = null;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        //3. 创建Person实例
        var person = new Person("六十五号腕");
        //4. 创建Person的Js引用
        _personRef = DotNetObjectReference.Create(person);
        //5. 调用Js的setCurrentPerson方法
        await JSRuntime.InvokeAsync<string>(
            "setCurrentPerson",
            _personRef);
    }

    await base.OnAfterRenderAsync(firstRender);
}

public void Dispose()
{
    //6. 组件回收的时候释放Js引用
    _personRef?.Dispose();
}

C#里面最好在Dispose的时候释放Js引用,以免引起内存泄漏。

//Javascript代码
var person = undefined;
//C#调用,给全局person赋值
function setCurrentPerson(p) {
    person = p;
}
//Js调用,显示当前Person的名字
function showCurrentPerson() {
    //同样使用invokeMethod或者invokeMethodAsync方法
    var name = person.invokeMethod("GetName");
    alert(name);
}

这里的javascript也是使用invokeMethod或者invokeMethodAsync方法来调用C#里面的方法。

更多推荐

Blazor中C#与Javascript的互操作