Node.js 實作 The F2E_ChatRoom (2) 仿登入系統與排程

集點送紅利 / Hiro
Nov 6 · 13 min read

本系列介紹使用 Node.js 實作聊天室時會遇到的大小坑。

上一篇我們介紹了建立路由監聽、串接 SQL 與 Socket.IO 基本使用,本篇則分享在聊天室建立當中,筆者較困惑的地方,以及在最後會附上片段程式碼。

本篇目錄

  1. 仿登入系統 - 與 Session 和 CORS 打交道
  2. 使用 Node Schedule 做排程

仿登入系統

這邊的仿登入系統,指的是輸入的名字不能和線上使用者撞名,因此我們需要搭配 Socket.IO 去做上線寫入資料庫,下線即刪除的動作。

但這邊值得注意的是,Socket.IO 隔一段時間會自動重新登入。

另外當使用者在聊天室頁面重新刷新或是發生連線逾時的話,
就會失去一開始登入的名字。

這在使用者體驗上是非常糟糕的,因此我們應該搭配 Session 去判斷使用者是真的離開,又或是單純的重新整理頁面而已。下圖是筆者能想到的邏輯判斷,但說實在有點過於複雜…還盼望有人能一起討論

Session

所以我們的第一步,要從 Session 走起!
本文主要講解實作上遇到的困難,如果不了解 Session 的話可以先參考這篇文章

第一步要先安裝:

$ npm install express-session

並且引入:

const session = require('express-session');app.use(session({
secret: 'thef2e_chatroom',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 60 * 60 * 1000 * 3, // 存活時間為三小時
},
}));
// 屬性配置可參考 NPM

設置好後當我們服務端接收到請求時,客戶端都會帶有一組 Session ID (SID)。正常沒有設置下,SID 會存活到使用者關閉瀏覽器為止,而我們在上處有設置存活 3 小時。

接著,我們會需要兩端來做測試才會準確,也就是說要先開啟服務端 (3000),再用客戶端 (8080) 對服務端做 API get。(用 Postman 會失效)

在服務端上我們貼上以下程式碼:

app.get('/session', (req, res) => {
console.log('SID: ', req.sessionID);
req.session.user = req.sessionID;
res.send('Hello Session');
});

客戶端我們用 axios 做 get 測試:

this.axios.get('http://localhost:3000/session').then((response) => {
console.log(response.data);
});

但…發現問題出在哪了嗎?
為什麼我每請求一次 SID 就給我一組新的 ?

是因為我們還沒設置好 CORS!

CORS

CORS又是什麼?能吃嗎?
如果不知道的話可以先參考這篇文章,講得非常詳細!

簡單來說就是因為前後端分離,為了要讓瀏覽器辨別是同一人
前後端都需要加上 CORS 的 Credentials 認證屬性

當初測試時,一直對 Session 刷新抱持著疑問,因為瀏覽器也沒報錯,弄了一整天才查到是 CORS 的問題,只覺得…我只是想認證怎麼這麼難(泣

接著我們來加上 CORS 試試看:

$ npm install cors --save

引入並設置 CORS:

const cors = require('cors');const corsOptions = {
origin: 'http://localhost:8080', // 客戶端 port
credentials: true,
};
app.use(cors(corsOptions)); // 要在 API 的上面先使用// 加上 credentials 後,origin 必須設置網址,不能為 * (通用)

另外在前端也需要開啟認證配置 (這邊使用 vue-axios)

axios.defaults.withCredentials = true;

這時候再試試看!

這樣就成功讓 Session 與 CORS 打交道啦!
接下來,我們只需要做出下面兩隻 API,並和 Socket.IO 做連線配合就 OK 了。

  • 登入後寫入資料庫 + Session
  • 確認登入狀態
    Server 端先辨認是否已存有 Session,再來以 Vue Router 判斷使用者是否為刷新,兩者為是則重新更換 socket ID 並上線。

最後,這邊貼上聊天室片段程式碼:

  1. Socket.IO
    在 connection 上線時,把 ID 先存進變數,變數在下面 API 會使用到。 disconnect 離線時,則是以離開聊天室的 ID,去查詢資料庫內有無此使用者,有則刪除使用者。
// variable
let socketID = '';
// Socket.IO
const io = require('socket.io')(server);

io.on('connection', socket => {
console.log('連接成功,上線ID: ', socket.id);

// 存進變數
socketID = socket.id;
// 監聽訊息
socket.on('getTopicMessage', message => {
console.log('topic 聊天室', message);
}
// 連接斷開
socket.on('disconnect', () => {
console.log('有人離開了!, 下線ID: ', socket.id);
// 查詢下線ID (socket.id)
pool.getConnection((err, connection) => {
connection.query(`SELECT * from user where user_socket_id =
"${socket.id}"`, (err, rows, fields) => {
if (err) throw err;
if (rows.length === 0) {
console.log('此人未登入過');
connection.release();
} else {
// 刪除資料讓使用者下線
connection.query(`DELETE from user where user_socket_id =
"${socket.id}"`, (err, rows, fields) => {
if (err) {
console.log(err);
console.log('下線失敗');
} else {
console.log('下線成功');
}
connection.release();
});
};
});
});
});
});

2. Login API
取得 POST 資料後,對資料庫做查詢,如沒撞名則寫入並上線

// User Login
app.post('/api/login', (req, res) => {

pool.getConnection((err, connection) => {
if (err) throw err;
// 查詢是否撞名
connection.query(`SELECT user_name from user where user_name =
"${req.body.username}"`, function(err, rows, fields) {
if (err) throw err;
if (rows.length !== 0) {
res.send({
success: false,
message: '此名字已有人使用,請使用其他名字'
});
connection.release();
} else {
// 寫入資料,使用者上線
connection.query('INSERT INTO user SET ?', {
user_name: req.body.username,
user_session: req.sessionID,
user_socket_id: socketID
}, (err, rows, fields) => {
if (err) {
console.log(err);
res.send({
success: false,
message: '登入失敗!'
})
connection.release();
} else {
// 新增 user 至 Session
req.session.user = {
session: req.sessionID,
name: req.body.username,
socketID: socketID
}
res.send({
success: true,
message: '登入成功!'
})
connection.release();
}
})
}
});
})
});

3. Login Status
這邊應該是最複雜的地方
第一,先判斷是否登入過 (有無 Session),無則回傳 false 並讓使用者繼續登入步驟
第二,客戶端會用 Vue Router 判斷是否為刷新頁面,並帶 params 進來服務端,否 (normal) 則放行使用者使用聊天室,是 (refresh) 則代表剛剛使用者下線但有 Session,所以我們應該做資料庫的寫入讓他重新上線。

要注意的是,使用者回來並重新帶了一個新的聊天室 ID,我們要在寫入資料庫前調換新聊天室 ID。

// User Login Status
app.get('/api/loginstatus/:method', (req, res) => {
// *第一步
if (req.session.user) {
// *第二步
if (req.params.method === 'normal') {
res.send({
success: true,
message: '使用者未下線,無需處理'
})
return;
}

// 更換 socketID,此處取上面存好的變數
req.session.user.socketID = socketID;

// *第三步
pool.getConnection((err, connection) => {
if (err) throw err;
connection.query(`SELECT user_name from user where user_name =
"${req.session.user.name}"`, function(err, rows, fields) {
if (err) throw err;
if (rows.length !== 0) {
res.send({
success: false,
message: '此名字已有人使用,請使用其他名字'
});
connection.release();
} else {
connection.query('INSERT INTO user SET ?', {
user_name: req.session.user.name,
user_session: req.sessionID,
user_socket_id: socketID
}, (err, rows, fields) => {
if (err) {
console.log(err);
res.send({
success: false,
message: '重新登入失敗!'
})
connection.release();
} else {
res.send({
success: true,
message: req.session.user.name
})
connection.release();
}
})
}
});
})
} else {
res.send({
success: false,
message: '此使用者未登入過'
});
}
});

排程

還記得嗎?在上一篇的聊天室圖中有個每日主題,需要在每日午時做主題更換。平常我們做固定時間處理某些事會用 SetInterval 或是 SetTimeout,但時間如果一拉長的話,就可以試著引入 Node Schedule 此套件。

使用方法非常簡單,先下載下來:

$ npm install node-schedule --save 

引入專案:

const schedule = require('node-schedule');// 42 分時執行 console 打印
const j = schedule.scheduleJob('42 * * * *', function(){
console.log('The answer to life, the universe, and everything!');
});

Node Schedule 總共有 6 個 * 號,分別為 (秒)、分、時、日期、月與星期幾,秒可以省略所以一般為 5 個 * 號。

這邊要注意的是,(5 * * * * *) 你可能會以為這是每五秒執行一次,
但這意思是時鐘指到第五秒時才執行,所以是每分執行一次。
想要每隔五秒執行一次的話,則是要輸入 (*/5 * * * * *)。

所以我們應該要改成,下圖為後台管理狀態

const j = schedule.scheduleJob('0 0 * * *', function(){
// 更換主題
});

後記

在做登入狀態判斷時,應該可以用更簡單的方法去做,只是我想得比較複雜…最糾結的點還是在於如何監測使用者離開網頁並做下線刪除動作。所以 Socket.IO 連線我才放在全域並搭配 Session 。

另外,在整理資料時發現了另一個比較多人用的排程工具 cron,寫法都很像(只好改天再研究看看了

預計下一篇會提到部署專案至 Heroku !

參考資料

集點送紅利 / Hiro

Written by

我喜歡日文,也喜歡coding。

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade