###### tags: `Angular` `SignalR`
# 🌝[T]SignalR
## 前言
SignalR適用開發任何即時(real-time)類型的應用程式。
但在HTML5之前,開發者需要採用Client端主動發出請求,Server端被動處理的方式 :
>Polling
* 傳統HTTP作法。
* 若知道伺服器資料更新的頻率,能使用此方法實現同步更新,但若是在不知道伺服器資料何時更新的情況下,為達到同步更新的效果不斷傳送Request,將造成網路資源浪費。
>Comet
* 為[解決Polling的弊端](https://www.iteye.com/blog/sp42-141335)。而出現
* Client端會發出一個長時間等待的Request,當伺服器有資料,就立即斷線再發送新的Request,主要兩種實作方式:
1.long-polling 長時間輪詢
* **特點** : 等到 拿到資料/過請求時間 再重新發送,能改善效率並減少流量浪費
* **缺點** : 若傳送量大,可能會傳送不完全

2.streaming 串流
* **特點** :
Server端與Client端建立一條持續的連線,為了保持連線,Server端會隔斷時間發送Response給Client。
Server端會將資料傳入[iframe](https://www.webdesigns.com.tw/html_iframe.asp),藉由iframe內的JavaScript更新Client端的頁面。
* **缺點** : 傳送資料時,訊息會被包起來,道防火牆前會被放進暫存器(Buffer),傳輸的速率會降低。

* Comet 雖然解決雙向溝通的問題,但為了達到長時間的連線,花費太多效能控制連線的生命週期,導致資料傳輸的效率降低。

`💡HTML5開始提供瀏覽器與伺服器全雙工通訊的WebSocket(與HTTP不同的協定)`
>WebSocket
* HTML5標準中的新網頁傳輸方式,可在一條連線上提供全雙工、雙向資料傳輸,也解決Comet架構容易出錯的問題(例如:Client端網路不穩,連線突然中斷,傳送中的訊息就會不見),架構也比以往簡單。

[淺談 Polling, Comet, Websocket](http://f2e-veru.com/%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98/%E6%B7%BA%E8%AB%87%20Polling,%20Comet,%20Websocket/)
[Best practices of building data streaming API](https://www.slideshare.net/kslisenko/best-practices-of-building-server-to-client-data-streaming)
[WebSocket新一代網路傳輸技術](http://www.syscom.com.tw/ePaper_New_Content.aspx?id=368&EPID=194&TableName=sgEPArticle)
[iframe介紹 深入了解/iframe有那些缺點/iframe傳值問題](https://medium.com/@small2883/iframe%E4%BB%8B%E7%B4%B9-%E6%B7%B1%E5%85%A5%E4%BA%86%E8%A7%A3-iframe%E6%9C%89%E9%82%A3%E4%BA%9B%E7%BC%BA%E9%BB%9E-iframe%E5%82%B3%E5%80%BC%E5%95%8F%E9%A1%8C-579e6c113436)
[網頁設計之HTML iframe框架](https://www.webdesigns.com.tw/html_iframe.asp)
---
## 什麼是SignalR?
[SignalR](https://github.com/signalr)是ASP.NET平台,提供能簡化新增即時web功能的應用程式開放原始碼程式庫。
SignalR提供一個簡單使用的高階API,實現伺服器與瀏覽器間的遠程程序呼叫(RPC,Remote Procedure Call),在這架構下伺服器端以.NET開發,瀏覽器主以JavaSricpt開發。
SignalR會針對目前執行的瀏覽器進行判斷,找到與伺服器間最適合的建立連線方式,SignalR 會優先的選用 WebSocket 技術與伺服器溝通,當然伺服器上的通訊協定管理方式也內含在 ASP.NET SignalR 的組件庫中,開發人員就不需要針對目前走的是 WebSocket、Comet 還是 Polling 進行特別的處理,所有的 code 都透過 ASP.NET SignalR高階的 API 進行訊息傳遞。

應用面: 遊戲、買賣(股票、拍賣)、地圖(GPS)、監控(儀表板、天氣、旅遊警告)、聊天室、
[SignalR 再次超越你對 Web 的想像 – 建立即時互動的 Web](https://blogs.msdn.microsoft.com/msdntaiwan/2013/09/09/signalr-web-web/)
---
## 如何實現SignalR?
後端會實作一個SignalR Hub類別,Hub負責產生與前端JavaScript綁定的相關定義。
後端要傳送訊息到瀏覽器,自訂Hub類別方法ex:Send,用Clients屬性決定訊息發布範圍(全部、群組...),

前端也必須要實作與後端建立連線,以下部分皆是與前端相關的實作。
[A1 SignalR 實作參考文章(SignalR Clients的部分)](https://blog.johnwu.cc/article/asp-net-core-angular-4-%E6%95%99%E5%AD%B8-signalr.html)
### step1. 安裝signalr套件
在Angular CLI 輸入 `npm install signalr`

安裝完,node_modules底下就可以看到signalr套件了(若沒看到`ng build` 就有了)

再到angular.json內添加signalr套件路徑
```json
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
]
```
### step2. 新增SignalrService
因為signalr是透過jQuery調用,為了方便使Angular使用新增一個service來包裝,下面只放了一些最基本的方法`開啟連線/接收訊息/傳送訊息`,有其他的需求要自己來。
#程式碼需依自己需求調整
signalr.service.ts
```typescript=
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...');
}
/* #A1 ---str*/
// this.hubConnection = $.hubConnection();
// this.hubConnection.url = `//${window.location.host}/signalr`;
this.hubConnection = $.hubConnection('https://a1notice-staging.azurewebsites.net/');
/* #A1 ---end*/
}
/**開啟連線*/
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({ withCredentials: false })
.done(() => {
// 成功連線
// #可以在這邊用invoke給後端token之類的,做訊息推送群組
/* #A1 ---str*/
this.hubProxy.invoke('registered', this.test.TenantID + '-' + this.test.ID, 'A1Web-testing').done(() => {
this.hubProxy.invoke('joinGroup', this.test.TenantID + '-' + this.test.ID, this.test.Name).done(() => {
this.hubProxy.invoke('UpdateUserInfo', '').done(() => {});
});
});
/* #A1 ---end*/
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);
}
}
```
接下來設定$(jquery)
1. 在Angular CLI輸入`npm i @types/jquery`,並在tsconfig.json檔案的types加上jquery
```json
"types":[
"jquery"
]
```
2. Angular CLI輸入`npm install jquery`,並在angular.json內添加jquery套件路徑
```json
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/signalr/jquery.signalR.js",
]
```
> 以上步驟都完成後,現在就可以在Angular裡使用signalR了!
### step3. 使用SignalR
這部分我是寫在AppComponent裡面,看個人需求調整。
```typescript
constructor(
private signalrService: SignalrService,
) {
// 啟動 SignalR Client 跟 Server 連線 名稱#A1('spokesman')要跟後端一致
this.signalrService.start('spokesman', true).subscribe(
(connectionStatus: ConnectionStatus) => {
console.log(`[signalr] start() - done - ${connectionStatus}`);
},
(error: any) => {
console.log(`[signalr] start() - fail - ${error}`);
});
// 監聽發生錯誤時的事件
this.signalrService.error.subscribe((error: any) => {
console.log(`[signalr] error - ${error}`);
});
// 監聽 Server 送來的事件,名稱#A1('addMessage')要跟 ChatHub 對應
this.signalrService.addEventListener('addMessage').subscribe(
(message) => {
// 收到事件
// #處理訊息,message是後端傳送的訊息
});
}
```
> 接下來就是來查看是否成功接送訊息。
### step4. 透過開發者工具觀察連線
透過DevTool(F12)可以看到webSocket建立好的連線
**`💡 紅色的箭頭是伺服器端傳送的資料、綠色是使用者端傳送的資料`**

如果接送的訊息無誤恭喜你實作成功啦~~
:::success
💡上面的程式碼內註解有用#A1是我使用A1專案現有的signalr來測試的
:::
## 錯誤集
### #jQuery問題
:::warning
**⚡Cannot find name '$' / $ is not defined /**
:::
:::info
⛏ 補給站
[Why does JQuery have dollar signs everywhere?](https://stackoverflow.com/questions/10787342/why-does-jquery-have-dollar-signs-everywhere)
```javascript
// These are the same barring your using noConflict (more below)
var divs = $("div"); // Find all divs
var divs = jQuery("div"); // Also find all divs, because
console.log($ === jQuery); // "true"
```
>$是jQuery的簡寫
:::
會出現此錯誤是因為$是js的語法,所以我們必須做一些設定
`npm i @types/jquery` : package.json > dependencies 會自動添加`"@types/jquery": "^3.3.31",`
此外,在tsconfig.json檔案的types加上jquery(此步驟還要再查是什麼意義)
```json
"types":[
"jquery"
]
```
設定完成後,會發現angular中她還會報紅,但她是可以正常使用的,那想在angular中想要他不報紅可以加上`declare var $: any`。
### #hubConnection錯誤
:::warning
**⚡$.hubConnection' are not defined / Property 'hubConnection' does not exist on type 'JQueryStatic'**
:::
因為hubConnection是使用/node_modules/signalr/jquery.signalR.js裡的function,所以出現此$.hubConnenction的錯誤表示還沒有下載signalR的程式庫。
`npm install jquery` : package.json > deoendencies會自動添加`"jquery": "^3.4.1"`
(這不太確定是否必要,我目前在都先有安裝,之後在多次測試)
`npm install signalr` : package.json > deoendencies會自動添加`"signalr": "^2.4.1"`
cli只有幫我們在package.json添加依賴,要打包使用的.JS檔還是要自己手動添加進angular-cli.json>scripts
```json
"scripts": [
"../node_modules/jquery/dist/jquery.min.js",
"../node_modules/signalr/jquery.signalR.js",
]
```
scripts添加時若出現此錯誤,依下面修正
:::warning
⚡[ENOENT: no such file or directory](https://stackoverflow.com/questions/50366935/enoent-no-such-file-or-directory-for-node-modules-jquery-dist-jquery-min-js)
:::
> ```json
> "scripts": [
> "node_modules/jquery/dist/jquery.min.js",
> "node_modules/signalr/jquery.signalR.js",
> ]
> ```
> * 先添加"node_modules/signalr/jquery.signalR.js", `ng build`
> * 再添加"node_modules/jquery/dist/jquery.min.js", `ng build`
> * `注意!` jquery.min.js必須在jquery.signalR.js上面。
>
> 我也不知道為什麼不能一次加上去就好QAQ[name=Ruby Lin]
### #CORS policy問題
:::warning
**⚡XMLHttpRequest at `'https://...'` from origin `'http://localhost:4200'` has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.**
:::
將start的寫法做點修正: `在start()裡加{withCredentials: false}`
`🤔不過不確定這方法是否有其他影響`
```typescript
start(hubName: string, debug: boolean = false): Observable<ConnectionStatus>{
this.hubProxy = this.hubConnection.createHubProxy('spokesman');
console.log('signalR start', this.hubProxy);
const errorSubject = new Subject<any>();
this.error = errorSubject.asObservable();
this.hubConnection.error((error: any) => errorSubject.next(error));
const subject = new Subject<ConnectionStatus>();
const observer = subject.asObservable();
/*start()裡加{withCredentials: false} ---str*/
//this.hubConnection.start()
this.hubConnection.start({ withCredentials: false })
/*start()裡加{withCredentials: false} ---end*/
.done(() => subject.next(ConnectionStatus.Connected))
.fail((error: any) => subject.error(error));
return observer;
}
```
:::spoiler 補給包
[CORS policy issue with angular 7 and ASP.NET core 2.2 using SIGNAL R](https://stackoverflow.com/questions/54808822/cors-policy-issue-with-angular-7-and-asp-net-core-2-2-using-signal-r)
[CORS and the Access-Control-Allow-Origin response header](https://portswigger.net/web-security/cors/access-control-allow-origin)
[跨來源資源共用(CORS)](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS)
此篇超推薦👍🏻 [輕鬆理解Ajax與跨來源請求](https://blog.techbridge.cc/2017/05/20/api-ajax-cors-and-jsonp/)
:::