# Express vs Hono:核心概念差異
首先讓我們從整體架構來理解這兩個框架的設計哲學。Express 是一個成熟且功能完整的 Node.js 框架,它在過去十年主導了後端開發。而 Hono 則是新一代的輕量級框架,它的設計目標是「快速、輕量、並且能夠在任何 JavaScript 運行環境中執行」,包括 Node.js、Deno、Bun,甚至是 Cloudflare Workers 這類邊緣運算平台。
Express 的設計理念是提供一個最小化但可擴展的核心,然後透過大量的中介軟體生態系統來擴充功能。Hono 則追求極致的效能和簡潔性,它的 API 設計受到 Express 的啟發,但做了現代化的改進。
# 路由定義的差異
Express 版本:
import express from 'express' | |
import * as orderController from '../controllers/Order/orderAdmin.js' | |
const router = express.Router() | |
router.get( | |
'/brands/:brandId/stores/:storeId/orders', | |
authenticate('admin'), | |
requireRole('primary_system_admin', 'system_admin'), | |
requireBrandAccess, | |
requireStoreAccess, | |
orderController.getStoreOrders | |
) | |
export default router |
Hono 版本:
import { Hono } from 'hono' | |
import * as orderController from '../controllers/Order/orderAdmin.js' | |
const app = new Hono() | |
// Hono 的路由定義非常相似,但有些微妙的差異 | |
app.get( | |
'/brands/:brandId/stores/:storeId/orders', | |
authenticate('admin'), | |
requireRole('primary_system_admin', 'system_admin'), | |
requireBrandAccess, | |
requireStoreAccess, | |
orderController.getStoreOrders | |
) | |
export default app |
你會發現表面上看起來幾乎一樣!這是 Hono 的優點之一,它的 API 設計讓從 Express 遷移變得相對容易。但底層有很大的不同,讓我們深入看看。
# 中介軟體(Middleware)的重要差異
這是最關鍵的差異點:
Express 版本:
export const authenticate = (userType = 'admin') => async (req, res, next) => { | |
try { | |
if (userType === 'admin') { | |
if (!req.session?.adminId) { | |
return res.status(401).json({ | |
success: false, | |
message: '請先登入', | |
}) | |
} | |
const admin = await Admin.findById(req.session.adminId) | |
// 在 Express 中,我們直接修改 req 物件 | |
req.auth = { | |
id: admin._id, | |
type: 'admin', | |
role: admin.role, | |
} | |
} | |
next() // Express 使用 next () 來傳遞控制權 | |
} catch (error) { | |
return res.status(500).json({ | |
success: false, | |
message: '伺服器錯誤', | |
}) | |
} | |
} |
Hono 版本:
export const authenticate = (userType = 'admin') => { | |
return async (c, next) => { | |
try { | |
// 在 Hono 中,c 是 Context 物件,包含了請求和回應的所有資訊 | |
if (userType === 'admin') { | |
// 從 session 取得資料的方式可能需要調整 | |
const session = c.get('session') // Hono 使用 get/set 來存取上下文變數 | |
if (!session?.adminId) { | |
return c.json({ | |
success: false, | |
message: '請先登入', | |
}, 401) // Hono 的回應方法更簡潔,狀態碼作為第二個參數 | |
} | |
const admin = await Admin.findById(session.adminId) | |
// 在 Hono 中,我們使用 c.set () 來儲存資料供後續中介軟體使用 | |
c.set('auth', { | |
id: admin._id, | |
type: 'admin', | |
role: admin.role, | |
}) | |
} | |
// Hono 也使用 await next () 來傳遞控制權 | |
// 但它是 async 的,這讓你可以在 next () 之後執行程式碼 | |
await next() | |
// 這裡可以加入「後處理」邏輯,例如記錄回應時間 | |
// 這在 Express 中比較難實現 | |
} catch (error) { | |
return c.json({ | |
success: false, | |
message: '伺服器錯誤', | |
}, 500) | |
} | |
} | |
} |
關鍵差異在於:
Express 使用三個分離的參數 (req, res, next) ,而 Hono 將所有東西整合進一個 Context 物件 c 。這個設計讓 Hono 更容易處理不同的運行環境(因為不同環境的請求和回應物件可能不同)。
# 控制器(Controller)的改寫
Express 版本:
export const getStoreOrders = asyncHandler(async (req, res) => { | |
const { storeId } = req.params | |
const options = { | |
status: req.query.status, | |
page: parseInt(req.query.page, 10) || 1, | |
limit: parseInt(req.query.limit, 10) || 20, | |
} | |
const result = await orderService.getStoreOrders(storeId, options) | |
res.json({ | |
success: true, | |
orders: result.orders, | |
pagination: result.pagination, | |
}) | |
}) |
Hono 版本:
export const getStoreOrders = async (c) => { | |
// 從 Context 中取得路徑參數 | |
const { storeId } = c.req.param() | |
// 取得 query 參數 | |
const status = c.req.query('status') | |
const page = parseInt(c.req.query('page') || '1', 10) | |
const limit = parseInt(c.req.query('limit') || '20', 10) | |
const options = { status, page, limit } | |
const result = await orderService.getStoreOrders(storeId, options) | |
// 回應 JSON | |
return c.json({ | |
success: true, | |
orders: result.orders, | |
pagination: result.pagination, | |
}) | |
} |
注意幾個重要的差異:
首先,Hono 使用 c.req.param() 來取得路徑參數,而 Express 直接從 req.params 取得。其次,Hono 使用 c.req.query('key') 來取得個別的 query 參數,而不是直接存取 req.query 物件。最後,在 Hono 中我們需要 return 回應,而 Express 則是調用 res.json() 就結束了。
# 錯誤處理的差異
Express 版本:
export const errorHandler = (err, req, res, next) => { | |
console.error('API Error:', err) | |
if (err.name === 'ValidationError') { | |
return res.status(400).json({ | |
success: false, | |
message: '資料驗證失敗', | |
}) | |
} | |
res.status(500).json({ | |
success: false, | |
message: '伺服器錯誤', | |
}) | |
} |
Hono 版本:
import { HTTPException } from 'hono/http-exception' | |
// Hono 有內建的錯誤處理機制 | |
app.onError((err, c) => { | |
console.error('API Error:', err) | |
// Hono 推薦使用 HTTPException | |
if (err instanceof HTTPException) { | |
return c.json({ | |
success: false, | |
message: err.message, | |
}, err.status) | |
} | |
if (err.name === 'ValidationError') { | |
return c.json({ | |
success: false, | |
message: '資料驗證失敗', | |
}, 400) | |
} | |
return c.json({ | |
success: false, | |
message: '伺服器錯誤', | |
}, 500) | |
}) | |
// 自定義錯誤類別的改寫 | |
export class AppError extends HTTPException { | |
constructor(message, statusCode) { | |
super(statusCode, { message }) | |
} | |
} |
Hono 提供了 HTTPException 作為標準的錯誤類別,這比 Express 的自訂錯誤處理更加一致和強型別。
# 參數驗證的改寫
Hono 版本(使用內建的 validator):
import { validator } from 'hono/validator' | |
import { z } from 'zod' // Hono 推薦搭配 Zod 使用 | |
// Hono 推薦使用 Zod 進行驗證,更加型別安全 | |
export const validateOrder = validator('json', (value, c) => { | |
// 定義驗證 schema | |
const schema = z.object({ | |
orderType: z.enum(['dine_in', 'takeout', 'delivery']), | |
items: z.array(z.object({ | |
itemType: z.enum(['dish', 'bundle']), | |
quantity: z.number().min(1), | |
subtotal: z.number(), | |
})), | |
total: z.number(), | |
}) | |
const parsed = schema.safeParse(value) | |
if (!parsed.success) { | |
return c.json({ | |
success: false, | |
message: '輸入資料驗證失敗', | |
errors: parsed.error.flatten(), | |
}, 400) | |
} | |
return parsed.data | |
}) | |
// 使用方式 | |
app.post('/orders', validateOrder, async (c) => { | |
const validatedData = c.req.valid('json') | |
//validatedData 已經被驗證和型別檢查過了 | |
}) |
Hono 的驗證機制與 TypeScript 整合得更好,如果你使用 TypeScript,你會得到完整的型別推斷。
# Session 管理的重要注意事項
這是一個需要特別小心的地方!一般用戶登入驗證大量使用了 req.session ,但 Hono 並沒有內建的 session 支援。你需要額外安裝中介軟體:
import { Hono } from 'hono' | |
import { getCookie, setCookie } from 'hono/cookie' | |
import { sign, verify } from 'hono/jwt' | |
const app = new Hono() | |
// 你可能需要自己實現 session 邏輯 | |
// 或者使用第三方套件,但 Hono 生態系統還不如 Express 成熟 | |
// 簡單的 JWT-based session 範例 | |
app.use('*', async (c, next) => { | |
const token = getCookie(c, 'session') | |
if (token) { | |
try { | |
const payload = await verify(token, process.env.JWT_SECRET) | |
c.set('session', payload) | |
} catch (e) { | |
// Token 無效 | |
} | |
} | |
await next() | |
}) |
這是從 Express 遷移到 Hono 時最大的挑戰之一。你需要重新思考 session 管理的實現方式。
# 主應用程式的組裝
最後,讓我們看看如何組裝整個應用程式:
Express 版本:
import express from 'express' | |
import orderAdminRoutes from './routes/orderAdmin.js' | |
import orderCustomerRoutes from './routes/orderCustomer.js' | |
const app = express() | |
app.use(express.json()) | |
app.use('/api/admin', orderAdminRoutes) | |
app.use('/api/customer', orderCustomerRoutes) | |
app.listen(3000) |
Hono 版本:
import { Hono } from 'hono' | |
import { serve } from '@hono/node-server' | |
import orderAdminRoutes from './routes/orderAdmin.js' | |
import orderCustomerRoutes from './routes/orderCustomer.js' | |
const app = new Hono() | |
// Hono 會自動解析 JSON,不需要額外的中介軟體 | |
// 路由掛載方式相同 | |
app.route('/api/admin', orderAdminRoutes) | |
app.route('/api/customer', orderCustomerRoutes) | |
// 啟動伺服器的方式不同 | |
serve({ | |
fetch: app.fetch, | |
port: 3000, | |
}) |
# 核心重點功能總結
讓我為你總結 Hono 的核心優勢和需要注意的地方:
Hono 的核心優勢:
第一,效能卓越。Hono 的路由匹配演算法比 Express 快很多,特別是在有大量路由的情況下。第二,跨平台執行能力。同一套程式碼可以在 Node.js、Deno、Bun、Cloudflare Workers 等環境中運行。第三,更好的 TypeScript 支援。Hono 從頭開始就考慮了型別安全,如果你使用 TypeScript,會有很好的開發體驗。第四,內建了許多實用功能,像是 CORS、JWT、Cookie 處理等,不需要安裝額外套件。
需要特別小心的地方:
首先,生態系統還不夠成熟。Express 有成千上萬的中介軟體可用,而 Hono 的生態系統還在建立中。其次,學習曲線雖然不陡,但概念上的轉換需要時間,特別是 Context 物件的使用方式。第三,Session 管理需要自己實現或尋找適合的解決方案。第四,某些 Express 的中介軟體無法直接在 Hono 中使用,需要改寫。第五,社群資源和教學資料相對較少,遇到問題時可能需要自己摸索。
遷移建議:
如果你想從 Express 遷移到 Hono,我建議採取漸進式的方法。首先,先在一個小型專案或新功能上試用 Hono,熟悉它的 API 和概念。其次,重點關注中介軟體的改寫,這是最大的改變。第三,建立一套通用的工具函數來處理常見任務,比如 session 管理、錯誤處理等。最後,充分利用 Hono 的型別系統,如果可能的話使用 TypeScript 來獲得最佳體驗。