許久沒有寫文章了(你好像常這樣說欸!),今天要來稍微講講專門用來實作即時通訊的 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