iKcamp|基於Koa2搭建Node.js實戰(含視訊)☞ 錯誤處理

iKcamp發表於2018-02-02

滬江CCtalk視訊地址:www.cctalk.com/v/151149238…

iKcamp|基於Koa2搭建Node.js實戰(含視訊)☞ 錯誤處理

處理錯誤請求

愛能遮掩一切過錯。

當我們在訪問一個站點的時候,如果訪問的地址不存在(404),或伺服器內部發生了錯誤(500),站點會展示出某個特定的頁面,比如:

iKcamp|基於Koa2搭建Node.js實戰(含視訊)☞ 錯誤處理

那麼如何在 Koa 中實現這種功能呢?其實,一個簡單的中介軟體即可實現,我們把它稱為 http-error。實現過程並不複雜,拆分為三步來看:

  • 第一步:確認需求
  • 第二步:整理思路
  • 第三步:程式碼實現

確認需求

打造一個事物前,需要先確認它具有什麼特性,這就是需求。


在這裡,稍微整理下即可得到幾個基本需求:

  • 在頁面請求出現 400500 類錯誤碼的時候,引導使用者至錯誤頁面;
  • 提供預設錯誤頁面;
  • 允許使用者自定義錯誤頁面。

整理思路

現在,從一個請求進入 Koa 開始說起:

  1. 一個請求訪問 Koa,出現了錯誤;
  2. 該錯誤會被 http-error 中介軟體捕捉到;
  3. 錯誤會被中介軟體的錯誤處理邏輯捕捉到,並進行處理;
  4. 錯誤處理邏輯根據錯誤碼狀態,呼叫渲染頁面邏輯;
  5. 渲染頁面邏輯渲染出對應的錯誤頁面。

可以看到,關鍵點就是捕捉錯誤,以及實現錯誤處理邏輯和渲染頁面邏輯。


程式碼實現

建立檔案

基於教程目錄結構,我們建立 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 工具來解析模板。pathnode 模組,我們只需從 npm 上安裝nunjucks 即可。


安裝 nunjucks 模組來解析模板檔案:

npm i nunjucks -S
複製程式碼

修改 mi-http-error/index.js,引入 pathnunjucks 模組:

// 引入 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}`)
      }
    }
  }
}
複製程式碼

上面所做的是使用渲染引擎對模板檔案進行渲染,並將生成的內容放到 HttpResponse 中,展示在使用者面前。感興趣的同學可以去中介軟體原始碼中檢視 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|基於Koa2搭建Node.js實戰(含視訊)☞ 錯誤處理

上一篇:iKcamp新課程推出啦~~~~~iKcamp|基於Koa2搭建Node.js實戰(含視訊)☞ 處理靜態資源

推薦: 翻譯專案Master的自述:

1. 乾貨|人人都是翻譯專案的Master

2. iKcamp出品微信小程式教學共5章16小節彙總(含視訊)


iKcamp|基於Koa2搭建Node.js實戰(含視訊)☞ 錯誤處理

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章