上一篇整理了一些基礎知識,今天就把重點放在應用的部分,建立簡單的伺服器以及設計API
其實前面第四天已經寫過一次範例了
const http = require('node:http');
const server = http.createServer((req, res) => {
const now = new Date();
const greeting = now.getHours() < 12 ? '早安' :
now.getHours() < 18 ? '午安' : '晚安';
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`${greeting}!現在是 ${now.toLocaleString('zh-TW')}`);
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`伺服器啟動了!快去 http://localhost:${PORT} 看看吧`);
});
createServer()
會建立一個新的 HTTP 伺服器req
包含了請求的所有資訊res
用來發送回應給客戶端listen()
讓伺服器開始監聽指定端口接下來看看如何獲取請求的詳細資訊:
const http = require('node:http');
const url = require('node:url');
const server = http.createServer((req, res) => {
// 解析 URL
const parsedUrl = url.parse(req.url, true);
// 取得各種請求資訊
const method = req.method; // GET, POST, PUT, DELETE...
const pathname = parsedUrl.pathname; // URL 路徑
const query = parsedUrl.query; // 查詢參數
const headers = req.headers; // 請求標頭
// 回應請求資訊
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
method,
pathname,
query,
userAgent: headers['user-agent']
}, null, 2));
});
server.listen(3000, () => {
console.log('伺服器啟動,請訪問 http://localhost:3000/test?name=John');
});
const http = require('node:http');
const url = require('node:url');
function handleGet(req, res, pathname, query) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (pathname === '/users') {
// 模擬用戶資料
const users = [
{ id: 1, name: '張三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
];
res.writeHead(200);
res.end(JSON.stringify({ users, query }));
}
else if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>API 測試頁面</h1>
<p>試試看這些連結:</p>
<ul>
<li><a href="/users">取得用戶列表</a></li>
<li><a href="/users?page=1&limit=2">分頁查詢</a></li>
</ul>
`);
}
else {
res.writeHead(404);
res.end(JSON.stringify({ error: '找不到頁面' }));
}
}
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
if (req.method === 'GET') {
handleGet(req, res, parsedUrl.pathname, parsedUrl.query);
} else {
res.writeHead(405);
res.end(JSON.stringify({ error: '不支援的請求方法' }));
}
});
server.listen(3000);
POST 請求比較複雜,因為需要接收請求主體的資料:
const http = require('node:http');
function handlePost(req, res, pathname) {
if (pathname === '/users') {
let body = '';
// 資料可能會分批傳送,需要逐步收集
req.on('data', chunk => {
body += chunk.toString();
// 防止過大的資料攻擊
if (body.length > 1000000) { // 1MB 限制
res.writeHead(413);
res.end(JSON.stringify({ error: '資料過大' }));
req.destroy();
return;
}
});
// 資料接收完成
req.on('end', () => {
try {
const userData = JSON.parse(body);
// 簡單驗證
if (!userData.name || !userData.email) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '姓名和 email 為必填' }));
return;
}
// 模擬創建用戶
const newUser = {
id: Date.now(),
...userData,
createdAt: new Date().toISOString()
};
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '用戶創建成功',
user: newUser
}));
} catch (err) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'JSON 格式錯誤' }));
}
});
// 處理請求錯誤
req.on('error', (err) => {
console.error('請求錯誤:', err);
res.writeHead(500);
res.end(JSON.stringify({ error: '伺服器錯誤' }));
});
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: '找不到 API 端點' }));
}
}
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
const pathname = require('node:url').parse(req.url).pathname;
handlePost(req, res, pathname);
} else {
res.writeHead(405);
res.end('只支援 POST 請求');
}
});
server.listen(3000);
有時候我們的伺服器需要向其他 API 發送請求,這時候就需要用到 HTTP 客戶端:
const http = require('node:http');
function sendGetRequest(url) {
return new Promise((resolve, reject) => {
console.log('發送 GET 請求到:', url);
http.get(url, (res) => {
let data = '';
console.log('回應狀態碼:', res.statusCode);
// 收集回應資料
res.on('data', (chunk) => {
data += chunk;
});
// 接收完成
res.on('end', () => {
try {
const parsedData = JSON.parse(data);
resolve({
statusCode: res.statusCode,
headers: res.headers,
data: parsedData
});
} catch (err) {
// 如果不是 JSON,就回傳原始文字
resolve({
statusCode: res.statusCode,
headers: res.headers,
data: data
});
}
});
}).on('error', (err) => {
reject(err);
});
});
}
// 使用範例
async function testGetRequest() {
try {
const result = await sendGetRequest('http://jsonplaceholder.typicode.com/posts/1');
console.log('請求成功:', result.data.title);
} catch (err) {
console.error('請求失敗:', err.message);
}
}
// testGetRequest();
const http = require('node:http');
function sendPostRequest(hostname, port, path, data) {
return new Promise((resolve, reject) => {
const postData = JSON.stringify(data);
const options = {
hostname: hostname,
port: port,
path: path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
console.log('發送 POST 請求到:', `${hostname}:${port}${path}`);
const req = http.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
data: responseData
});
});
});
req.on('error', (err) => {
reject(err);
});
// 設定超時
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('請求超時'));
});
// 發送資料
req.write(postData);
req.end();
});
}
// 使用範例
async function testPostRequest() {
try {
const result = await sendPostRequest(
'jsonplaceholder.typicode.com',
80,
'/posts',
{
title: '測試文章',
body: '這是測試內容',
userId: 1
}
);
console.log('POST 請求成功:', result.statusCode);
console.log('回應資料:', JSON.parse(result.data));
} catch (err) {
console.error('POST 請求失敗:', err.message);
}
}
// testPostRequest();
const http = require('node:http');
const fs = require('node:fs/promises');
const path = require('node:path');
// 檔案類型對應表
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.txt': 'text/plain; charset=utf-8'
};
async function serveStaticFile(filePath, publicDir = './public') {
// 安全檢查:防止目錄遍歷攻擊
const safePath = path.normalize(filePath).replace(/^(\.\.[\/\\])+/, '');
const fullPath = path.join(publicDir, safePath);
// 確保請求的檔案在允許的目錄內
if (!fullPath.startsWith(path.resolve(publicDir))) {
throw new Error('禁止存取');
}
try {
const stats = await fs.stat(fullPath);
if (stats.isDirectory()) {
// 如果是目錄,嘗試提供 index.html
const indexPath = path.join(fullPath, 'index.html');
try {
await fs.access(indexPath);
return await serveStaticFile(path.join(safePath, 'index.html'), publicDir);
} catch {
throw new Error('目錄沒有 index.html');
}
}
// 讀取檔案
const fileContent = await fs.readFile(fullPath);
// 確定檔案類型
const ext = path.extname(fullPath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
return {
content: fileContent,
contentType: contentType,
size: stats.size,
lastModified: stats.mtime.toUTCString()
};
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error('檔案不存在');
}
throw err;
}
}
const server = http.createServer(async (req, res) => {
const pathname = require('node:url').parse(req.url).pathname;
console.log('請求檔案:', pathname);
try {
const fileData = await serveStaticFile(pathname);
res.writeHead(200, {
'Content-Type': fileData.contentType,
'Content-Length': fileData.size,
'Last-Modified': fileData.lastModified,
'Cache-Control': 'public, max-age=3600' // 1小時快取
});
res.end(fileData.content);
} catch (err) {
console.log('檔案服務錯誤:', err.message);
if (err.message === '檔案不存在') {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>404 - 找不到檔案</h1>
<p>你要找的檔案 <code>${pathname}</code> 不存在</p>
<a href="/">回首頁</a>
`);
} else {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('伺服器錯誤');
}
}
});
server.listen(3000, async () => {
console.log('靜態檔案伺服器啟動在 http://localhost:3000');
console.log('請將檔案放在 ./public 目錄');
// 自動建立範例檔案
try {
await fs.mkdir('./public', { recursive: true });
const indexContent = `
<!DOCTYPE html>
<html>
<head>
<title>靜態檔案測試</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>靜態檔案伺服器測試</h1>
<p>這個檔案位於 ./public/index.html</p>
<p>你可以在 public 目錄中添加更多檔案</p>
</body>
</html>`;
await fs.writeFile('./public/index.html', indexContent);
console.log('已建立範例 index.html');
} catch (err) {
console.log('請手動建立 public 目錄');
}
});
const http = require('node:http');
class HttpServer {
constructor() {
this.server = null;
this.isShuttingDown = false;
}
createServer() {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res).catch(err => {
console.error('請求處理錯誤:', err);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: '伺服器內部錯誤' }));
}
});
});
// 伺服器錯誤處理
this.server.on('error', (err) => {
console.error('伺服器錯誤:', err);
if (err.code === 'EADDRINUSE') {
console.error('端口已被使用');
process.exit(1);
}
});
// 客戶端錯誤處理
this.server.on('clientError', (err, socket) => {
console.error('客戶端錯誤:', err.message);
if (!socket.destroyed) {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
});
return this.server;
}
async handleRequest(req, res) {
const startTime = Date.now();
const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} from ${clientIP}`);
// 設定請求超時
req.setTimeout(30000, () => {
if (!res.headersSent) {
res.writeHead(408);
res.end('請求超時');
}
});
// 檢查伺服器狀態
if (this.isShuttingDown) {
res.writeHead(503, { 'Connection': 'close' });
res.end('伺服器正在關閉');
return;
}
try {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === '/health') {
await this.handleHealthCheck(res);
} else if (url.pathname === '/') {
await this.handleHomePage(res);
} else {
res.writeHead(404);
res.end('找不到頁面');
}
// 記錄回應時間
const responseTime = Date.now() - startTime;
console.log(`請求完成,耗時 ${responseTime}ms`);
} catch (err) {
throw err;
}
}
async handleHealthCheck(res) {
const uptime = process.uptime();
const memUsage = process.memoryUsage();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'healthy',
uptime: Math.floor(uptime),
memory: Math.round(memUsage.heapUsed / 1024 / 1024),
timestamp: new Date().toISOString()
}));
}
async handleHomePage(res) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>HTTP 伺服器</h1>
<p>伺服器運行正常</p>
<ul>
<li><a href="/health">健康檢查</a></li>
</ul>
`);
}
listen(port) {
return new Promise((resolve, reject) => {
if (!this.server) {
this.createServer();
}
this.server.listen(port, (err) => {
if (err) {
reject(err);
} else {
console.log(`伺服器啟動在端口 ${port}`);
resolve();
}
});
});
}
// 關閉
async gracefulShutdown() {
this.isShuttingDown = true;
return new Promise((resolve) => {
if (!this.server) {
resolve();
return;
}
this.server.close((err) => {
if (err) {
console.error('關閉伺服器時出錯:', err);
} else {
console.log('伺服器已關閉');
}
resolve();
});
// 10秒後強制關閉
setTimeout(() => {
console.log('強制關閉伺服器');
resolve();
}, 10000);
});
}
}
// 使用範例
const server = new HttpServer();
server.listen(3000).catch(err => {
console.error('伺服器啟動失敗:', err);
process.exit(1);
});
// 關閉處理
process.on('SIGTERM', async () => {
await server.gracefulShutdown();
process.exit(0);
});
process.on('SIGINT', async () => {
await server.gracefulShutdown();
process.exit(0);
});
// 一個完整的請求處理流程
const server = http.createServer((req, res) => {
// 1. 解析請求
const method = req.method;
const url = req.url;
// 2. 處理請求主體(如果有的話)
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
// 3. 處理業務邏輯
// 4. 發送回應
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('處理完成');
});
});
明天再來整理 RESTful API,也再多整理其他 PUT、DELETE、PATCH 等 API 範例,
後續就可以開始往框架前進了。