iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
佛心分享-IT 人自學之術

欸欸!! 這是我的學習筆記系列 第 9

Day9 - Express.js 基礎知識 & 安裝

  • 分享至 

  • xImage
  •  

前言

先前我們用原生 Node.js HTTP 模組寫的那些程式碼雖然功能完整,但功能延伸越來越多之後,總是顯得有點雜亂,想整理又不知道如何下手。相較於前端有 Vue、React 等框架,基於 Node.js 也有一些好用的框架如 Express.js、Koa.js、NestJS...等等。
接下來的文章就會圍繞在 Express.js 做整理。

原生 HTTP 模組的痛點 & Express 解決的問題

舉個先前用過的例子:

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
});

產生的問題

  1. 路由處理複雜:需要大量的 if-else 判斷
  2. 請求解析麻煩:手動解析 JSON、查詢參數等
  3. 程式碼重複:CORS、錯誤處理等邏輯重複出現
  4. 擴展困難:新增功能需要修改核心邏輯
  5. 沒有結構:所有程式碼混在一起,難以維護

Express.js 的誕生背景

Express.js 在 2010 年由 TJ Holowaychuk 創建,靈感來自於 Ruby 的 Sinatra 框架。它的核心理念是:

"用最少的程式碼,做最多的事情"

Express 不是要取代 Node.js,而是在 Node.js HTTP 模組的基礎上,提供一層更友善的抽象層。它就像是給原生 HTTP 模組穿上了一件舒適的外衣。

Express 解決了什麼問題?

  1. 路由簡化app.get('/users', handler)
  2. 中間件系統:可重用的功能模組
  3. 請求解析:自動解析 JSON、URL 參數等
  4. 擴展性:插件生態系統豐富
  5. 程式碼組織:提供架構指導

安裝

# 建立專案資料夾
mkdir my-express-app
cd my-express-app

# 初始化 npm 專案
npm init -y

# 安裝 Express
npm install express

# 安裝開發依賴
npm install --save-dev nodemon eslint

package.json 設定

{
  "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"
  }
}

ESLint 基本設定

建立 .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'
  }
};

建立一個 Express 伺服器

建立 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}`);
});

與原生 HTTP 模組對比

讓我們看看同樣的功能,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);

內建和常用中間件

解析 JSON 請求

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);

與原生 HTTP 的對比總結

讓我們回顧一下,同樣實作一個簡單的 RESTful API,兩種方式的差異:

原生 HTTP 模組版本

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);

Express 版本

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 的優勢:

  • 程式碼簡潔性
    • 原生:需要 20+ 行處理基本路由
    • Express:同樣功能只需要 10 行
  • 可讀性
    • 原生:大量的 if-else 判斷,邏輯混雜
    • Express:語義化的路由定義,一目了然
  • 維護性
    • 原生:新增路由需要修改核心邏輯
    • Express:新增路由只要加一行
  • 擴展性
    • 原生:要自己實作所有功能
    • Express:豐富的中間件生態系統
  • 錯誤處理
    • 原生:需要在每個地方處理錯誤
    • Express:統一的錯誤處理中間件

參考

Express Wiki
Express 官網


上一篇
Day8 - Node.js RESTful API
下一篇
Day10 - Express 資料夾結構 & MVC 設計模式
系列文
欸欸!! 這是我的學習筆記10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言