本篇將介紹 Angular 4 跟 ASP.NET Core 透過 SignalR 的互動,範例是做一個簡單的即時聊天室。
透過 TypeScript 把 jQuery 及 SignalR 包裝成 Injectable class,讓使用更為便利。
程式碼延續前兩篇的範例:
ASP.NET Core + Angular 4 教學 - 從無到有
ASP.NET Core + Angular 4 教學 - Webpack打包
安裝 NuGet 套件
ASP.NET Core SignalR 的套件名稱為 Microsoft.AspNetCore.SignalR,是一個非常新的套件,還沒正式發布。在預設的 nuget.org
搜尋是找不到它滴!
首先我們先打開 NuGet 管理員,加入新的套件來源 https://dotnet.myget.org/f/aspnetcore-dev/api/v3/index.json,如下:
新增來源後,套件管理員的右上角就可以切換來源了。選擇剛剛新增的 asp.net core
來源,並勾選搶鮮版(因為還沒正式發佈),然後就可以搜尋到 Microsoft.AspNetCore.SignalR,但我們實際要安裝的是 Microsoft.AspNetCore.SignalR.Server。
切換回 nuget.org
,搜尋 Microsoft.AspNetCore.WebSockets.Server 並安裝。跟以前有點不一樣,以前安裝完 SignalR 套件後,會一併幫你把 WebSockets 的功能包含在裡面。
SignalR 有四種連線模式,就算不用 WebSockets 也能透過 long polling 連線。但目前版本 long polling 極度不穩定,連線速度超級慢,而且會一直斷線。
安裝 npm 套件
SignalR 相依於 jQuery,所以兩個都要安裝,指令如下:
1
| npm install --save signalr jquery
|
SignalR Server
註冊 SignalR 服務
在 Startup.cs 的 ConfigureServices 加入 SignalR 服務。
在 Configure 加入啟用 WebSockets 及 SignalR 的 middleware。
Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12
| public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); }
public void Configure(IApplicationBuilder app) { app.UseWebSockets(); app.UseSignalR(); }
|
建立 Hub
Hubs\ChatHub.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Hubs; using System; using System.Threading.Tasks;
namespace MyWebsite.Hubs { [HubName("chathub")] public class ChatHub : Hub<IChatHub> { public override async Task OnConnected() { await Clients.All.ServerMessage($"[{DateTime.Now.ToString("HH:mm:ss")}] {Context.ConnectionId} joined"); }
public override async Task OnDisconnected(bool stopCalled) { await Clients.All.ServerMessage($"[{DateTime.Now.ToString("HH:mm:ss")}] {Context.ConnectionId} left"); }
public Task ClientMessage(dynamic data) { string name = data.name.Value; string message = data.message.Value; return Clients.All.ServerMessage($"[{DateTime.Now.ToString("HH:mm:ss")}] {name}: {message}"); } } }
|
Hub 中用到 Clients 的泛型是 dynamic。如果沒有自訂介面,開發時不好使用 Clients,因為不會自動跳出可用方法,也可能因為打錯字,在執行階段才發現錯誤。
Hubs\IChatHub.cs
1 2 3 4 5 6 7 8 9 10
| using System.Threading.Tasks;
namespace MyWebsite.Hubs { public interface IChatHub { Task ServerMessage(string message); } }
|
SignalR Client
隨著範例增加,檔案結構有點小亂,我重新把範例中的結構調整了一下,方便之後繼續擴充。之前有說過的部分都會略過不說明,請直接參考底部的附件。
SignalRService
因為 SignalR 是透過 jQuery 調用,為了方便在 Angular 中使用,我建立了一個 SignalRService 包裝 SignalR。
shared\services\signalr.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import { Injectable, Inject } from "@angular/core"; import { Subject, Observable } from "rxjs";
declare var $: any;
export enum ConnectionStatus { Connected = 1, Disconnected = 2, Error = 3 }
@Injectable() export class SignalRService { private hubConnection: any; private hubProxy: any; error: Observable<any>;
constructor() { if ($ === undefined || $.hubConnection === undefined) { throw new Error("The '$' or the '$.hubConnection' are not defined..."); } this.hubConnection = $.hubConnection(); this.hubConnection.url = `//${window.location.host}/signalr`; }
start(hubName: string, debug: boolean = false): Observable<ConnectionStatus> { this.hubConnection.logging = debug; this.hubProxy = this.hubConnection.createHubProxy(hubName);
let errorSubject = new Subject<any>(); this.error = errorSubject.asObservable(); this.hubConnection.error((error: any) => errorSubject.next(error));
let subject = new Subject<ConnectionStatus>(); let observer = subject.asObservable(); this.hubConnection.start() .done(() => subject.next(ConnectionStatus.Connected)) .fail((error: any) => subject.error(error)); return observer; }
addEventListener(eventName: string): Observable<any> { let subject = new Subject<any>(); let observer = subject.asObservable(); this.hubProxy.on(eventName, (data: any) => subject.next(data)); return observer; }
invoke(eventName: string, data: any): void { this.hubProxy.invoke(eventName, data); } }
|
載入套件
在 NgModule 的 providers 注入 SignalRService。
app\main.ts
1 2 3 4 5 6 7 8 9 10
| import { SignalRService } from "./shared/services/signalr.service";
@NgModule({ providers: [ SignalRService ], }) export class AppModule { }
|
app\bundle-vendors.ts 加入以下程式碼:
1 2 3
| require("jquery"); require("signalr");
|
Webpack 設定
為了讓 jQuery 能在 Angular 中使用,所以在 plugins 加入了 ProvidePlugin。如下:
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11
| module.exports = { plugins: [ new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" }) ] }
|
Angular
app\components\chat.component.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <h2>Chat rooms</h2>
<div #records style="height:200px; overflow-y: scroll;"> </div>
<div> <label>Name</label> <div><input type="text" [(ngModel)]="name" /></div> </div> <div> <label>Message</label> <div><input type="text" [(ngModel)]="message" /></div> </div> <div> <input type="button" value="Send" (click)="send()" /> <input type="button" value="Clear" (click)="clear()" /> </div>
|
app\components\chat.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import { Component, ViewChild, ElementRef } from "@angular/core"; import { SignalRService, ConnectionStatus } from "../shared/services/signalr.service"; import { ResultModel } from "../shared/models/result.model";
@Component({ template: require("./chat.component.html") }) export class ChatComponent { message: string; name: string; @ViewChild("records") records: ElementRef;
constructor(private signalrService: SignalRService) { signalrService.start("chathub", true).subscribe( (connectionStatus: ConnectionStatus) => { console.log(`[signalr] start() - done - ${connectionStatus}`); }, (error: any) => { console.log(`[signalr] start() - fail - ${error}`); }); signalrService.error.subscribe((error: any) => { console.log(`[signalr] error - ${error}`); }); signalrService.addEventListener("ServerMessage").subscribe( (message: string) => { this.records.nativeElement.innerHTML += `<p>${message}<p>`; }); }
send(): void { if (this.name && this.message) { this.signalrService.invoke("ClientMessage", { name: this.name, message: this.message }); } }
clear(): void { this.records.nativeElement.innerHTML = ""; } }
|
執行結果
範例程式碼
asp-net-core-angular-signalr
參考
ASP.NET Core SignalR