許久沒有寫文章了(你好像常這樣說欸!),今天要來稍微講講專門用來實作即時通訊的 Node.js 模組 – Socket.io。
Socket.io 其實是一個完整實作 Websocket 的函式庫,他提供更簡單的方式讓開發者可以方便的使用 Websocket 這樣的通訊技術來實作許多應用。
我將以實作一個簡易聊天室作為範例來介紹這一個模組。
這篇文章的程式範例將以 Node.js + Socket.io 為主,因此你必須先安裝好 Node.js 的環境,安裝方式請直接至官方網站下載安裝即可。
確認裝好 Node.js 的環境後,我們要來做一些基礎設定以及安裝該有的基本模組啦!請在你想要開發的資料夾中開啟終端機,輸入以下指令:
npm init -y npm install -S express socket.io
這兩行指令,第一行是在資料夾中產生一個 package.json
的檔案,這個檔案會紀錄專案的各種資訊,包含所相依的模組等等。
第二行指令則是安裝我們所需的兩種模組:Express.js、socket.io。
接下來,我們在專案資料夾中開一個新檔案:index.js
。
一切就緒後,我們就可以開始進行了。
凡與 HTTP 有關係的東西就離不開 HTTP 伺服器啦!在 Node.js 中,建一個 HTTP 的伺服器非常簡單,大概就這不到 10 行的程式碼就能完成了吧。那個,別發呆,還不趕快把下面這幾行程式碼輸入到index.js
中!
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");
});
然後輸入這個指令來啟動伺服器:
node index.js
你應該會看到終端機顯示這樣的字樣:
Server Started. http://localhost:3000
好了,你可以打開瀏覽器,然後輸入這個網址:http://localhost:3000
你的瀏覽器應該會出現
Hello, World!
HTTP 伺服器完成!
再來是Socket.io的部分,這邊我們要稍微修改一下index.js
的程式碼。
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伺服器時一樣
node index.js
然後你會發現什麼都沒有變。
是的,因為我們的網站頁面還沒有放上去,當然什麼都不會變啊!畢竟這時候瀏覽器也不知道要打開Websocket呢!不信?那你連看看http://localhost:3000,看伺服器有沒有出現Hello
啊!沒有對吧~
所以呢,接下來我們要透過網頁來與伺服器搭上線,抓緊,我們要準備起飛了。
伺服器大致完成後,要來處理人看得到的部分,也就是網頁呈現的部分。總不能只顯示個 Hello, World!,然後什麼事都不能做吧!?這樣還要聊個毛天啊!
所以,接下來請在你的專案資料夾下建立一個資料夾,這個資料夾專門用來存放網頁程式用,我想我們就叫做…views
好了。
然後在views
中建一個檔案:index.html。
這個檔案將會呈現我們聊天室的主要畫面,請記得在檔案裝輸入下方的程式碼。
<!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
// 修改這一部分
app.get('/', (req, res) => {
res.sendFile( __dirname + '/views/index.html');
});
這樣伺服器才會把我們剛剛的網頁內容推到瀏覽器上顯示。
那麼,這其實只是一個非常基本的頁面,只有一個目標,就是讓客戶端與伺服器的 WebSocket 連接埠建立連線。要確認有無連線,請重新整理剛剛打開的瀏覽器畫面,然後按下重新整理,你應該會在終端機中看到Hello!
:關閉網頁的話會看到Bye~
喔。
如果沒的話,重新執行一次伺服器吧,請記得先用CTRL+C
關閉伺服器再啟動。
現在我們要來做一點實驗,我們先在index.html
的body
部分加入如下的片段
<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>
喔對,還有修改伺服器的程式
io.on('connection', (socket) => {
console.log('Hello!');
// 加入這一段
// 接收來自前端的 greet 事件
// 然後回送 greet 事件,並附帶內容
socket.on("greet", () => {
socket.emit("greet", "Hi! Client.");
});
//...
});
重新啟動伺服器然後重新整理網頁,你應該會看到網頁上顯示Hi! Client.
,這代表一切正常沒有問題。
在這一部份中,我們重新調整了伺服器的程式,以及前端網頁的JavaScript程式。
在前端網頁的部分
body
的內容,加入一個div
來顯示接收到的訊息div
顯示在伺服器的部分
秉持著自己的玩具自己做的精神,本教學不使用過多額外的框架,避免過於複雜的學習難度,所以在這邊你會看到許多原生的語法以及有點糟糕的程式碼。
喔對,如果要說框架的話,我有用到一個:Vanilla.js
回到正題。
一個正常的聊天室,我們需要幾個基本功能:
所以我們的介面大概會長這樣…等等你就會看到了。
好,開始囉。
index.html
在
</head>
之前加入
<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>
替換成
<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
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);
});
});
重啟伺服器與網頁,注意右上角的資訊,理應會看到:
Server: Connected. / 1 online.
這樣的資訊才對,如果沒有,那應該是失敗了,請再做一次看看。
這時你可以再開一個網頁視窗,應該會看到右上角的數字變成 2。
搞定了簡單的外觀後,接下來是訊息輸入的部分,也就是聊天內容輸入。
index.html
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
io.on('connection', (socket) => {
//...
// 加入這段
socket.on("send", (msg) => {
console.log(msg)
});
//...
});
現在重新啟動伺服器和網頁,然後嘗試輸入一些東西後送出,觀察終端機的變化吧~
這裡我們在前端網頁的部分加入
伺服器
完成了送出的部分,並且也確認伺服器能正確收到資料後,我們要來讓聊天室可以顯示這些聊天資料啦!
index.html
<!-- 刪除這部分 -->
<div class="msg">
<span class="name">Duye</span>
Hello!
</div>
<div class="msg">
<span class="name">Alice</span>
Hi!
</div>
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
// 修改 console.log 成 io.emit
socket.on("send", (msg) => {
// 廣播訊息到聊天室
io.emit("msg", msg);
});
好,現在重啟伺服器,開兩個視窗,連上伺服器,你可以開始聊天了!(雖然是跟自己)
我想你有發現到,當你沒有輸入任何東西直接送出時還是能送出,這其實是一件很詭異的事情,所以我們要來解決這個問題。
index.html
#send-box input.error {
border: 1px solid red;
}
submit
監聽器。
//...
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
//...
// 修改 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)
拿掉,然後試著不輸入任何東西直接送出,我相信伺服器他絕對是毫無反應,就只是個收到了,這樣。
如果你有點懶得動手做,這邊有實品展示可以玩玩。
這個聊天室加入全新的功能,有興趣的話可以到這邊來看看:http://single9.net/2018/01/node-js-與-socket-io-即時聊天室實作二/
感謝你看完這篇文章,如果你也有跟著時實做完文章內容,你應該會看到這樣的完整程式碼:
index.html
<!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
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");
});
這篇文章重點:
感謝收看,我們下一篇再見~
View Comments
多謝你的網誌,讓我更了解如何應用
感謝
跟著你的文章順利做出來了~文章寫的很詳細易懂
非常謝謝你
有幫到忙真是太好了
剛剛留言,結果卻沒出現? 不知道是什麼問題,總之謝謝你們這篇文章。
但我又回頭留言主要的原因是 .. 我留言之後,重新連到這篇文章時
原本程式碼片段的是像 gist 那樣有帶顏色的。
但被轉址後網址多了 /amp/ ,程式碼片段就變成純文字了 ...
可是文章中有些連結又不會轉址,覺得怪怪的反應一下~
還是有顏色比較容易閱讀啦 XD
最近比較忙沒什麼上來除草,感謝您的留言~
沒跑出來原因是因為您是第一次留言,所以需要人工審核...XD
想請問 怎麼cordova 包裝成一個手機APP