開(kāi)發(fā)中,有些開(kāi)發(fā)者會(huì)積極尋求處理錯(cuò)誤,力求減少開(kāi)發(fā)時(shí)間,但也有些人完全忽略了錯(cuò)誤的存在。正確處理錯(cuò)誤不僅意味著能夠輕松發(fā)現(xiàn)和糾正錯(cuò)誤,而且還意味著能夠?yàn)榇笮蛻?yīng)用程序開(kāi)發(fā)出穩(wěn)健的代碼庫(kù)。
特別是對(duì)于 Node.js 開(kāi)發(fā)人員,他們有時(shí)會(huì)也發(fā)現(xiàn)自己使用了不那么整潔的代碼來(lái)處理各種錯(cuò)誤,例如會(huì)在所有地方都用相同的邏輯來(lái)處理錯(cuò)誤。那么,難道 Node.js 在處理錯(cuò)誤方面不太友好 ?
不。本文里,我想告訴的是 Node.js 一點(diǎn)問(wèn)題也沒(méi)有。
Node.js 錯(cuò)誤處理之錯(cuò)誤類型
首先,我們有必要對(duì) Node.js 中的錯(cuò)誤有一個(gè)清晰的認(rèn)識(shí)。一般來(lái)說(shuō),Node.js錯(cuò)誤分為兩大類: 操作錯(cuò)誤 和 開(kāi)發(fā)者錯(cuò)誤。
- 操作錯(cuò)誤:表示運(yùn)行時(shí)問(wèn)題,其結(jié)果是預(yù)期的,應(yīng)該以適當(dāng)?shù)姆绞教幚?。操作錯(cuò)誤并不意味著應(yīng)用程序本身有錯(cuò)誤,但開(kāi)發(fā)者需要仔細(xì)處理它們。操作錯(cuò)誤的例子包括“內(nèi)存不足”、“API 參數(shù)的無(wú)效輸入”等等。
- 開(kāi)發(fā)者錯(cuò)誤:是指在寫(xiě)得不好的代碼中出現(xiàn)了意想不到的錯(cuò)誤。意思就是代碼邏輯本身有一些問(wèn)題,需要解決。一個(gè)很好的例子是嘗試讀取
“undefined”
的屬性。要解決這個(gè)問(wèn)題,必須更改代碼。因?yàn)檫@是開(kāi)發(fā)者制造的錯(cuò)誤,而不是操作錯(cuò)誤。
接下來(lái)的一個(gè)問(wèn)題是:“為什么我們要把它們分成兩類來(lái)處理?”
原因是,如果你沒(méi)有對(duì)錯(cuò)誤有一個(gè)清晰的認(rèn)識(shí),那么每當(dāng)出現(xiàn)錯(cuò)誤時(shí),你可能會(huì)想重啟服務(wù)。而當(dāng)成千上萬(wàn)的用戶正在使用你的程序時(shí),他們可能看到的是“Not Found”。那這樣的重啟是否有意義?
同樣,如果你的代碼邏輯發(fā)生錯(cuò)誤的時(shí)候,給應(yīng)用帶來(lái)了意想不到的問(wèn)題,影響到了用戶體驗(yàn),這是否有意義?
正確處理錯(cuò)誤
假設(shè)你有一些使用異步 Js 的經(jīng)驗(yàn),那么在使用回調(diào)處理錯(cuò)誤時(shí)可能會(huì)遇到一些挑戰(zhàn)。例如在回調(diào)函數(shù)中你不斷地進(jìn)行錯(cuò)誤檢查,可能會(huì)導(dǎo)致嵌套過(guò)深,從而引發(fā)“回調(diào)地獄”的問(wèn)題。這種情況會(huì)使代碼流變得難以跟蹤和理解。
那么,你可以使用 promise或async/await
替代回調(diào)。例如下面這段代碼:
const doAsyncJobs = async () => {
try {
const result1 = await job1();
const result2 = await job2(result1);
const result3 = await job3(result2);
return await job4(result3);
} catch (error) {
console.error(error);
} finally {
await anywayDoThisJob();
}
}
在 Node.js 中有一個(gè)內(nèi)置的 Error
對(duì)象,也是一個(gè)很好的處理辦法,因?yàn)樗酥庇^而清晰的錯(cuò)誤信息,比如 StackTrace
,大多數(shù)開(kāi)發(fā)者都依賴它來(lái)跟蹤錯(cuò)誤的根源。除此之外,還有一些其他有意義的屬性,如 HTTP 狀態(tài)碼和通過(guò)擴(kuò)展 Error 類的描述,將使其錯(cuò)誤描述的更加具體。
class BaseError extends Error {
public readonly name: string;
public readonly httpCode: HttpStatusCode;
public readonly isOperational: boolean;
constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) {
super(description);
Object.setPrototypeOf(this, new.target.prototype);
this.name = name;
this.httpCode = httpCode;
this.isOperational = isOperational;
Error.captureStackTrace(this);
}
}
//繼承 BaseError
class APIError extends BaseError {
constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') {
super(name, httpCode, isOperational, description);
}
}
為了簡(jiǎn)單起見(jiàn),我只實(shí)現(xiàn)了一些 HTTP 狀態(tài)碼,你可以嘗試添加更多狀態(tài)碼:
export enum HttpStatusCode {
OK = 200,
BAD_REQUEST = 400,
NOT_FOUND = 404,
INTERNAL_SERVER = 500,
}
同時(shí),你可以根據(jù)你的需要和個(gè)人偏好對(duì)常見(jiàn)錯(cuò)誤進(jìn)行擴(kuò)展:
class HTTP400Error extends BaseError {
constructor(description = 'bad request') {
super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description);
}
}
那么如何使用它呢? 很簡(jiǎn)單,就是拋出這種錯(cuò)誤類型:
const user = await User.getUserById(1);
if (user === null)
throw new APIError(
'NOT FOUND',
HttpStatusCode.NOT_FOUND,
true,
'detailed explanation'
);
集中式 Node.js 錯(cuò)誤處理組件
現(xiàn)在,我們準(zhǔn)備構(gòu)建 Node.js 錯(cuò)誤處理系統(tǒng)的主要組件: 集中式錯(cuò)誤處理組件。
構(gòu)建集中式的錯(cuò)誤處理組件通常是一個(gè)好主意,以便在處理錯(cuò)誤時(shí)避免可能的代碼重復(fù)。錯(cuò)誤處理組件負(fù)責(zé)使捕獲的錯(cuò)誤變得可以理解,例如,通過(guò)向系統(tǒng)管理員發(fā)送通知、將事件傳輸?shù)奖O(jiān)視服務(wù)器中(如 Sentry)、打日志記錄錯(cuò)誤。
下圖中我給出了處理錯(cuò)誤的基本工作流程:
在代碼的某些部分,錯(cuò)誤會(huì)被捕獲并傳遞給錯(cuò)誤處理中間件:
try {
userService.addNewUser(req.body).then((newUser: User) => {
res.status(200).json(newUser);
}).catch((error: Error) => {
next(error)
});
} catch (error) {
next(error);
}
錯(cuò)誤處理中間件是區(qū)分錯(cuò)誤類型并將它們發(fā)送到集中式錯(cuò)誤處理組件的好地方:
app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => {
if (!errorHandler.isTrustedError(err)) {
next(err);
}
await errorHandler.handleError(err);
});
到目前為止,你應(yīng)該可以想象到集中式組件應(yīng)該是什么樣子。不過(guò)請(qǐng)記住,這完全取決于你如何實(shí)現(xiàn)它。例如,它可能看起來(lái)像以下這樣:
class ErrorHandler {
public async handleError(err: Error): Promise<void> {
await logger.error(
'Error message from the centralized error-handling component',
err,
);
await sendMailToAdminIfCritical();
await sendEventsToSentry();
}
public isTrustedError(error: Error) {
if (error instanceof BaseError) {
return error.isOperational;
}
return false;
}
}
export const errorHandler = new ErrorHandler();
不過(guò),有時(shí)候你會(huì)發(fā)現(xiàn)默認(rèn)的 “console.error”
輸出錯(cuò)誤信息不是很好閱讀。相反,以格式化的方式輸出錯(cuò)誤可能會(huì)更好,這樣開(kāi)發(fā)者可以更快速理解問(wèn)題并確保它們得到修復(fù)。
這里,我向你推薦 winston
或 morgan
這樣的可定制記錄器。
例如,下面是一個(gè)定制的 winston
記錄器:
const customLevels = {
levels: {
trace: 5,
debug: 4,
info: 3,
warn: 2,
error: 1,
fatal: 0,
},
colors: {
trace: 'white',
debug: 'green',
info: 'green',
warn: 'yellow',
error: 'red',
fatal: 'red',
},
};
const formatter = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.splat(),
winston.format.printf((info) => {
const { timestamp, level, message, ...meta } = info;
return `${timestamp} [${level}]: ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
}),
);
class Logger {
private logger: winston.Logger;
constructor() {
const prodTransport = new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
});
const transport = new winston.transports.Console({
format: formatter,
});
this.logger = winston.createLogger({
level: isDevEnvironment() ? 'trace' : 'error',
levels: customLevels.levels,
transports: [isDevEnvironment() ? transport : prodTransport],
});
winston.addColors(customLevels.colors);
}
trace(msg: any, meta?: any) {
this.logger.log('trace', msg, meta);
}
debug(msg: any, meta?: any) {
this.logger.debug(msg, meta);
}
info(msg: any, meta?: any) {
this.logger.info(msg, meta);
}
warn(msg: any, meta?: any) {
this.logger.warn(msg, meta);
}
error(msg: any, meta?: any) {
this.logger.error(msg, meta);
}
fatal(msg: any, meta?: any) {
this.logger.log('fatal', msg, meta);
}
}
export const logger = new Logger();
它主要提供的是以格式化的方式在多個(gè)不同級(jí)別進(jìn)行日志記錄,顏色清晰,并根據(jù)運(yùn)行時(shí)環(huán)境記錄到錯(cuò)誤日志文件中。這樣做的好處是,你可以使用 winston
的內(nèi)置 api
來(lái)監(jiān)視和查詢?nèi)罩?。此外,你可以使用日志分析工具?lái)分析格式化的日志文件,以獲得有關(guān)應(yīng)用程序的更多有用信息。
到目前為止,我們主要討論了如何處理操作錯(cuò)誤,那開(kāi)發(fā)者的代碼邏輯造成的錯(cuò)誤呢?
由于開(kāi)發(fā)者的錯(cuò)誤是意料之外的,它們是實(shí)際的 bug,可能導(dǎo)致應(yīng)用程序最終處于錯(cuò)誤的狀態(tài),并以意想不到的方式運(yùn)行。那么,處理這些錯(cuò)誤的最佳方法是“立即崩潰”,然后使用像 PM2
這樣的自動(dòng)重啟器優(yōu)雅地重新啟動(dòng):
process.on('uncaughtException', (error: Error) => {
errorHandler.handleError(error);
if (!errorHandler.isTrustedError(error)) {
process.exit(1);
}
});
最后我想要提到的是處理未處理的 promise.reject
和 異常。
在開(kāi)發(fā) Node.js/Express
應(yīng)用程序時(shí),你可能會(huì)發(fā)現(xiàn)自己花了很多時(shí)間處理承諾。當(dāng)你忘記處理 reject
時(shí),會(huì)看到有關(guān)未處理 promise.reject
的警告信息。
除了日志記錄之外,警告消息不會(huì)做太多事情,但是使用適當(dāng)?shù)幕赝撕陀嗛?process.on('unhandledRejection',callback)
是一個(gè)不錯(cuò)的做法。你可以將其視為Node.js 的一種全局的錯(cuò)誤處理程序。
典型的錯(cuò)誤處理流程如下所示:
User.getUserById(1).then((firstUser) => {
if (firstUser.isSleeping === false) throw new Error('He is not sleeping!');
});
...
// 獲取未處理的 reject 并將其扔給我們已有的另一個(gè)回退處理程序
process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => {
throw reason;
});
process.on('uncaughtException', (error: Error) => {
errorHandler.handleError(error);
if (!errorHandler.isTrustedError(error)) {
process.exit(1);
}
});
結(jié)尾
現(xiàn)在,你是否意識(shí)到無(wú)論是在開(kāi)發(fā)階段還是在生產(chǎn)階段錯(cuò)誤處理可不是一個(gè)可選的功能,而是應(yīng)用程序的一個(gè)必要部分。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-694820.html
在 Node.js 中的單個(gè)組件中處理錯(cuò)誤的策略將確保開(kāi)發(fā)人員節(jié)省寶貴的時(shí)間,并通過(guò)避免代碼重復(fù)和丟失錯(cuò)誤上下文來(lái)編寫(xiě)干凈且可維護(hù)的代碼。不得不說(shuō),它已經(jīng)成為 Node.js 應(yīng)用程序的必備保健品。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-694820.html
到了這里,關(guān)于Node.js 應(yīng)用的御用品: Node.js 錯(cuò)誤處理系統(tǒng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!