許久沒有寫文章了(你好像常這樣說欸!),今天要來稍微講講專門用來實作即時通訊的 Node.js 模組 – Socket.io。
Socket.io 其實是一個完整實作 Websocket 的函式庫,他提供更簡單的方式讓開發者可以方便的使用 Websocket 這樣的通訊技術來實作許多應用。
我將以實作一個簡易聊天室作為範例來介紹這一個模組。
開始之前
這篇文章的程式範例將以 Node.js + Socket.io 為主,因此你必須先安裝好 Node.js 的環境,安裝方式請直接至官方網站下載安裝即可。
確認裝好 Node.js 的環境後,我們要來做一些基礎設定以及安裝該有的基本模組啦!請在你想要開發的資料夾中開啟終端機,輸入以下指令:
1 2 |
npm init -y npm install -S express socket.io |
這兩行指令,第一行是在資料夾中產生一個 package.json
的檔案,這個檔案會紀錄專案的各種資訊,包含所相依的模組等等。
第二行指令則是安裝我們所需的兩種模組:Express.js、socket.io。
接下來,我們在專案資料夾中開一個新檔案:index.js
。
一切就緒後,我們就可以開始進行了。
伺服器程式
HTTP
凡與 HTTP 有關係的東西就離不開 HTTP 伺服器啦!在 Node.js 中,建一個 HTTP 的伺服器非常簡單,大概就這不到 10 行的程式碼就能完成了吧。那個,別發呆,還不趕快把下面這幾行程式碼輸入到index.js
中!
1 2 3 4 5 6 7 8 9 10 11 |
const express = require('express'); const app = express(); app.get('/', (req, res) => { res.send('Hello, World!'); }); app.listen(3000, () => { console.log("Server Started. http://localhost:3000"); }); |
然後輸入這個指令來啟動伺服器:
1 2 |
node index.js |
你應該會看到終端機顯示這樣的字樣:
1 2 |
Server Started. http://localhost:3000 |
好了,你可以打開瀏覽器,然後輸入這個網址:http://localhost:3000
你的瀏覽器應該會出現
1 2 |
Hello, World! |
HTTP 伺服器完成!
Socket.io
再來是Socket.io的部分,這邊我們要稍微修改一下index.js
的程式碼。
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 |
const express = require('express'); const app = express(); // 加入這兩行 const server = require('http').Server(app); const io = require('socket.io')(server); app.get('/', (req, res) => { res.send('Hello, World!'); }); // 當發生連線事件 io.on('connection', (socket) => { console.log('Hello!'); // 顯示 Hello! // 當發生離線事件 socket.on('disconnect', () => { console.log('Bye~'); // 顯示 bye~ }); }); // 注意,這邊的 server 原本是 app server.listen(3000, () => { console.log("Server Started. http://localhost:3000"); }); |
啟動方式跟我們在執行HTTP伺服器時一樣
1 2 |
node index.js |
然後你會發現什麼都沒有變。
是的,因為我們的網站頁面還沒有放上去,當然什麼都不會變啊!畢竟這時候瀏覽器也不知道要打開Websocket呢!不信?那你連看看http://localhost:3000,看伺服器有沒有出現Hello
啊!沒有對吧~
所以呢,接下來我們要透過網頁來與伺服器搭上線,抓緊,我們要準備起飛了。
網頁
伺服器大致完成後,要來處理人看得到的部分,也就是網頁呈現的部分。總不能只顯示個 Hello, World!,然後什麼事都不能做吧!?這樣還要聊個毛天啊!
所以,接下來請在你的專案資料夾下建立一個資料夾,這個資料夾專門用來存放網頁程式用,我想我們就叫做…views
好了。
然後在views
中建一個檔案:index.html。
這個檔案將會呈現我們聊天室的主要畫面,請記得在檔案裝輸入下方的程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="zh-tw"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Chatroom</title> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); </script> </head> <body> <div>什麼都沒有做,只有連線。</div> </body> </html> |
然後修改index.js
1 2 3 4 5 |
// 修改這一部分 app.get('/', (req, res) => { res.sendFile( __dirname + '/views/index.html'); }); |
這樣伺服器才會把我們剛剛的網頁內容推到瀏覽器上顯示。
那麼,這其實只是一個非常基本的頁面,只有一個目標,就是讓客戶端與伺服器的 WebSocket 連接埠建立連線。要確認有無連線,請重新整理剛剛打開的瀏覽器畫面,然後按下重新整理,你應該會在終端機中看到Hello!
:關閉網頁的話會看到Bye~
喔。
如果沒的話,重新執行一次伺服器吧,請記得先用CTRL+C
關閉伺服器再啟動。
收發測試
現在我們要來做一點實驗,我們先在index.html
的body
部分加入如下的片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<body> <div id="msg"></div> <script> // 當觸發連線建立事件 // 發送 greet 事件給伺服器 socket.on("connect", function () { socket.emit("greet"); }); // 當收到伺服器回傳到 greet 事件 // 將內容轉到 div 中呈現 socket.on("greet", function (msg) { document.getElementById("msg").innerText = msg; }); </script> </body> |
喔對,還有修改伺服器的程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
io.on('connection', (socket) => { console.log('Hello!'); // 加入這一段 // 接收來自前端的 greet 事件 // 然後回送 greet 事件,並附帶內容 socket.on("greet", () => { socket.emit("greet", "Hi! Client."); }); //... }); |
重新啟動伺服器然後重新整理網頁,你應該會看到網頁上顯示Hi! Client.
,這代表一切正常沒有問題。
在這一部份中,我們重新調整了伺服器的程式,以及前端網頁的JavaScript程式。
在前端網頁的部分
- 更改
body
的內容,加入一個div
來顯示接收到的訊息 - 加入新的 JavaScript 程式,觸發伺服器回傳訊息與接收訊息並用
div
顯示
在伺服器的部分
- 接收前端網頁的事件
- 回傳一個附帶內容的事件給前端網頁
介面設計
秉持著自己的玩具自己做的精神,本教學不使用過多額外的框架,避免過於複雜的學習難度,所以在這邊你會看到許多原生的語法以及有點糟糕的程式碼。
喔對,如果要說框架的話,我有用到一個:Vanilla.js
回到正題。
一個正常的聊天室,我們需要幾個基本功能:
- 伺服器狀態
- 發言者名稱
- 顯示聊天內容
- 發言者
- 發言時間
- 訊息內容
- 訊息輸入
所以我們的介面大概會長這樣…等等你就會看到了。
好,開始囉。
index.html
在
1 2 |
</head> |
之前加入
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<style> html, body { padding: 0; margin: 0; } #container { top: 50px; width: 500px; margin: 0 auto; display: block; position: relative; } #status-box { text-align: right; font-size: .6em; } #content { width: 100%; height: 350px; border: 1px solid darkolivegreen; border-radius: 5px; overflow: auto; } #send-box { width: 100%; text-align: center; } #send-box input { display: inline-block; } input[name="name"] { width: 15%; } input[name="msg"] { width: 70%; } input[type="button"] { width: 10%; } .msg { width: 73%; display: inline-block; padding: 5px 0 5px 10px; } .msg > span { width: 25%; display: inline-block; } .msg > span::before { color: darkred; content: " { "; } .msg > span::after { color: darkred; content: " } "; } </style> |
將<body>...</body>
替換成
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 |
<body> <div id="container"> <div id="status-box">Server: <span id="status">-</span> / <span id="online">0</span> online.</div> <div id="content"> <div class="msg"> <span class="name">Duye</span> Hello! </div> <div class="msg"> <span class="name">Alice</span> Hi! </div> </div> <div id="send-box"> <form id="send-form"> <input type="text" name="name" id="name" placeholder="暱稱"> <input type="text" name="msg" id="msg" placeholder="說點什麼?"> <input type="submit" value="送出"> </form> </div> </div> <script> document.addEventListener("DOMContentLoaded", () => { var status = document.getElementById("status"); var online = document.getElementById("online"); socket.on("connect", function () { status.innerText = "Connected."; }); socket.on("disconnect", function () { status.innerText = "Disconnected."; }); socket.on("online", function (amount) { online.innerText = amount; }); }); </script> </body> |
重新整理網頁後應會看到一個新的畫面,這時還沒有任何功能,雖然有點簡陋,不過該有的都有啦,別嫌 XD。
接下來我們要來修改伺服器的部分,順便讓伺服器狀態這個功能動起來。
index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const server = require('http').Server(app); const io = require('socket.io')(server); // 加入線上人數計數 let onlineCount = 0; // 修改 connection 事件 io.on('connection', (socket) => { // 有連線發生時增加人數 onlineCount++; // 發送人數給網頁 io.emit("online", onlineCount); socket.on("greet", () => { socket.emit("greet", onlineCount); }); socket.on('disconnect', () => { // 有人離線了,扣人 onlineCount = (onlineCount < 0) ? 0 : onlineCount-=1; io.emit("online", onlineCount); }); }); |
重啟伺服器與網頁,注意右上角的資訊,理應會看到:
1 2 |
Server: Connected. / 1 online. |
這樣的資訊才對,如果沒有,那應該是失敗了,請再做一次看看。
這時你可以再開一個網頁視窗,應該會看到右上角的數字變成 2。
訊息輸入
搞定了簡單的外觀後,接下來是訊息輸入的部分,也就是聊天內容輸入。
index.html
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 |
document.addEventListener("DOMContentLoaded", () => { var status = document.getElementById("status"); var online = document.getElementById("online"); var sendForm = document.getElementById("send-form"); // 加入這行 //... // 加入這段 sendForm.addEventListener("submit", function (e) { e.preventDefault(); var formData = {}; var formChild = sendForm.children; for (var i=0; i< sendForm.childElementCount; i++) { var child = formChild[i]; if (child.name !== "") { formData[child.name] = child.value; } } socket.emit("send", formData); }); }); |
index.js
1 2 3 4 5 6 7 8 9 |
io.on('connection', (socket) => { //... // 加入這段 socket.on("send", (msg) => { console.log(msg) }); //... }); |
現在重新啟動伺服器和網頁,然後嘗試輸入一些東西後送出,觀察終端機的變化吧~
這裡我們在前端網頁的部分加入
- 表單送出(submit)事件監聽器
- 取消原有的送出動作
- 讀取表單內容
- 透過 socket 送出表單內容到伺服器
伺服器
- 增加新的事件監聽器
- 接收來自網頁端的訊息
- 將訊息寫是在終端機上
顯示聊天訊息
完成了送出的部分,並且也確認伺服器能正確收到資料後,我們要來讓聊天室可以顯示這些聊天資料啦!
index.html
1 2 3 4 5 6 7 8 9 10 |
<!-- 刪除這部分 --> <div class="msg"> <span class="name">Duye</span> Hello! </div> <div class="msg"> <span class="name">Alice</span> Hi! </div> |
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 |
document.addEventListener("DOMContentLoaded", () => { //... var sendForm = document.getElementById("send-form"); var content = document.getElementById("content"); // 加入這行 //... socket.on("online", function (amount) { online.innerText = amount; }); // 加入這一段 socket.on("msg", function (d) { var msgBox = document.createElement("div") msgBox.className = "msg"; var nameBox = document.createElement("span"); nameBox.className = "name"; var name = document.createTextNode(d.name); var msg = document.createTextNode(d.msg); nameBox.appendChild(name); msgBox.appendChild(nameBox); msgBox.appendChild(msg); content.appendChild(msgBox); }); //... }); |
index.js
1 2 3 4 5 6 |
// 修改 console.log 成 io.emit socket.on("send", (msg) => { // 廣播訊息到聊天室 io.emit("msg", msg); }); |
好,現在重啟伺服器,開兩個視窗,連上伺服器,你可以開始聊天了!(雖然是跟自己)
空白輸入
我想你有發現到,當你沒有輸入任何東西直接送出時還是能送出,這其實是一件很詭異的事情,所以我們要來解決這個問題。
index.html
- CSS 部分加入
1 2 3 4 |
#send-box input.error { border: 1px solid red; } |
- 替換掉原本的
submit
監聽器。
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 |
//... sendForm.addEventListener("submit", function (e) { e.preventDefault(); var ok = true; var formData = {}; var formChild = sendForm.children; for (var i=0; i< sendForm.childElementCount; i++) { var child = formChild[i]; if (child.name !== "") { var val = child.value; if (val === "" || !val) { // 如果值為空或不存在 ok = false; child.classList.add("error"); } else { child.classList.remove("error"); formData[child.name] = val; } } } // ok 為真才能送出 if (ok) socket.emit("send", formData); }); |
這樣前端網頁發現值為空時,除了警告提示外,也不會將事件送出去給伺服器。但這樣還不夠,如果有人直接對 WebSocket 送事件的話還是會被發現,所以我們要讓後端也能做一次驗證.
index.js
1 2 3 4 5 6 7 8 9 10 11 12 |
//... // 修改 send 事件監聽器 socket.on("send", (msg) => { // 如果 msg 內容鍵值小於 2 等於是訊息傳送不完全 // 因此我們直接 return ,終止函式執行。 if (Object.keys(msg).length < 2) return; // 廣播訊息到聊天室 io.emit("msg", msg); }); |
伺服器端的驗證很簡單,只判斷鍵值長度是否小於2,而2這個長度則是因為我們的訊息內寫包含有兩個東西
- 暱稱
- 訊息本體
好了,你可以嘗試把index.html
中的if(ok)
拿掉,然後試著不輸入任何東西直接送出,我相信伺服器他絕對是毫無反應,就只是個收到了,這樣。
結束之後
Demo
如果你有點懶得動手做,這邊有實品展示可以玩玩。
下一階
這個聊天室加入全新的功能,有興趣的話可以到這邊來看看:http://single9.net/2018/01/node-js-與-socket-io-即時聊天室實作二/
完整程式
感謝你看完這篇文章,如果你也有跟著時實做完文章內容,你應該會看到這樣的完整程式碼:
index.html
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
<!DOCTYPE html> <html lang="zh-tw"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Chatroom</title> <script src="/socket.io/socket.io.js"></script> <script> var socket = io(); </script> <style> html, body { padding: 0; margin: 0; } #container { top: 50px; width: 500px; margin: 0 auto; display: block; position: relative; } #status-box { text-align: right; font-size: .6em; } #content { width: 100%; height: 350px; border: 1px solid darkolivegreen; border-radius: 5px; overflow: auto; } #send-box { width: 100%; text-align: center; } #send-box input { display: inline-block; } #send-box input.error { border: 1px solid red; } input[name="name"] { width: 15%; } input[name="msg"] { width: 70%; } input[type="button"] { width: 10%; } .msg { width: 73%; display: inline-block; padding: 5px 0 5px 10px; } .msg > span { width: 25%; display: inline-block; } .msg > span::before { color: darkred; content: " { "; } .msg > span::after { color: darkred; content: " } "; } </style> </head> <body> <div id="container"> <div id="status-box">Server: <span id="status">-</span> / <span id="online">0</span> online.</div> <div id="content"> </div> <div id="send-box"> <form id="send-form"> <input type="text" name="name" id="name" placeholder="暱稱"> <input type="text" name="msg" id="msg" placeholder="說點什麼?"> <input type="submit" value="送出"> </form> </div> </div> <script> document.addEventListener("DOMContentLoaded", () => { var status = document.getElementById("status"); var online = document.getElementById("online"); var sendForm = document.getElementById("send-form"); var content = document.getElementById("content"); socket.on("connect", function () { status.innerText = "Connected."; }); socket.on("disconnect", function () { status.innerText = "Disconnected."; }); socket.on("online", function (amount) { online.innerText = amount; }); socket.on("msg", function (d) { var msgBox = document.createElement("div") msgBox.className = "msg"; var nameBox = document.createElement("span"); nameBox.className = "name"; var name = document.createTextNode(d.name); var msg = document.createTextNode(d.msg); nameBox.appendChild(name); msgBox.appendChild(nameBox); msgBox.appendChild(msg); content.appendChild(msgBox); }); sendForm.addEventListener("submit", function (e) { e.preventDefault(); var ok = true; var formData = {}; var formChild = sendForm.children; for (var i=0; i< sendForm.childElementCount; i++) { var child = formChild[i]; if (child.name !== "") { var val = child.value; if (val === "" || !val) { ok = false; child.classList.add("error"); } else { child.classList.remove("error"); formData[child.name] = val; } } } if (ok) socket.emit("send", formData); }); }); </script> </body> </html> |
index.js
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 |
const express = require('express'); const app = express(); const server = require('http').Server(app); const io = require('socket.io')(server); // 加入線上人數計數 let onlineCount = 0; app.get('/', (req, res) => { res.sendFile( __dirname + '/views/index.html'); }); io.on('connection', (socket) => { // 有連線發生時增加人數 onlineCount++; // 發送人數給網頁 io.emit("online", onlineCount); socket.on("greet", () => { socket.emit("greet", onlineCount); }); socket.on("send", (msg) => { // 如果 msg 內容鍵值小於 2 等於是訊息傳送不完全 // 因此我們直接 return ,終止函式執行。 if (Object.keys(msg).length < 2) return; // 廣播訊息到聊天室 io.emit("msg", msg); }); socket.on('disconnect', () => { // 有人離線了,扣人 onlineCount = (onlineCount < 0) ? 0 : onlineCount-=1; io.emit("online", onlineCount); }); }); server.listen(3000, () => { console.log("Server Started. http://localhost:3000"); }); |
最後
這篇文章重點:
- Node.js 專案初始化
- Node.js HTTP 伺服器
- Socket.io 應用
- 全端技能++
感謝收看,我們下一篇再見~
多謝你的網誌,讓我更了解如何應用
感謝
跟著你的文章順利做出來了~文章寫的很詳細易懂
非常謝謝你
有幫到忙真是太好了
剛剛留言,結果卻沒出現? 不知道是什麼問題,總之謝謝你們這篇文章。
但我又回頭留言主要的原因是 .. 我留言之後,重新連到這篇文章時
原本程式碼片段的是像 gist 那樣有帶顏色的。
但被轉址後網址多了 /amp/ ,程式碼片段就變成純文字了 …
可是文章中有些連結又不會轉址,覺得怪怪的反應一下~
還是有顏色比較容易閱讀啦 XD
最近比較忙沒什麼上來除草,感謝您的留言~
沒跑出來原因是因為您是第一次留言,所以需要人工審核…XD
想請問 怎麼cordova 包裝成一個手機APP