iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0

嘿!你有沒有發現,在撰寫程式的時候,總是會有一些程式碼不斷地重複出現?它們就像是影分身術一樣,在不同的地方出現,做著類似的事情。這些重複的程式碼不僅讓我們的程式變得冗長,也讓修改和維護變得更加困難。
但是,身為一個聰明的開發者,我們怎麼能讓這些重複的程式碼繼續存在呢?就像孫悟空掌握了七十二變一樣,我們也要學會如何把這些重複的程式碼"變"出去,讓我們的程式碼變得更加簡潔和優雅。
除了重複的程式碼,錯誤處理也是我們經常要面對的問題。就像在玩遊戲時遇到 boss 一樣,錯誤總是在關鍵時刻出現,讓我們的程式陷入困境。但是,如果我們提前準備好對策,就可以輕鬆地應對這些錯誤,讓我們的程式穩定運行。
所以,讓我們一起來看看如何通過抽取重複的程式碼和實現全面的錯誤處理,讓我們的程式變得更加強大和可靠吧!相信學會這些技巧,你也能成為一名出色的開發者!

製作可重用的函式

首先,我們在專案的根目錄下新增一個名為 utils 的資料夾,用於存放我們抽取出來的工具函式。
utils 資料夾中,我們新增一個名為 handleSuccess.js 的檔案,並添加以下程式碼:

// utils/handleSuccess.js

function handleSuccess(res, data, message, statusCode) {
  res.status(statusCode).json({
    statusCode: statusCode || 200,
    status: "success",
    message,
    data,
  });
}

module.exports = handleSuccess;

這個 handleSuccess 函式用於處理成功回應。它接收四個參數:

  • res: Express 的回應物件
  • data: 要在回應中返回的資料
  • message: 成功訊息
  • statusCode: HTTP 狀態碼,預設為 200
    函式內部使用 res.status() 方法設置 HTTP 狀態碼,然後使用 res.json() 方法返回一個包含狀態碼、狀態、訊息和資料的 JSON 物件。

接下來,我們在 utils 資料夾中新增一個名為 appError.js 的檔案,並添加以下程式碼:

// utils/appError.js

const appError = (httpStatus, errMessage, next) => {
  const error = new Error(errMessage);
  error.message = errMessage;
  error.statusCode = httpStatus;
  error.isOperational = true;
  return error;
};

module.exports = appError;

這個 appError 函式用於創建自定義的錯誤物件。它接收三個參數:

  • httpStatus:HTTP 狀態碼
  • errMessage:錯誤訊息
  • next:Express 的 next 函式
    函式內部創建一個新的 Error 物件,並設置錯誤的屬性,包括錯誤訊息、HTTP 狀態碼和一個表示錯誤是否為操作錯誤的標誌 isOperational。最後,函式返回這個錯誤物件。
    這個錯誤物件會被傳遞到 Express 的錯誤處理中間件中,我們稍後會在 app.js 中進行處理。
    最後,我們在 utils 資料夾中新增一個名為 handleErrorAsync.js 的檔案,並添加以下程式碼:
// utils/handleErrorAsync.js

const handleErrorAsync = function handleErrorAsync(func) {
  return function (req, res, next) {
    func(req, res, next).catch(function (error) {
      return next(error);
    });
  };
};

module.exports = handleErrorAsync;

這個 handleErrorAsync 函式是一個高階函式,用於處理異步函式的錯誤。它接收一個異步函式 func 作為參數,並返回一個新的函式。
這個新的函式會執行原始的異步函式 func,並使用 catch 方法捕獲可能發生的錯誤。如果發生錯誤,就將錯誤傳遞給 Express 的 next 函式,交給錯誤處理中間件進行處理。
使用 handleErrorAsync 函式可以簡化異步函式的錯誤處理,避免了繁瑣的 try...catch 語句。

全面的錯誤處理

有了上面的工具函式,我們就可以在 Express 應用程式中實現更全面的錯誤處理了。
我們回到 app.js 檔案:
添加一個全域的未捕獲異常處理程式:

// 程式出現重大錯誤時
process.on("uncaughtException", (err) => {
// 記錄錯誤下來,等到服務都處理完後,停掉該 process
  console.error("Uncaughted Exception!");
  console.error(err);
  process.exit(1);
});

這個處理程式會在程式出現未捕獲的異常時觸發。它會記錄錯誤訊息,並在服務處理完所有請求後,停止該 process。

接下來,我們添加一個 404 錯誤處理中間件:

// 404 錯誤
app.use(function (req, res, next) {
  res.status(404).json({
    status: "error",
    message: "無此路由資訊",
    path: req.originalUrl,
  });
});

這個中間件會在沒有匹配到任何路由時觸發,返回一個 404 錯誤訊息。

然後,我們添加一個開發環境的錯誤處理函式:

const resError = (err, res) => {
  res.status(err.statusCode).json({
    message: err.message,
    statusCode: err.statusCode,
    isOperational: err.isOperational,
    stack: err.stack,
  });
};

這個函式用於處理開發環境下的錯誤,返回詳細的錯誤訊息,包括錯誤堆疊。

接下來,我們添加一個全域的錯誤處理中間件:

app.use(function (err, req, res, next) {
  err.statusCode = err.statusCode || 500;

  if (err.name === "ValidationError") {
    err.message = "資料欄位未填寫正確,請重新輸入!";
    err.isOperational = true;
    return resError(err, res);
  }

  resError(err, res);
});

這個中間件會捕獲所有的錯誤,並進行處理。如果是資料驗證錯誤,就設置相應的錯誤訊息和 isOperational 標誌,然後呼叫 resError 函式處理錯誤。如果是其他類型的錯誤,就直接呼叫 resError 函式處理。

最後,我們添加一個未捕獲的 Promise 錯誤處理程式:

// 未捕獲到的 catch
process.on("unhandledRejection", (err, promise) => {
  console.error("未捕獲到的 rejection:", promise, "原因:", err);
});

module.exports = app;

這個處理程式會在有未捕獲的 Promise 錯誤時觸發,記錄錯誤訊息。

在路由中使用工具函式

現在,我們可以在 Express 的路由中使用之前定義的工具函式了。
我們回到 routes/feedback.js 檔案,並在檔案的頂部引入工具函式:

const handleSuccess = require("../utils/handleSuccess");
const handleErrorAsync = require("../utils/handleErrorAsync");
const appError = require("../utils/appError");

然後,在新增 feedback 的路由中,我們可以使用 handleErrorAsync 函式來處理異步函式的錯誤:

router.post(
  "/",
  handleErrorAsync(async (req, res, next) => {
    const { contactPerson, phone, email, feedback } = req.body;

    // 必填欄位驗證
    if (!contactPerson || !email || !feedback) {
      // 使用 appError 函式創建錯誤物件
      // 狀態碼為 400,錯誤訊息為 "姓名、信箱、內容為必填欄位"
      return next(appError(400, "姓名、信箱、內容為必填欄位"));
    }

    const newFeedback = await Feedback.create({
      contactPerson,
      phone,
      email,
      feedback,
    });

    // 使用 handleSuccess 函式處理成功回應
    // 狀態碼為 201,回應中包含新創建的 feedback 資料和成功訊息
    handleSuccess(res, newFeedback, "新增成功", 201);
  })
);

讓我們詳細解釋一下新增的錯誤和成功處理部分:

  1. 錯誤處理:
    • 在進行必填欄位驗證後,如果驗證失敗(即有欄位為空或未定義),我們使用 appError 函式創建一個自定義的錯誤物件。
    • appError 函式接收兩個參數:HTTP 狀態碼和錯誤訊息。在這裡,我們將狀態碼設為 400(Bad Request),表示客戶端提交的請求有問題。錯誤訊息設為 "姓名、信箱、內容為必填欄位",明確指出哪些欄位是必填的。
    • 創建完錯誤物件後,我們使用 return next(appError(...)),將錯誤物件傳遞給 Express 的 next 函式。這會將錯誤交給 Express 的錯誤處理中間件進行處理。
    • Express 的錯誤處理中間件會根據錯誤物件的屬性(如狀態碼和訊息)來生成適當的錯誤回應,並將其返回給客戶端。
  2. 成功處理:
    • 當新增 feedback 成功時,我們使用 handleSuccess 函式來處理成功回應。
    • handleSuccess 函式接收四個參數:
      • res:Express 的回應物件,用於發送回應給客戶端。
      • newFeedback:新創建的 feedback 資料,作為回應的一部分返回給客戶端。
      • "新增成功":成功訊息,告知客戶端新增 feedback 成功。
      • 201:HTTP 狀態碼,表示資源已成功創建。
    • handleSuccess 函式內部,它會設置適當的 HTTP 狀態碼(如果沒有提供,預設為 200),並將包含狀態、訊息和資料的 JSON 回應返回給客戶端。
    • 這樣,客戶端就能收到一個結構化的成功回應,其中包含了新創建的 feedback 資料和成功訊息。

通過使用 appErrorhandleSuccess 函式,我們將錯誤處理和成功回應的邏輯抽象到了單獨的函式中。這樣做的好處是:

  • 程式碼更加簡潔和可讀,不需要在路由處理函式中手動設置 HTTP 狀態碼和回應格式。
  • 錯誤處理和成功回應的邏輯被集中管理,提高了程式碼的一致性和可維護性。
  • 可以在多個路由處理函式中重複使用這些函式,減少了重複的程式碼。

app.js 完整程式碼

  • code
    var createError = require("http-errors");
    var express = require("express");
    var path = require("path");
    var cookieParser = require("cookie-parser");
    var logger = require("morgan");
    
    var cors = require("cors");
    
    const indexRouter = require("./routes/index");
    const usersRouter = require("./routes/users");
    const feedbackRoute = require("./routes/feedback");
    
    const mongoose = require("mongoose");
    
    const dotenv = require("dotenv");
    dotenv.config({ path: "./config.env" });
    
    const Feedback = require("./models/feedback");
    
    var app = express();
    
    // 程式出現重大錯誤時
    process.on("uncaughtException", (err) => {
      // 記錄錯誤下來,等到服務都處理完後,停掉該 process
      console.error("Uncaughted Exception!");
      console.error(err);
      process.exit(1);
    });
    
    const DB = process.env.DATABASE.replace(
      "<password>",
      process.env.DATABASE_PASSWORD
    );
    
    mongoose
      .connect(DB)
      .then(() => console.log("資料庫連接成功"))
      .catch((err) => {
        console.log("MongoDB 連接失敗:", err);
      });
    
    app.use(cors());
    
    app.set("views", path.join(__dirname, "views"));
    app.set("view engine", "ejs");
    
    app.use(logger("dev"));
    app.use(express.json());
    app.use(express.urlencoded({ extended: false }));
    app.use(cookieParser());
    app.use(express.static(path.join(__dirname, "public")));
    
    app.use("/", indexRouter);
    app.use("/users", usersRouter);
    app.use("/feedbacks", feedbackRoute);
    
    // 404 錯誤
    app.use(function (req, res, next) {
      // 回應一個包含錯誤訊息的 JSON 對象
      res.status(404).json({
        status: "error",
        message: "無此路由資訊",
        path: req.originalUrl, // 提供更多的上下文信息
      });
    });
    
    // * 開發環境 錯誤處理
    const resError = (err, res) => {
      res.status(err.statusCode).json({
        message: err.message,
        statusCode: err.statusCode,
        isOperational: err.isOperational,
        stack: err.stack,
      });
    };
    
    app.use(function (err, req, res, next) {
      err.statusCode = err.statusCode || 500;
    
      if (err.name === "ValidationError") {
        err.message = "資料欄位未填寫正確,請重新輸入!";
        err.isOperational = true;
        return resError(err, res);
      }
    
      resError(err, res);
    });
    
    // 未捕捉到的 catch
    process.on("unhandledRejection", (err, promise) => {
      console.error("未捕捉到的 rejection:", promise, "原因:", err);
    });
    
    // 導出給 ./bin/www 使用
    module.exports = app;
    
    

總結

在本文中,我們學習了如何將重複的程式碼抽取到單獨的函式或模組中,提高程式碼的可讀性和可維護性。我們創建了 handleSuccessappErrorhandleErrorAsync 三個工具函式,分別用於處理成功回應、創建自定義錯誤物件和處理異步函式的錯誤。
然後,我們在 Express 應用程式中實現了更全面的錯誤處理,包括全域的未捕獲異常處理、404 錯誤處理、開發環境錯誤處理、全域錯誤處理中間件以及未捕獲的 Promise 錯誤處理。
最後,我們在 Express 的路由中使用了這些工具函式,簡化了路由的錯誤處理邏輯,提高了程式碼的可讀性。
希望通過本文的學習,你能夠更好地理解如何抽取重複的程式碼,並實現更全面的錯誤處理。如果你有任何問題或建議,歡迎在評論區留言討論。
(對了,如果你覺得今天的內容對你有幫助,別忘了給個讚支持一下喔!這會是我繼續努力的動力呢~)


上一篇
[ DAY16 ] Postman 初體驗:API 測試小白入門指南
下一篇
[ DAY18 ] 手寫 API 文件?不用啦!Swagger 幫你搞定
系列文
房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言