WebSocket 與 API(REST API)的比較
WebSocket 和 API(這裡主要指 REST API)是兩種常見的客戶端與伺服器溝通方式,但它們的設計目的不同:
REST API 適合一般的請求-回應模式,像是在查詢資料或提交表單。
WebSocket 則專門用於即時、雙向的資料交換,像聊天室或股票即時報價。
介紹
-
REST API:基於 HTTP 協定,使用 GET、POST、PUT、DELETE 等方法來操作資料。每個請求都是獨立的(無狀態),伺服器收到請求後回應,連接就斷開。適合不需即時的應用,例如登入、讀取部落格文章。
-
WebSocket:建立一個持久連接(像電話通話),連接後雙方可以隨時發送/接收資料,不用每次都重新連線。適合需要低延遲的即時應用,例如即時聊天或遊戲。
比較
以下表格整理了兩者的關鍵差異、優缺點,以及適合的使用情境。我參考了多個可靠來源(如開發文件和討論),確保資訊平衡(有些來源強調 REST 的簡單性,有些則推 WebSocket 的即時性)。
| 項目 | REST API | WebSocket |
|---|---|---|
| 通訊模式 | 請求-回應(單向,客戶端主動請求伺服器) | 雙向、持久連接(伺服器可主動推送資料給客戶端) |
| 狀態管理 | 無狀態(每個請求獨立,不記住前次狀態) | 有狀態(連接維持中,記住連接狀態) |
| 延遲與效能 | 較高延遲(每次請求需開新連接,適合不頻繁操作);可快取回應,提升效能 | 低延遲(單一連接,適合頻繁小量資料交換);但維持連接需更多資源 |
| 優點 | - 簡單易懂,使用標準 HTTP,易擴展與快取 - 相容性高,支援代理與負載平衡 - 適合 CRUD 操作(新增、讀取、更新、刪除) | - 即時更新,低頭部開銷 - 雙向溝通,適合互動應用 - 資料傳輸效率高(無需重複頭部資訊) |
| 缺點 | - 不適合即時應用(需輪詢伺服器,浪費資源) - 頻繁請求時效能差 | - 擴展複雜(需管理連接狀態) - 瀏覽器相容性較低(舊版需後備) - 除錯較難 |
| 適合情境 | - 一般網頁應用(如登入、查詢資料、提交表單) - 行動 App 的後台同步 | - 即時聊天、線上遊戲、股票/體育即時更新 - 協作工具(如 Google Docs) |
從表格看,REST API 像「寄信」一樣穩當,WebSocket 像「視訊通話」一樣即時。選擇時,問自己:需要伺服器主動推送嗎?如果是的,就用 WebSocket;否則 REST 更簡單。
實際操作範例:用 JavaScript 實作 REST API 與 WebSocket
假設你有個簡單的聊天應用:用 REST API 來載入歷史訊息(GET 請求),用 WebSocket 來即時接收新訊息。伺服器端我假設是個簡單的後端(如 Node.js with Express),但你只需關注前端部分。
// 步驟 1: 定義 React 元件 - ChatApp
function ChatApp() {
// 步驟 2: 使用 useState 管理訊息列表和輸入框內容
const [messages, setMessages] = React.useState([]); // 存歷史和即時訊息
const [inputMessage, setInputMessage] = React.useState(''); // 輸入框內容
const [wsStatus, setWsStatus] = React.useState('未連線'); // WebSocket 狀態
const wsRef = React.useRef(null); // 儲存 WebSocket 實例
const messagesEndRef = React.useRef(null); // 用於自動滾動
// 步驟 3: 載入歷史訊息(REST API)
React.useEffect(() => {
async function loadHistoryMessages() {
try {
// 發 GET 請求到 REST API
const response = await fetch('http://localhost:3000/api/messages', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP 錯誤!狀態碼: ${response.status}`);
}
const historyMessages = await response.json();
// 更新訊息列表(將歷史訊息加入)
setMessages(historyMessages);
console.log('REST API 載入成功:', historyMessages);
} catch (error) {
console.error('REST API 載入失敗:', error);
setMessages(prev => [...prev, '載入歷史訊息失敗,請檢查網路']);
}
}
loadHistoryMessages(); // 執行載入
}, []); // 空依賴陣列,僅在元件掛載時執行
// 步驟 4: 建立 WebSocket 連線
React.useEffect(() => {
function connectWebSocket() {
wsRef.current = new WebSocket('ws://localhost:3000/ws');
// 連接成功
wsRef.current.onopen = () => {
setWsStatus('已連線');
setMessages(prev => [...prev, '已連線,即時訊息準備好了!']);
console.log('WebSocket 連接成功');
};
// 接收新訊息
wsRef.current.onmessage = (event) => {
const message = event.data;
setMessages(prev => [...prev, message]);
console.log('收到即時訊息:', message);
};
// 連接斷開,重試
wsRef.current.onclose = () => {
setWsStatus('斷線中');
setMessages(prev => [...prev, '連線斷開,3秒後重試']);
console.log('WebSocket 斷線,重試中...');
setTimeout(connectWebSocket, 3000);
};
// 錯誤處理
wsRef.current.onerror = (error) => {
console.error('WebSocket 錯誤:', error);
setMessages(prev => [...prev, '連線出錯,請檢查伺服器']);
};
}
connectWebSocket();
// 清理:元件卸載時關閉 WebSocket
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []); // 空依賴陣列,僅在掛載時執行
// 步驟 5: 自動滾動到最新訊息
React.useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]); // 當 messages 更新時滾動
// 步驟 6: 處理輸入框變化
const handleInputChange = (e) => {
setInputMessage(e.target.value);
};
// 步驟 7: 發送訊息(透過 WebSocket)
const sendMessage = () => {
if (!inputMessage.trim() || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
alert('請輸入訊息並確保連線中!');
return;
}
wsRef.current.send(inputMessage);
console.log('已發送:', inputMessage);
setInputMessage(''); // 清空輸入框
};
// 步驟 8: 按 Enter 鍵發送
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
sendMessage();
}
};
// 步驟 9: 渲染 UI(使用 Tailwind CSS)
return (
<div className="max-w-md mx-auto mt-8 p-4 bg-gray-100 rounded-lg shadow-lg">
<h1 className="text-2xl font-bold text-center mb-4">聊天室</h1>
<div className="text-sm text-gray-600 mb-2">連線狀態: {wsStatus}</div>
<div className="h-64 overflow-y-auto bg-white p-4 rounded-md mb-4">
{messages.map((msg, index) => (
<p key={index} className="mb-2 text-gray-800">{msg}</p>
))}
<div ref={messagesEndRef} />
</div>
<div className="flex gap-2">
<input
type="text"
value={inputMessage}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
placeholder="輸入訊息..."
className="flex-1 p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={sendMessage}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
>
傳送
</button>
</div>
</div>
);
}