iKcamp|基於Koa2搭建Node.js實戰(含視頻)? 錯誤處理
滬江CCtalk視頻地址:https://www.cctalk.com/v/15114923887518
處理錯誤請求
愛能遮掩一切過錯。
當我們在訪問一個站點的時候,如果訪問的地址不存在(404),或服務器內部發生了錯誤(500),站點會展示出某個特定的頁面,比如:
那麽如何在 Koa
中實現這種功能呢?其實,一個簡單的中間件即可實現,我們把它稱為 http-error
。實現過程並不復雜,拆分為三步來看:
- 第一步:確認需求
- 第二步:整理思路
- 第三步:代碼實現
確認需求
打造一個事物前,需要先確認它具有什麽特性,這就是需求。
在這裏,稍微整理下即可得到幾個基本需求:
- 在頁面請求出現
400
500
類錯誤碼的時候,引導用戶至錯誤頁面; - 提供默認錯誤頁面;
- 允許使用者自定義錯誤頁面。
整理思路
現在,從一個請求進入 Koa
開始說起:
- 一個請求訪問
Koa
,出現了錯誤; - 該錯誤會被
http-error
中間件捕捉到; - 錯誤會被中間件的錯誤處理邏輯捕捉到,並進行處理;
- 錯誤處理邏輯根據錯誤碼狀態,調用渲染頁面邏輯;
- 渲染頁面邏輯渲染出對應的錯誤頁面。
可以看到,關鍵點就是捕捉錯誤,以及實現錯誤處理邏輯和渲染頁面邏輯。
代碼實現
建立文件
基於教程目錄結構,我們創建 middleware/mi-http-error/index.js
文件,存放中間件的邏輯代碼。初始目錄結構如下:
middleware/
├─ mi-http-error/
│ └── index.js
└─ index.js
註意: 目錄結構不存在,需要自己創建。
捕捉錯誤
該中間件第一項需要實現的功能是捕捉到所有的 http
錯誤。根據中間件的洋蔥模型,需要做幾件事:
1. 引入中間件
修改 middleware/index.js
,引入 mi-http-error
中間件,並將它放到洋蔥模型的最外層
const path = require(‘path‘)
const ip = require("ip")
const bodyParser = require(‘koa-bodyparser‘)
const nunjucks = require(‘koa-nunjucks-2‘)
const staticFiles = require(‘koa-static‘)
const miSend = require(‘./mi-send‘)
const miLog = require(‘./mi-log‘)
// 引入請求錯誤中間件
const miHttpError = require(‘./mi-http-error‘)
module.exports = (app) => {
// 應用請求錯誤中間件
app.use(miHttpError())
app.use(miLog(app.env, {
env: app.env,
projectName: ‘koa2-tutorial‘,
appLogLevel: ‘debug‘,
dir: ‘logs‘,
serverIp: ip.address()
}));
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: ‘html‘,
path: path.join(__dirname, ‘../views‘),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
}
2. 捕獲中間件異常情況
修改 mi-http-error/index.js
,在中間件內部對內層的其它中間件進行錯誤監聽,並對捕獲 catch
到的錯誤進行處理
module.exports = () => {
return async (ctx, next) => {
try {
await next();
/**
* 如果沒有更改過 response 的 status,則 koa 默認的 status 是 404
*/
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
/*此處進行錯誤處理,下面會講解具體實現*/
}
}
}
上面的準備工作做完,下面實現兩個關鍵邏輯。
錯誤處理邏輯
錯誤處理邏輯其實很簡單,就是對錯誤碼進行判斷,並指定要渲染的文件名。這段代碼運行在錯誤 catch
中。
修改 mi-http-error/index.js
:
module.exports = () => {
let fileName = ‘other‘
return async (ctx, next) => {
try {
await next();
/**
* 如果沒有更改過 response 的 status,則 koa 默認的 status 是 404
*/
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
// 默認錯誤信息為 error 對象上攜帶的 message
const message = e.message
// 對 status 進行處理,指定錯誤頁面文件名
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
// 其它錯誤 指定渲染 other 文件
default:
fileName = ‘other‘
}
}
}
}
}
也就是說,對於不同的情況,會展示不同的錯誤頁面:
├─ 400.html
├─ 404.html
├─ 500.html
├─ other.html
這幾個頁面文件我們會在後面創建,接下來我們開始講述下頁面渲染的問題。
渲染頁面邏輯
首先我們創建默認的錯誤頁面模板文件 mi-http-error/error.html
,這裏采用 nunjucks
語法。
<!DOCTYPE html>
<html>
<head>
<title>Error - {{ status }}</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>Looks like something broke!</p>
{% if (env === ‘development‘) %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html>
因為牽涉到文件路徑的解析,我們需要引入 path
模塊。另外,還需要引入 nunjucks
工具來解析模板。path
是 node
模塊,我們只需從 npm
上安裝nunjucks
即可。
安裝 nunjucks
模塊來解析模板文件:
npm i nunjucks -S
修改 mi-http-error/index.js
,引入 path
和 nunjucks
模塊:
// 引入 path nunjucks 模塊
const Path = require(‘path‘)
const nunjucks = require(‘nunjucks‘)
module.exports = () => {
// 此處代碼省略,與之前一樣
}
為了支持自定義錯誤文件目錄,原來調用中間件的代碼需要修改一下。我們給中間件傳入一個配置對象,該對象中有一個字段 errorPageFolder
,表示自定義錯誤文件目錄。
修改 middleware/index.js
:
// app.use(miHttpError())
app.use(miHttpError({
errorPageFolder: path.resolve(__dirname, ‘../errorPage‘)
}))
註意: 代碼中,我們指定了 /errorPage
為默認的模板文件目錄。
修改 mi-http-error/index.js
,處理接收到的參數:
const Path = require(‘path‘)
const nunjucks = require(‘nunjucks‘)
module.exports = (opts = {}) => {
// 400.html 404.html other.html 的存放位置
const folder = opts.errorPageFolder
// 指定默認模板文件
const templatePath = Path.resolve(__dirname, ‘./error.html‘)
let fileName = ‘other‘
return async (ctx, next) => {
try {
await next()
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
const message = e.message
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
default:
fileName = ‘other‘
}
}else{// 其它情況,統一返回為 500
status = 500
fileName = status
}
// 確定最終的 filePath 路徑
const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
}
}
}
路徑和參數準備好之後,我們需要做的事情就剩返回渲染的頁面了。
修改 mi-http-error/index.js
,對捕捉到的不同錯誤返回相應的視圖頁面:
const Path = require(‘path‘)
const nunjucks = require(‘nunjucks‘)
module.exports = (opts = {}) => {
// 增加環境變量,用來傳入到視圖中,方便調試
const env = opts.env || process.env.NODE_ENV || ‘development‘
const folder = opts.errorPageFolder
const templatePath = Path.resolve(__dirname, ‘./error.html‘)
let fileName = ‘other‘
return async (ctx, next) => {
try {
await next()
if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
} catch (e) {
let status = parseInt(e.status)
const message = e.message
if(status >= 400){
switch(status){
case 400:
case 404:
case 500:
fileName = status;
break;
default:
fileName = ‘other‘
}
}else{
status = 500
fileName = status
}
const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
// 渲染對應錯誤類型的視圖,並傳入參數對象
try{
// 指定視圖目錄
nunjucks.configure( folder ? folder : __dirname )
const data = await nunjucks.render(filePath, {
env: env, // 指定當前環境參數
status: e.status || e.message, // 如果錯誤信息中沒有 status,就顯示為 message
error: e.message, // 錯誤信息
stack: e.stack // 錯誤的堆棧信息
})
// 賦值給響應體
ctx.status = status
ctx.body = data
}catch(e){
// 如果中間件存在錯誤異常,直接拋出信息,由其他中間件處理
ctx.throw(500, `錯誤頁渲染失敗:${e.message}`)
}
}
}
}
上面所做的是使用渲染引擎對模板文件進行渲染,並將生成的內容放到 Http
的 Response
中,展示在用戶面前。感興趣的同學可以去中間件源碼中查看 error.html
查看模板內容(其實是從 koa-error
那裏拿來稍作修改的)。
在代碼的最後,我們還有一個異常的拋出 ctx.throw()
,也就是說,中間件處理時候也會存在異常,所以我們需要在最外層做一個錯誤監聽處理。
修改 middleware/index.js
:
const path = require(‘path‘)
const ip = require("ip")
const bodyParser = require(‘koa-bodyparser‘)
const nunjucks = require(‘koa-nunjucks-2‘)
const staticFiles = require(‘koa-static‘)
const miSend = require(‘./mi-send‘)
const miLog = require(‘./mi-log‘)
const miHttpError = require(‘./mi-http-error‘)
module.exports = (app) => {
app.use(miHttpError({
errorPageFolder: path.resolve(__dirname, ‘../errorPage‘)
}))
app.use(miLog(app.env, {
env: app.env,
projectName: ‘koa2-tutorial‘,
appLogLevel: ‘debug‘,
dir: ‘logs‘,
serverIp: ip.address()
}));
app.use(staticFiles(path.resolve(__dirname, "../public")))
app.use(nunjucks({
ext: ‘html‘,
path: path.join(__dirname, ‘../views‘),
nunjucksConfig: {
trimBlocks: true
}
}));
app.use(bodyParser())
app.use(miSend())
// 增加錯誤的監聽處理
app.on("error", (err, ctx) => {
if (ctx && !ctx.headerSent && ctx.status < 500) {
ctx.status = 500
}
if (ctx && ctx.log && ctx.log.error) {
if (!ctx.state.logged) {
ctx.log.error(err.stack)
}
}
})
}
下面,我們增加對應的錯誤渲染頁面:
創建 errorPage/400.html
:
<!DOCTYPE html>
<html>
<head>
<title>400</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>錯誤碼 400 的描述信息</p>
{% if (env === ‘development‘) %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html>
創建 errorPage/404.html
:
<!DOCTYPE html>
<html>
<head>
<title>404</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>錯誤碼 404 的描述信息</p>
{% if (env === ‘development‘) %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html>
創建 errorPage/500.html
:
<!DOCTYPE html>
<html>
<head>
<title>500</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>錯誤碼 500 的描述信息</p>
{% if (env === ‘development‘) %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html>
創建 errorPage/other.html
:
<!DOCTYPE html>
<html>
<head>
<title>未知異常</title>
</head>
<body>
<div id="error">
<h1>Error - {{ status }}</h1>
<p>未知異常</p>
{% if (env === ‘development‘) %}
<h2>Message:</h2>
<pre>
<code>
{{ error }}
</code>
</pre>
<h2>Stack:</h2>
<pre>
<code>
{{ stack }}
</code>
</pre>
{% endif %}
</div>
</body>
</html>
errorPage
中的頁面展示內容,可以根據自己的項目信息修改,以上僅供參考。
至此,我們基本完成了用來處理『請求錯誤』的中間件。而這個中間件並不是固定的形態,大家在真實項目中,還需要多考慮自己的業務場景和需求,打造出適合自己項目的中間件。
下一節中,我們將學習下規範與部署——制定合適的團隊規範,提升開發效率。
上一篇:iKcamp新課程推出啦~~~~~iKcamp|基於Koa2搭建Node.js實戰(含視頻)? 處理靜態資源
推薦: 翻譯項目Master的自述:
1. 幹貨|人人都是翻譯項目的Master
2. iKcamp出品微信小程序教學共5章16小節匯總(含視頻)
iKcamp|基於Koa2搭建Node.js實戰(含視頻)? 錯誤處理