將 SignalR 與 Web API 和 JavaScript Web App 一起使用,並支援 CORS

目標: 使用 SignalR 在 Web API 和基於 TypeScript / JavaScript 的 Web App 之間進行通知,其中 Web API 和 Web App 託管在不同的域中。

在 Web API 上啟用 SignalR 和 CORS: 建立標準 Web API 專案,並安裝以下 NuGet 包:

  • Microsoft.Owin.Cors
  • Microsoft.AspNet.WebApi.Cors
  • Microsoft.AspNet.WebApi.Owin
  • Microsoft.AspNet.SignalR.Core

之後,你可以擺脫 Global.asax 並新增一個 OWIN Startup 類。

using System.Web.Http;
using System.Web.Http.Cors;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(WebAPI.Startup), "Configuration")]
namespace WebAPI
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
        
            var httpConfig = new HttpConfiguration();
            
            //change this configuration as you want.
            var cors = new EnableCorsAttribute("http://localhost:9000", "*", "*"); 
            httpConfig.EnableCors(cors);

            SignalRConfig.Register(app, cors);

            WebApiConfig.Register(httpConfig);

            app.UseWebApi(httpConfig);
        }
    }
}

建立 SignalRConfig 類如下:

using System.Linq;
using System.Threading.Tasks;
using System.Web.Cors;
using System.Web.Http.Cors;
using Microsoft.Owin.Cors;
using Owin;

namespace WebAPI
{
    public static class SignalRConfig
    {
        public static void Register(IAppBuilder app, EnableCorsAttribute cors)
        {

            app.Map("/signalr", map =>
            {
                var corsOption = new CorsOptions
                {
                    PolicyProvider = new CorsPolicyProvider
                    {
                        PolicyResolver = context =>
                        {
                            var policy = new CorsPolicy { AllowAnyHeader = true, AllowAnyMethod = true, SupportsCredentials = true };

                            // Only allow CORS requests from the trusted domains.
                            cors.Origins.ToList().ForEach(o => policy.Origins.Add(o));

                            return Task.FromResult(policy);
                        }
                    }
                };
                map.UseCors(corsOption).RunSignalR();
            });
        }
    }
}

到目前為止,我們剛剛在伺服器端啟用了帶有 CORS 的 SignalR。現在讓我們看看如何從伺服器端釋出事件。為此我們需要一個 Hub

public class NotificationHub:Hub
{
    //this can be in Web API or in any other class library that is referred from Web API.
}

現在終於實際廣播變化的一些程式碼:

public class SuperHeroController : ApiController
{
    [HttpGet]
    public string RevealAlterEgo(string id)
    {
        var alterEgo = $"The alter ego of {id} is not known.";
        var superHero = _superHeroes.SingleOrDefault(sh => sh.Name.Equals(id));
        if (superHero != null)
        {
            alterEgo = superHero.AlterEgo;
            
            /*This is how you broadcast the change. 
             *For simplicity, in this example, the broadcast is done from a Controller, 
             *but, this can be done from any other associated class library having access to NotificationHub.
             */
            var notificationHubContext = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
            if (notificationHubContext != null)
            {
                var changeData = new { changeType = "Critical", whatHappened = $"Alter ego of {id} is revealed." };

                //somethingChanged is an arbitrary method name.
                //however, same method name is also needs to be used in client.
                notificationHubContext.Clients.All.somethingChanged(changeData);
            }
        }
        return alterEgo;
    }
}

因此,到目前為止,我們已經準備好了伺服器端。對於客戶端,我們需要 jQuerysignalr 包。你可以使用 jspm 安裝。如果需要,安裝兩者的型別。

我們不會使用預設生成的 JavaScript 代理。我們寧願建立一個非常簡單的類來處理 SignalR 通訊。

/**
 * This is created based on this gist: https://gist.github.com/donald-slagle/bf0673b3c188f3a2559c.
 * As we are crreating our own SignalR proxy, 
 * we don't need to get the auto generated proxy using `signalr/hubs` link.
 */
export class SignalRClient {

    public connection = undefined;
    private running: boolean = false;

    public getOrCreateHub(hubName: string) {
        hubName = hubName.toLowerCase();
        if (!this.connection) {
            this.connection = jQuery.hubConnection("https://localhost:44378");
        }

        if (!this.connection.proxies[hubName]) {
            this.connection.createHubProxy(hubName);
        }

        return this.connection.proxies[hubName];
    }

    public registerCallback(hubName: string, methodName: string, callback: (...msg: any[]) => void,
        startIfNotStarted: boolean = true) {

        var hubProxy = this.getOrCreateHub(hubName);
        hubProxy.on(methodName, callback);

        //Note: Unlike C# clients, for JavaScript clients, 
        //      at least one callback needs to be registered, 
        //      prior to start the connection.
        if (!this.running && startIfNotStarted)
            this.start();
    }

    start() {
        const self = this;
        if (!self.running) {
            self.connection.start()
                .done(function () {
                    console.log('Now connected, connection Id=' + self.connection.id);
                    self.running = true;
                })
                .fail(function () {
                    console.log('Could not connect');
                });
        }
    }
}

最後使用此類來收聽更改廣播,如下所示:

/**
 * Though the example contains Aurelia codes, 
 * the main part of SignalR communication is without any Aurelia dependency.
 */
import {autoinject, bindable} from "aurelia-framework";
import {SignalRClient} from "./SignalRClient";

@autoinject
export class SomeClass{
    
    //Instantiate SignalRClient.
    constructor(private signalRClient: SignalRClient) {
    }

    attached() {
        //To register callback you can use lambda expression...
        this.signalRClient.registerCallback("notificationHub", "somethingChanged", (data) => {
            console.log("Notified in VM via signalr.", data);
        });
        
        //... or function name.
        this.signalRClient.registerCallback("notificationHub", "somethingChanged", this.somethingChanged);
    }

    somethingChanged(data) {
        console.log("Notified in VM, somethingChanged, via signalr.", data);
    }
}