2fish 的部落格

歡迎來到我的部落格!這裡是我記錄開發點滴的地方,希望你會喜歡😌

0%

[ 2024 IT 鐵人賽 DAY17 ] 後端開發的關鍵:可重用函式設計與全面錯誤管理

嘿!你有沒有發現,在撰寫程式的時候,總是會有一些程式碼不斷地重複出現?它們就像是影分身術一樣,在不同的地方出現,做著類似的事情。這些重複的程式碼不僅讓我們的程式變得冗長,也讓修改和維護變得更加困難。

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

房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案。

挑戰人生第一次鐵人賽,就來個佛心分享 side project,從專案發想、規劃、設計、資料庫、開發到部署,技術使用 Nuxt 3、Tailwind CSS、Pinia、Axios、Express、MongoDB,製作一個前後端分離的鸚鵡專案,功能主要介紹食物計算機和聯絡我們,希望可以讓更多人瞭解專案的完整流程 ✨

IT 全文章連結

製作可重用的函式

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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 的檔案,並添加以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
// 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 的檔案,並添加以下程式碼:
1
2
3
4
5
6
7
8
9
10
11
// 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 檔案:
添加一個全域的未捕獲異常處理程式:

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

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

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

1
2
3
4
5
6
7
8
// 404 錯誤
app.use(function (req, res, next) {
res.status(404).json({
status: "error",
message: "無此路由資訊",
path: req.originalUrl,
});
});

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

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

1
2
3
4
5
6
7
8
const resError = (err, res) => {
res.status(err.statusCode).json({
message: err.message,
statusCode: err.statusCode,
isOperational: err.isOperational,
stack: err.stack,
});
};

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

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

1
2
3
4
5
6
7
8
9
10
11
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 錯誤處理程式:

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

module.exports = app;

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

在路由中使用工具函式

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 完整程式碼

點擊這裡查看完整範例程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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 的路由中使用了這些工具函式,簡化了路由的錯誤處理邏輯,提高了程式碼的可讀性。
希望通過本文的學習,你能夠更好地理解如何抽取重複的程式碼,並實現更全面的錯誤處理。如果你有任何問題或建議,歡迎在評論區留言討論。




希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉