# 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 來獲得最佳體驗。

更新於 閱讀次數 次