【问题标题】:How to use SignalR with Angular 2?如何在 Angular 2 中使用 SignalR?
【发布时间】:2016-05-01 03:23:10
【问题描述】:

如何在 Angular 2 中使用 SignalR?

从 SignalR 接收数据时如何手动运行更改检测?

【问题讨论】:

  • 您好,您的问题有点模糊,因此很容易被版主关闭。请重新表述您的问题。例如,如果您想了解变更检测,请提供一些不适合您或您希望获得支持的示例代码。

标签: signalr angular


【解决方案1】:

就示例而言,可能还没有。欢迎来到框架的开始。但一定要随着时间的推移不断检查,因为随着流行度和采用率的提高,肯定会有很多例子。

就运行变化检测而言,这是一个非常模糊的问题,因为 angular2 的变化检测现在非常不同,并且得到了很大改进。

我的方法是让 angular2 处理它,根本不触发手动更改检测,因为大多数时候 Angular2 会接受更改并重新渲染视图。

如果这不起作用,那么下一步是在NgZone 上触发.run()

示例:

import {NgZone, Component} from 'angular2/core';

@Component({...})
export class MyComponent{
  myProperty: string = 'Hello';
  constructor(myService: MyService, ngZone: NgZone){}

  doSomething(){
    this.myService.doSomething().then(x => {
      this.ngZone.run(() => {
        this.myProperty = x;
      });
    });
  }
}

不过,我再次发现,即使使用异步代码,angular2 通常也会在完全不使用 ngZone 的情况下进行更改。

【讨论】:

  • 嗨。是的,新兴的框架有点让人头疼。但我有一种感觉,就像你说的,在这个月的过程中,Angular 2 将呈指数级增长。谢谢你。将尝试使用 NgZone 制作一个简单的应用程序。
【解决方案2】:

我最近写了一篇文章,演示了一种使用“通道/事件”模型集成 Angular 2 和 SignalR 的方法:

https://blog.sstorie.com/integrating-angular-2-and-signalr-part-2-of-2/

我认为仅仅链接到另一个站点是不合适的,所以这里是暴露 SignalR 的 Angular 2 服务的核心:

import {Injectable, Inject} from "angular2/core";
import Rx from "rxjs/Rx";

/**
 * When SignalR runs it will add functions to the global $ variable 
 * that you use to create connections to the hub. However, in this
 * class we won't want to depend on any global variables, so this
 * class provides an abstraction away from using $ directly in here.
 */
export class SignalrWindow extends Window {
    $: any;
}

export enum ConnectionState {
    Connecting = 1,
    Connected = 2,
    Reconnecting = 3,
    Disconnected = 4
}

export class ChannelConfig {
    url: string;
    hubName: string;
    channel: string;
}

export class ChannelEvent {
    Name: string;
    ChannelName: string;
    Timestamp: Date;
    Data: any;
    Json: string;

    constructor() {
        this.Timestamp = new Date();
    }
}

class ChannelSubject {
    channel: string;
    subject: Rx.Subject<ChannelEvent>;
}

/**
 * ChannelService is a wrapper around the functionality that SignalR
 * provides to expose the ideas of channels and events. With this service
 * you can subscribe to specific channels (or groups in signalr speak) and
 * use observables to react to specific events sent out on those channels.
 */
@Injectable()
export class ChannelService {

    /**
     * starting$ is an observable available to know if the signalr 
     * connection is ready or not. On a successful connection this
     * stream will emit a value.
     */
    starting$: Rx.Observable<any>;

    /**
     * connectionState$ provides the current state of the underlying
     * connection as an observable stream.
     */
    connectionState$: Rx.Observable<ConnectionState>;

    /**
     * error$ provides a stream of any error messages that occur on the 
     * SignalR connection
     */
    error$: Rx.Observable<string>;

    // These are used to feed the public observables 
    //
    private connectionStateSubject = new Rx.Subject<ConnectionState>();
    private startingSubject = new Rx.Subject<any>();
    private errorSubject = new Rx.Subject<any>();

    // These are used to track the internal SignalR state 
    //
    private hubConnection: any;
    private hubProxy: any;

    // An internal array to track what channel subscriptions exist 
    //
    private subjects = new Array<ChannelSubject>();

    constructor(
        @Inject(SignalrWindow) private window: SignalrWindow,
        @Inject("channel.config") private channelConfig: ChannelConfig
    ) {
        if (this.window.$ === undefined || this.window.$.hubConnection === undefined) {
            throw new Error("The variable '$' or the .hubConnection() function are not defined...please check the SignalR scripts have been loaded properly");
        }

        // Set up our observables
        //
        this.connectionState$ = this.connectionStateSubject.asObservable();
        this.error$ = this.errorSubject.asObservable();
        this.starting$ = this.startingSubject.asObservable();

        this.hubConnection = this.window.$.hubConnection();
        this.hubConnection.url = channelConfig.url;
        this.hubProxy = this.hubConnection.createHubProxy(channelConfig.hubName);

        // Define handlers for the connection state events
        //
        this.hubConnection.stateChanged((state: any) => {
            let newState = ConnectionState.Connecting;

            switch (state.newState) {
                case this.window.$.signalR.connectionState.connecting:
                    newState = ConnectionState.Connecting;
                    break;
                case this.window.$.signalR.connectionState.connected:
                    newState = ConnectionState.Connected;
                    break;
                case this.window.$.signalR.connectionState.reconnecting:
                    newState = ConnectionState.Reconnecting;
                    break;
                case this.window.$.signalR.connectionState.disconnected:
                    newState = ConnectionState.Disconnected;
                    break;
            }

            // Push the new state on our subject
            //
            this.connectionStateSubject.next(newState);
        });

        // Define handlers for any errors
        //
        this.hubConnection.error((error: any) => {
            // Push the error on our subject
            //
            this.errorSubject.next(error);
        });

        this.hubProxy.on("onEvent", (channel: string, ev: ChannelEvent) => {
            //console.log(`onEvent - ${channel} channel`, ev);

            // This method acts like a broker for incoming messages. We 
            //  check the interal array of subjects to see if one exists
            //  for the channel this came in on, and then emit the event
            //  on it. Otherwise we ignore the message.
            //
            let channelSub = this.subjects.find((x: ChannelSubject) => {
                return x.channel === channel;
            }) as ChannelSubject;

            // If we found a subject then emit the event on it
            //
            if (channelSub !== undefined) {
                return channelSub.subject.next(ev);
            }
        });

    }

    /**
     * Start the SignalR connection. The starting$ stream will emit an 
     * event if the connection is established, otherwise it will emit an
     * error.
     */
    start(): void {
        // Now we only want the connection started once, so we have a special
        //  starting$ observable that clients can subscribe to know know if
        //  if the startup sequence is done.
        //
        // If we just mapped the start() promise to an observable, then any time
        //  a client subscried to it the start sequence would be triggered
        //  again since it's a cold observable.
        //
        this.hubConnection.start()
            .done(() => {
                this.startingSubject.next();
            })
            .fail((error: any) => {
                this.startingSubject.error(error);
            });
    }

    /** 
     * Get an observable that will contain the data associated with a specific 
     * channel 
     * */
    sub(channel: string): Rx.Observable<ChannelEvent> {

        // Try to find an observable that we already created for the requested 
        //  channel
        //
        let channelSub = this.subjects.find((x: ChannelSubject) => {
            return x.channel === channel;
        }) as ChannelSubject;

        // If we already have one for this event, then just return it
        //
        if (channelSub !== undefined) {
            console.log(`Found existing observable for ${channel} channel`)
            return channelSub.subject.asObservable();
        }

        //
        // If we're here then we don't already have the observable to provide the
        //  caller, so we need to call the server method to join the channel 
        //  and then create an observable that the caller can use to received
        //  messages.
        //

        // Now we just create our internal object so we can track this subject
        //  in case someone else wants it too
        //
        channelSub = new ChannelSubject();
        channelSub.channel = channel;
        channelSub.subject = new Rx.Subject<ChannelEvent>();
        this.subjects.push(channelSub);

        // Now SignalR is asynchronous, so we need to ensure the connection is
        //  established before we call any server methods. So we'll subscribe to 
        //  the starting$ stream since that won't emit a value until the connection
        //  is ready
        //
        this.starting$.subscribe(() => {
            this.hubProxy.invoke("Subscribe", channel)
                .done(() => {
                    console.log(`Successfully subscribed to ${channel} channel`);
                })
                .fail((error: any) => {
                    channelSub.subject.error(error);
                });
        },
            (error: any) => {
                channelSub.subject.error(error);
            });

        return channelSub.subject.asObservable();
    }

    // Not quite sure how to handle this (if at all) since there could be
    //  more than 1 caller subscribed to an observable we created
    //
    // unsubscribe(channel: string): Rx.Observable<any> {
    //     this.observables = this.observables.filter((x: ChannelObservable) => {
    //         return x.channel === channel;
    //     });
    // }

    /** publish provides a way for calles to emit events on any channel. In a 
     * production app the server would ensure that only authorized clients can
     * actually emit the message, but here we're not concerned about that.
     */
    publish(ev: ChannelEvent): void {
        this.hubProxy.invoke("Publish", ev);
    }

}

然后组件可以通过订阅(不是 rxjs 意义上的......)特定通道并响应发出的特定事件来使用此服务:

import {Component, OnInit, Input} from "angular2/core";
import {Http, Response} from "angular2/http";
import Rx from "rxjs/Rx";

import {ChannelService, ChannelEvent} from "./services/channel.service";

class StatusEvent {
    State: string;
    PercentComplete: number;
}

@Component({
    selector: 'task',
    template: `
        <div>
            <h4>Task component bound to '{{eventName}}'</h4>
        </div>

        <div class="commands">
            <textarea 
                class="console"
                cols="50" 
                rows="15"
                disabled
                [value]="messages"></textarea> 

            <div class="commands__input">
                <button (click)="callApi()">Call API</button>
            </div>
        </div>
    `
})
export class TaskComponent implements OnInit {
    @Input() eventName: string;
    @Input() apiUrl: string;

    messages = "";

    private channel = "tasks";

    constructor(
        private http: Http,
        private channelService: ChannelService
    ) {

    }

    ngOnInit() {
        // Get an observable for events emitted on this channel
        //
        this.channelService.sub(this.channel).subscribe(
            (x: ChannelEvent) => {
                switch (x.Name) {
                    case this.eventName: { this.appendStatusUpdate(x); }
                }
            },
            (error: any) => {
                console.warn("Attempt to join channel failed!", error);
            }
        )
    }


    private appendStatusUpdate(ev: ChannelEvent): void {
        // Just prepend this to the messages string shown in the textarea
        //
        let date = new Date();
        switch (ev.Data.State) {
            case "starting": {
                this.messages = `${date.toLocaleTimeString()} : starting\n` + this.messages;
                break;
            }

            case "complete": {
                this.messages = `${date.toLocaleTimeString()} : complete\n` + this.messages;
                break;
            }

            default: {
                this.messages = `${date.toLocaleTimeString()} : ${ev.Data.State} : ${ev.Data.PercentComplete} % complete\n` + this.messages;
            }
        }
    }

    callApi() {
        this.http.get(this.apiUrl)
            .map((res: Response) => res.json())
            .subscribe((message: string) => { console.log(message); });
    }
}

我尝试将 SignalR 概念映射到 observables,但我仍在学习如何有效地使用 RxJS。无论如何,我希望这有助于展示这在 Angular 2 应用程序的上下文中是如何工作的。

【讨论】:

  • 这一行断码-> this.startingSubject.next(),等待一个参数,是否为空?
  • @Sam Storie:您能否为您的包装器创建一个node package(及其配置和 api),以便将来可以轻松地为任何人使用?
  • @SyedAliTaqi 我已经考虑过了,但老实说,我不确定我是否有时间/精力来维护这样的包。我很犹豫是否要扔掉一些东西,让它流连忘返。
  • @SamStorie:嗯,总比没有好,我想。无论如何,如果您将来这样做,请务必更新我。 P.S:你的博客真的很有帮助,非常感谢:)
  • 我不断收到 jquery.hubConnection() 未定义。我添加了对 jquery 输入信号器的引用。似乎它只是调用 jquery 而不是 signalr。信号器中没有导出 $ 。你不应该调用 SignalR 接口而不是 jquery 吗?很抱歉,我重复发布此评论。
【解决方案3】:

您没有指定用于开发 Angular 2 应用程序的语法。

我假设您使用的是 typescript

一种方法是使用绝对类型文件。

1 - 您需要下载绝对类型化的 JQuery:

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/jquery/jquery.d.ts

2 - 在此之后,下载一个确定类型的 SignalR:

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/signalr/signalr.d.ts

3 - 在组件中添加 JQuery 引用:

/// <reference path="../jquery.d.ts" />

4 - 现在,您可以使用 intelissense 调用 SignalR 方法。但是您需要使用后期绑定方法:

var connection = $.hubConnection(); var proxy = connection.createHubProxy(proxy.on("newOrder", (order) => console.log(order)); connection.start();

【讨论】:

  • 后期绑定方法是什么意思?
  • 当你启动你的 signalR 没有生成的代理asp.net/signalr/overview/guide-to-the-api/…
  • @RafaelMiceli 我不断收到 jquery.hubConnection() 未定义。我添加了对 jquery 输入信号器的引用。似乎它只是调用 jquery 而不是 signalr。信号器中没有导出 $ 。你不应该调用 SignalR 接口而不是 jquery 吗?
【解决方案4】:

您也可以尝试使用 ng2-signalr

npm install ng2-signalr --save
  • 使用区域处理 ng2 更改检测
  • 允许使用 rxjs 监听您的服务器事件。

这是source的链接。

【讨论】:

  • 您的实现的问题在于它不是服务。您不能在一处创建连接,然后在多个组件中订阅服务器事件。每次都必须断开连接并重新连接
  • 您好,您的实现与信号器核心兼容吗?
猜你喜欢
  • 2017-05-13
  • 1970-01-01
  • 2018-04-10
  • 2016-11-22
  • 2017-09-13
  • 2016-05-25
  • 2017-05-19
  • 2017-02-17
  • 1970-01-01
相关资源
最近更新 更多