即時聊天室
即時聊天室開發教學
目錄
專案設置
建立專案
# 建立專案目錄
mkdir chat-app
cd chat-app
# 初始化 Node.js 專案
npm init -y
# 安裝必要套件
npm install express socket.io
專案結構
chat-app/
├── public/
│ ├── index.html
│ ├── style.css
│ └── client.js
├── server.js
└── package.json
後端開發
基本伺服器設置 (server.js)
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// 提供靜態檔案
app.use(express.static('public'));
// 連接處理
io.on('connection', (socket) => {
console.log('使用者連接');
// 處理新訊息
socket.on('chat message', (msg) => {
io.emit('chat message', {
username: socket.username,
message: msg,
time: new Date().toLocaleTimeString()
});
});
// 處理使用者加入
socket.on('user join', (username) => {
socket.username = username;
io.emit('user joined', {
username: username,
time: new Date().toLocaleTimeString()
});
});
// 處理斷開連接
socket.on('disconnect', () => {
if (socket.username) {
io.emit('user left', {
username: socket.username,
time: new Date().toLocaleTimeString()
});
}
});
});
// 啟動伺服器
const PORT = process.env.PORT || 3000;
http.listen(PORT, () => {
console.log(`伺服器運行在 http://localhost:${PORT}`);
});
前端開發
HTML 結構 (public/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>即時聊天室</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chat-container">
<div id="join-form" class="join-container">
<h2>加入聊天室</h2>
<input type="text" id="username" placeholder="輸入你的名字">
<button onclick="joinChat()">加入</button>
</div>
<div id="chat-room" class="chat-room hidden">
<div class="chat-header">
<h2>聊天室</h2>
<span id="online-count">線上人數: 0</span>
</div>
<div id="messages" class="messages"></div>
<div class="input-area">
<input type="text" id="message-input" placeholder="輸入訊息...">
<button onclick="sendMessage()">發送</button>
</div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="client.js"></script>
</body>
</html>
CSS 樣式 (public/style.css)
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f2f5;
}
.chat-container {
max-width: 800px;
margin: 20px auto;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.join-container {
padding: 20px;
text-align: center;
}
.chat-room {
display: flex;
flex-direction: column;
height: 80vh;
}
.chat-header {
padding: 15px;
background: #f0f2f5;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.messages {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
}
.message {
margin-bottom: 10px;
padding: 10px;
border-radius: 10px;
max-width: 70%;
}
.message.received {
background: #f0f2f5;
margin-right: auto;
}
.message.sent {
background: #0084ff;
color: white;
margin-left: auto;
}
.system-message {
text-align: center;
color: #666;
margin: 10px 0;
}
.input-area {
padding: 20px;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
}
input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
flex-grow: 1;
}
button {
padding: 10px 20px;
background: #0084ff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background: #0066cc;
}
.hidden {
display: none;
}
客戶端邏輯 (public/client.js)
const socket = io();
let username = '';
// DOM 元素
const joinForm = document.getElementById('join-form');
const chatRoom = document.getElementById('chat-room');
const messages = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const onlineCount = document.getElementById('online-count');
// 加入聊天室
function joinChat() {
const usernameInput = document.getElementById('username');
username = usernameInput.value.trim();
if (username) {
joinForm.classList.add('hidden');
chatRoom.classList.remove('hidden');
socket.emit('user join', username);
// 啟用發送訊息的回車鍵監聽
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});
}
}
// 發送訊息
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
socket.emit('chat message', message);
messageInput.value = '';
}
}
// 接收訊息
socket.on('chat message', (data) => {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.classList.add(data.username === username ? 'sent' : 'received');
messageElement.innerHTML = `
<div class="message-header">
<span class="username">${data.username}</span>
<span class="time">${data.time}</span>
</div>
<div class="message-content">${data.message}</div>
`;
messages.appendChild(messageElement);
messages.scrollTop = messages.scrollHeight;
});
// 使用者加入通知
socket.on('user joined', (data) => {
const messageElement = document.createElement('div');
messageElement.classList.add('system-message');
messageElement.textContent = `${data.username} 加入了聊天室`;
messages.appendChild(messageElement);
messages.scrollTop = messages.scrollHeight;
});
// 使用者離開通知
socket.on('user left', (data) => {
const messageElement = document.createElement('div');
messageElement.classList.add('system-message');
messageElement.textContent = `${data.username} 離開了聊天室`;
messages.appendChild(messageElement);
messages.scrollTop = messages.scrollHeight;
});
功能擴展
1. 在線用戶列表
// 在 server.js 中添加
const users = new Set();
io.on('connection', (socket) => {
socket.on('user join', (username) => {
users.add(username);
io.emit('update users', Array.from(users));
});
socket.on('disconnect', () => {
if (socket.username) {
users.delete(socket.username);
io.emit('update users', Array.from(users));
}
});
});
2. 私人訊息
// 在 server.js 中添加
socket.on('private message', (data) => {
const targetSocket = findSocketByUsername(data.to);
if (targetSocket) {
targetSocket.emit('private message', {
from: socket.username,
message: data.message,
time: new Date().toLocaleTimeString()
});
}
});
3. 輸入中狀態
// 在 client.js 中添加
let typingTimer;
messageInput.addEventListener('input', () => {
socket.emit('typing', username);
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
socket.emit('stop typing', username);
}, 1000);
});
部署說明
本地運行
- 確保已安裝 Node.js
- 下載專案檔案
- 安裝依賴:
npm install
- 運行伺服器:
node server.js
- 開啟瀏覽器訪問 http://localhost:3000
錯誤處理
// 在 server.js 中添加
process.on('uncaughtException', (err) => {
console.error('未捕獲的異常:', err);
});
io.on('error', (err) => {
console.error('Socket.IO 錯誤:', err);
});
性能優化
- 使用訊息佇列
- 實施速率限制
- 定期清理斷開的連接
- 使用 Redis 進行擴展
// 速率限制示例
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分鐘
max: 100 // 限制每個 IP 100 個請求
});
app.use(limiter);
安全考慮
1. 輸入驗證
function sanitizeMessage(message) {
return message
.trim()
.replace(/</g, "<")
.replace(/>/g, ">");
}
2. 身份驗證
const jwt = require('jsonwebtoken');
function authenticateToken(socket, next) {
const token = socket.handshake.auth.token;
if (token) {
jwt.verify(token, process.env.SECRET_KEY, (err, user) => {
if (err) return next(new Error('Authentication error'));
socket.user = user;
next();
});
} else {
next(new Error('Authentication error'));
}
}
io.use(authenticateToken);
3. 防止 XSS 攻擊
const xss = require('xss');
socket.on('chat message', (msg) => {
const sanitizedMessage = xss(msg);
io.emit('chat message', sanitizedMessage);
});