先前我們用原生 Node.js HTTP 模組寫的那些程式碼雖然功能完整,但功能延伸越來越多之後,總是顯得有點雜亂,想整理又不知道如何下手。相較於前端有 Vue、React 等框架,基於 Node.js 也有一些好用的框架如 Express.js、Koa.js、NestJS...等等。
接下來的文章就會圍繞在 Express.js 做整理。
舉個先前用過的例子:
const http = require('node:http');
const url = require('node:url');
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const method = req.method;
const pathname = parsedUrl.pathname;
// 手動解析路由
if (method === 'GET' && pathname === '/users') {
// 處理用戶列表
} else if (method === 'POST' && pathname === '/users') {
// 處理創建用戶
} else if (method === 'GET' && pathname.match(/^\/users\/\d+$/)) {
// 處理單一用戶...
}
// ... 更多 if-else
});
Express.js 在 2010 年由 TJ Holowaychuk 創建,靈感來自於 Ruby 的 Sinatra 框架。它的核心理念是:
"用最少的程式碼,做最多的事情"
Express 不是要取代 Node.js,而是在 Node.js HTTP 模組的基礎上,提供一層更友善的抽象層。它就像是給原生 HTTP 模組穿上了一件舒適的外衣。
app.get('/users', handler)
# 建立專案資料夾
mkdir my-express-app
cd my-express-app
# 初始化 npm 專案
npm init -y
# 安裝 Express
npm install express
# 安裝開發依賴
npm install --save-dev nodemon eslint
{
"name": "my-express-app",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"lint": "eslint ."
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1",
"eslint": "^8.0.0"
}
}
建立 .eslintrc.js
:
module.exports = {
env: {
node: true,
es2021: true
},
extends: 'eslint:recommended',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'indent': ['error', 2],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'no-console': 'warn',
'no-unused-vars': 'error'
}
};
建立 app.js
:
const express = require('express');
// 建立 Express 應用實例
const app = express();
const PORT = 3000;
// 定義一個簡單的路由
app.get('/', (req, res) => {
res.send('Hello Express!');
});
// 啟動伺服器
app.listen(PORT, () => {
console.log(`伺服器運行在 http://localhost:${PORT}`);
});
讓我們看看同樣的功能,Express 和原生寫法的差異:
原生 HTTP 模組:
const http = require('node:http');
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!');
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('伺服器運行在 http://localhost:3000');
});
Express 版本:
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('伺服器運行在 http://localhost:3000');
});
差異很明顯:Express 把複雜的底層操作都抽象化了。
const express = require('express');
const app = express();
// HTTP 方法對應的路由
app.get('/users', (req, res) => {
res.send('取得所有用戶');
});
app.post('/users', (req, res) => {
res.send('創建新用戶');
});
app.put('/users/:id', (req, res) => {
res.send(`更新用戶 ${req.params.id}`);
});
app.delete('/users/:id', (req, res) => {
res.send(`刪除用戶 ${req.params.id}`);
});
app.listen(3000);
const express = require('express');
const app = express();
// 路由參數
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.json({
message: `取得用戶資料`,
userId: userId,
type: typeof userId
});
});
// 多個參數
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({
message: '取得用戶的特定文章',
userId,
postId
});
});
// 可選參數
app.get('/products/:id?', (req, res) => {
if (req.params.id) {
res.send(`產品 ID: ${req.params.id}`);
} else {
res.send('所有產品');
}
});
app.listen(3000);
const express = require('express');
const app = express();
// 查詢參數自動解析
app.get('/search', (req, res) => {
const { q, page, limit } = req.query;
res.json({
searchTerm: q,
page: page ? parseInt(page) : 1,
limit: limit ? parseInt(limit) : 10,
allQuery: req.query
});
});
// 訪問: /search?q=express&page=2&limit=20
app.listen(3000);
const express = require('express');
const app = express();
// 萬用字元匹配
app.get('/files/*', (req, res) => {
const filePath = req.params[0]; // 萬用字元後的內容
res.send(`檔案路徑: ${filePath}`);
});
// 正規表達式
app.get(/.*fly$/, (req, res) => {
res.send('以 fly 結尾的路徑');
});
// 多個路徑匹配同一個處理器
app.get(['/about', '/info'], (req, res) => {
res.send('關於我們');
});
app.listen(3000);
中間件是 Express 的核心概念。可以把它想像成生產線上的工作站,每個工作站都有特定的任務。
const express = require('express');
const app = express();
// 這是一個中間件函數
function logger(req, res, next) {
const timestamp = new Date().toISOString();
console.log(`${timestamp} - ${req.method} ${req.url}`);
// 呼叫 next() 將控制權傳給下一個中間件
next();
}
// 使用中間件
app.use(logger);
// 路由處理器也是一種中間件
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);
const express = require('express');
const app = express();
// 中間件 1
app.use((req, res, next) => {
console.log('中間件 1');
next();
});
// 中間件 2
app.use((req, res, next) => {
console.log('中間件 2');
next();
});
// 路由處理器
app.get('/', (req, res) => {
console.log('路由處理器');
res.send('完成!');
});
// 中間件 3(這個不會執行,因為上面的路由已經結束了回應)
app.use((req, res, next) => {
console.log('中間件 3');
next();
});
app.listen(3000);
// 訪問 / 時會印出:
// 中間件 1
// 中間件 2
// 路由處理器
const express = require('express');
const app = express();
// 只在特定路徑執行的中間件
app.use('/api', (req, res, next) => {
console.log('這只在 /api 路徑下執行');
next();
});
// 只在特定 HTTP 方法執行的中間件
app.use('/users', (req, res, next) => {
if (req.method === 'POST') {
console.log('有人要創建新用戶');
}
next();
});
app.get('/api/users', (req, res) => {
res.send('API 用戶列表');
});
app.get('/users', (req, res) => {
res.send('普通用戶列表');
});
app.listen(3000);
const express = require('express');
const app = express();
// 解析 JSON 請求主體
app.use(express.json());
// 解析 URL 編碼的請求主體(表單資料)
app.use(express.urlencoded({ extended: true }));
app.post('/users', (req, res) => {
console.log('收到的資料:', req.body);
// 現在可以直接使用 req.body
const { name, email } = req.body;
res.json({
message: '用戶創建成功',
user: { name, email }
});
});
app.listen(3000);
測試:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"張三","email":"zhang@example.com"}'
const express = require('express');
const app = express();
// 提供靜態檔案
app.use(express.static('public'));
// 也可以指定虛擬路徑
app.use('/assets', express.static('public'));
app.get('/', (req, res) => {
res.send(`
<h1>靜態檔案測試</h1>
<p>試試看訪問:</p>
<ul>
<li><a href="/style.css">直接存取</a></li>
<li><a href="/assets/style.css">透過虛擬路徑</a></li>
</ul>
`);
});
app.listen(3000);
const express = require('express');
const app = express();
app.use(express.json());
// 正常路由
app.get('/users/:id', (req, res, next) => {
const userId = parseInt(req.params.id);
if (isNaN(userId)) {
// 創建錯誤並傳遞給錯誤處理中間件
const error = new Error('用戶 ID 必須是數字');
error.status = 400;
return next(error);
}
if (userId > 100) {
// 也可以直接拋出錯誤
const error = new Error('用戶不存在');
error.status = 404;
return next(error);
}
res.json({ id: userId, name: '測試用戶' });
});
// 錯誤處理中間件(一定要放在最後)
app.use((err, req, res, next) => {
const status = err.status || 500;
const message = err.message || '伺服器錯誤';
console.error(`錯誤: ${message}`);
res.status(status).json({
error: message,
status: status
});
});
// 404 處理(也要放在最後,但在錯誤處理之前)
app.use((req, res) => {
res.status(404).json({
error: '找不到頁面',
path: req.url
});
});
app.listen(3000);
const express = require('express');
const app = express();
// 模擬非同步操作
function asyncOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('操作成功');
} else {
reject(new Error('操作失敗'));
}
}, 1000);
});
}
// 處理非同步錯誤的包裝函數
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/async-test', asyncHandler(async (req, res) => {
const result = await asyncOperation();
res.json({ result });
}));
// 錯誤處理中間件
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message
});
});
app.listen(3000);
讓我們回顧一下,同樣實作一個簡單的 RESTful API,兩種方式的差異:
const http = require('node:http');
const url = require('node:url');
const users = [
{ id: 1, name: '張三' },
{ id: 2, name: '李四' }
];
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
const method = req.method;
const pathname = parsedUrl.pathname;
// 設定 CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json');
if (method === 'GET' && pathname === '/users') {
res.writeHead(200);
res.end(JSON.stringify(users));
} else if (method === 'GET' && pathname.match(/^\/users\/\d+$/)) {
const id = parseInt(pathname.split('/')[2]);
const user = users.find(u => u.id === id);
if (user) {
res.writeHead(200);
res.end(JSON.stringify(user));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: '找不到用戶' }));
}
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: '找不到頁面' }));
}
});
server.listen(3000);
const express = require('express');
const app = express();
const users = [
{ id: 1, name: '張三' },
{ id: 2, name: '李四' }
];
// 自動處理 CORS、JSON 回應等
app.use(express.json());
app.get('/users', (req, res) => {
res.json(users);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (user) {
res.json(user);
} else {
res.status(404).json({ error: '找不到用戶' });
}
});
app.listen(3000);
經過這個對比,我們可以清楚看到 Express 的優勢: