3-websocket-client.md

May 19, 2025 · View on GitHub

4.3 製作 WebSocket 用戶端

現在是製作一個 Frontend 的時機了,透過一個簡單的 Frontend 來測試目前的實作成果。我們要 Frontend 的文件,命名為 client.html。實作 client.html 前,瀏覽器必須支援 HTML5 的 WebSocket 標準。新版的瀏覽器皆支援 WebSocket,本書皆使用 Chrome 18+ 進行測試。

實作 client.html

現在,我們可以不考慮 HTML5 的相容性問題,假設使用者的瀏覽器都支援 WebSocket。不過,實作時仍可考慮加入一段檢查程式碼,以提示使用者目前的瀏覽器是否支援 WebSocket。

 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <meta charset="UTF-8">
 5   <title>NoChat Client</title>
 6 </head>
 7 <body>
 8   <div id="header"></div>
 9   <input type="text" id="messageBox" placeholder="輸入訊息...">
10   <button onclick="WebSocketTest();">Get News</button>
11   <button onclick="sendMessage();">送出訊息</button>
12 
13   <script type="text/javascript">  
14   var ws;

15   function WebSocketTest() {
16     if ("WebSocket" in window) {
17       // 建立 WebSocket 連線
18       ws = new WebSocket("ws://svn.moko365.com:8080/", "echo-protocol");

19       ws.onopen = function(evt) {
20         console.log("WebSocket opened.");
21         document.getElementById('header').innerText = 'WebSocket 連線成功';
22       };

23       ws.onmessage = function(evt) {
24         console.log("Received: " + evt.data);
25         alert("接收到訊息:" + evt.data);
26       };

27       ws.onerror = function(evt) {
28         console.error("WebSocket error:", evt);
29         alert("WebSocket 發生錯誤,請確認伺服器是否啟動");
30       };

31       ws.onclose = function(evt) {
32         alert("WebSocket 連線已關閉。");
33       };

34     } else {
35       // 不支援 WebSocket
36       alert("WebSocket NOT supported by your Browser!");
37     }
38   }

39   function sendMessage() {
40     const msg = document.getElementById('messageBox').value;
41     if (ws && ws.readyState === WebSocket.OPEN) {
42       ws.send(msg);
43       console.log("已送出訊息:" + msg);
44     } else {
45       alert("WebSocket 尚未建立連線。");
46     }
47   }
48   </script>
49 </body>
50 </html>

說明如下:

  • 第 15 行:檢查瀏覽器是否支援 WebSocket
  • 第 18 行:與伺服器建立 WebSocket 連線,第一個參數是伺服器位址,第二個參數是自定協定(protocol)
  • 第 19 行:WebSocket 成功連線後,觸發 onopen 回呼函數,並更新畫面
  • 第 23 行:伺服器傳送訊息時,觸發 onmessage,這是實作 Data Push 的核心
  • 第 27 行:當 WebSocket 發生錯誤時觸發,便於除錯與提示
  • 第 31 行:WebSocket 關閉時,觸發 onclose 回呼函數
  • 第 39 行:sendMessage() 函數將訊息送給伺服器,作為簡易的訊息測試按鈕

補充說明:

  • WebSocket 物件是瀏覽器內建的 API,類似於瀏覽器內的「TCP socket 實作」,它讓前端程式能與後端進行雙向、持續的資料通訊。
  • onopen 代表的是連線初始化的成功提示。它常見用途是:更新 UI 狀態,顯示已連線成功、解除送出按鈕的禁用等。
  • onmessage 是核心。資料推送模型(Data Push Model)建立在此回呼函數內,它會攔截並處理從伺服器推送來的所有訊息。
  • onerror 提供了基本的錯誤處理能力,是前端與後端「協定容錯」的重要檢查點。
  • onclose 表示該 WebSocket 連線生命周期終結,開發者可藉此釋放資源或提示使用者重新連線。
  • ws.send(msg) 代表 Client-to-Server 的訊息送出,會轉為 UTF-8 格式推向 Server。

這段程式碼為 WebSocket 前端測試的基礎,整合 UI 顯示與訊息推送回應的流程。使用瀏覽器打開 client.html,按下按鈕後,將觸發 WebSocketTest() 函數,並可輸入訊息測試傳送功能。

圖 4.1:範例 01-ws-open.html


4.3.1 使用 ES6 重構 client.html

下面是以 ES6 語法改寫的版本,更加符合現代 JavaScript 的開發標準。

 1 <!DOCTYPE html>
 2 <html lang="zh-Hant">
 3 <head>
 4   <meta charset="UTF-8">
 5   <title>NoChat Client (ES6)</title>
 6 </head>
 7 <body>
 8   <div id="header"></div>
 9   <input type="text" id="messageBox" placeholder="輸入訊息...">
10   <button id="connectBtn">建立連線</button>
11   <button id="sendBtn">送出訊息</button>
12 
13   <script type="module">
14   let ws;
15 
16   const connectBtn = document.getElementById('connectBtn');
17   const sendBtn = document.getElementById('sendBtn');

18   connectBtn.addEventListener('click', () => {
19     if ('WebSocket' in window) {
20       ws = new WebSocket('ws://svn.moko365.com:8080/', 'echo-protocol');
21 
22       ws.onopen = () => {
23         console.log('WebSocket opened.');
24         document.getElementById('header').textContent = 'WebSocket 連線成功';
25       };

26       ws.onmessage = ({ data }) => {
27         console.log(`接收到訊息:${data}`);
28         alert(`接收到訊息:${data}`);
29       };

30       ws.onerror = (e) => {
31         console.error('WebSocket error:', e);
32         alert('WebSocket 發生錯誤,請確認伺服器是否啟動');
33       };

34       ws.onclose = () => {
35         alert('WebSocket 連線已關閉。');
36       };
37     } else {
38       alert('此瀏覽器不支援 WebSocket');
39     }
40   });

41   sendBtn.addEventListener('click', () => {
42     const msg = document.getElementById('messageBox').value;
43     if (ws && ws.readyState === WebSocket.OPEN) {
44       ws.send(msg);
45       console.log(`已送出訊息:${msg}`);
46     } else {
47       alert('WebSocket 尚未建立連線。');
48     }
49   });
50   </script>
51 </body>
52 </html>

差異說明:

特性ES5ES6 改寫
變數宣告varlet / const 用於區塊作用域
事件綁定HTML 屬性 (onclick)addEventListener() 模式
字串處理字串相加模板字串(Template literals) ${}
函數語法function 關鍵字箭頭函數 =>

使用 ES6 可提升語意清晰度與維護性,也符合現代瀏覽器的執行效能與可讀性標準。


4.3.2 使用 ES6 Class 封裝 WebSocket 客戶端(含 async/await)

在本節中,我們將進一步將 WebSocket 客戶端重構為一個具備封裝性與可重用性的 ES6 類別(Class),並導入 async/await 語法來處理初始化流程中的非同步行為。

這樣做的目的不只是提升語意清晰度,更是對「封裝、行為控制、非同步處理」的最佳實踐。

 1 <!DOCTYPE html>
 2 <html lang="zh-Hant">
 3 <head>
 4   <meta charset="UTF-8">
 5   <title>NoChat Client - Class 封裝</title>
 6 </head>
 7 <body>
 8   <div id="header"></div>
 9   <input type="text" id="messageBox" placeholder="輸入訊息...">
10   <button id="connectBtn">建立連線</button>
11   <button id="sendBtn">送出訊息</button>
12 
13   <script type="module">
14   class WebSocketClient {
15     constructor(url, protocol) {
16       this.url = url;
17       this.protocol = protocol;
18       this.ws = null;
19     }
20 
21     async connect() {
22       if (!('WebSocket' in window)) {
23         alert('此瀏覽器不支援 WebSocket');
24         return;
25       }
26 
27       this.ws = new WebSocket(this.url, this.protocol);
28 
29       this.ws.onopen = () => {
30         console.log('WebSocket 已連線');
31         document.getElementById('header').textContent = 'WebSocket 已連線';
32       };
33 
34       this.ws.onmessage = ({ data }) => {
35         console.log(`接收到訊息:${data}`);
36         alert(`接收到訊息:${data}`);
37       };
38 
39       this.ws.onerror = (err) => {
40         console.error('WebSocket 錯誤:', err);
41       };
42 
43       this.ws.onclose = () => {
44         console.log('WebSocket 已關閉');
45       };
46     }
47 
48     async send(msg) {
49       if (this.ws && this.ws.readyState === WebSocket.OPEN) {
50         this.ws.send(msg);
51         console.log(`送出訊息:${msg}`);
52       } else {
53         alert('WebSocket 尚未建立連線。');
54       }
55     }
56   }
57 
58   const client = new WebSocketClient('ws://svn.moko365.com:8080/', 'echo-protocol');
59 
60   document.getElementById('connectBtn').addEventListener('click', async () => {
61     await client.connect();
62   });
63 
64   document.getElementById('sendBtn').addEventListener('click', async () => {
65     const msg = document.getElementById('messageBox').value;
66     await client.send(msg);
67   });
68   </script>
69 </body>
70 </html>

ES6 封裝設計對照

概念傳統寫法(ES5)封裝後寫法(ES6)
WebSocket 實例全域變數 var ws類別屬性 this.ws
建立連線全域函數 WebSocketTest()類別方法 connect()
傳送訊息全域函數 sendMessage()類別方法 send(msg)
訊息處理直接寫在 onmessage 事件中封裝於 connect() 方法中
非同步控制傳統事件觸發async/await 確保流程明確
重複使用無模組化,無法複用任何頁面可建立多個 WebSocketClient 實例使用

封裝的效益

  1. 語意集中:所有與 WebSocket 連線與互動邏輯集中於 WebSocketClient 類別中。
  2. 控制範圍:變數不再污染全域空間,提升封裝與維護性。
  3. 可擴展性:未來可為此類別增加自動重連、心跳偵測、訊息佇列等功能。
  4. 語法現代化:透過 async/await,非同步邏輯清楚易懂,減少 callback 地獄的可能性。

下一節將進入 jQuery Pattern,並對比 Class 封裝與 jQuery Plugin Pattern 的差異。

Next: 4.4 使用 jQuery 模式