沪江CCtalk视频地址:https://www.cctalk.com/v/15114923887518

处理错误请求

爱能遮掩一切过错。

当我们在访问一个站点的时候,如果访问的地址不存在(404),或服务器内部发生了错误(500),站点会展示出某个特定的页面,比如:

那么如何在 Koa 中实现这种功能呢?其实,一个简单的中间件即可实现,我们把它称为 http-error。实现过程并不复杂,拆分为三步来看:

  • 第一步:确认需求
  • 第二步:整理思路
  • 第三步:代码实现

确认需求

打造一个事物前,需要先确认它具有什么特性,这就是需求。

在这里,稍微整理下即可得到几个基本需求:

  • 在页面请求出现 400500 类错误码的时候,引导用户至错误页面;
  • 提供默认错误页面;
  • 允许使用者自定义错误页面。

整理思路

现在,从一个请求进入 Koa 开始说起:

  1. 一个请求访问 Koa,出现了错误;
  2. 该错误会被 http-error 中间件捕捉到;
  3. 错误会被中间件的错误处理逻辑捕捉到,并进行处理;
  4. 错误处理逻辑根据错误码状态,调用渲染页面逻辑;
  5. 渲染页面逻辑渲染出对应的错误页面。

可以看到,关键点就是捕捉错误,以及实现错误处理逻辑和渲染页面逻辑。

代码实现

建立文件

基于教程目录结构,我们创建 middleware/mi-http-error/index.js 文件,存放中间件的逻辑代码。初始目录结构如下:

  1. middleware/
  2. ├─ mi-http-error/
  3. └── index.js
  4. └─ index.js

注意: 目录结构不存在,需要自己创建。

捕捉错误

该中间件第一项需要实现的功能是捕捉到所有的 http 错误。根据中间件的洋葱模型,需要做几件事:

1. 引入中间件

修改 middleware/index.js,引入 mi-http-error 中间件,并将它放到洋葱模型的最外层

  1. const path = require('path')
  2. const ip = require("ip")
  3. const bodyParser = require('koa-bodyparser')
  4. const nunjucks = require('koa-nunjucks-2')
  5. const staticFiles = require('koa-static')
  6. const miSend = require('./mi-send')
  7. const miLog = require('./mi-log')
  8. // 引入请求错误中间件
  9. const miHttpError = require('./mi-http-error')
  10. module.exports = (app) => {
  11. // 应用请求错误中间件
  12. app.use(miHttpError())
  13. app.use(miLog(app.env, {
  14. env: app.env,
  15. projectName: 'koa2-tutorial',
  16. appLogLevel: 'debug',
  17. dir: 'logs',
  18. serverIp: ip.address()
  19. }));
  20. app.use(staticFiles(path.resolve(__dirname, "../public")))
  21. app.use(nunjucks({
  22. ext: 'html',
  23. path: path.join(__dirname, '../views'),
  24. nunjucksConfig: {
  25. trimBlocks: true
  26. }
  27. }));
  28. app.use(bodyParser())
  29. app.use(miSend())
  30. }

2. 捕获中间件异常情况

修改 mi-http-error/index.js,在中间件内部对内层的其它中间件进行错误监听,并对捕获 catch 到的错误进行处理

  1. module.exports = () => {
  2. return async (ctx, next) => {
  3. try {
  4. await next();
  5. /**
  6. * 如果没有更改过 response 的 status,则 koa 默认的 status 是 404
  7. */
  8. if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
  9. } catch (e) {
  10. /*此处进行错误处理,下面会讲解具体实现*/
  11. }
  12. }
  13. }

上面的准备工作做完,下面实现两个关键逻辑。

错误处理逻辑

错误处理逻辑其实很简单,就是对错误码进行判断,并指定要渲染的文件名。这段代码运行在错误 catch 中。

修改 mi-http-error/index.js

  1. module.exports = () => {
  2. let fileName = 'other'
  3. return async (ctx, next) => {
  4. try {
  5. await next();
  6. /**
  7. * 如果没有更改过 response 的 status,则 koa 默认的 status 是 404
  8. */
  9. if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
  10. } catch (e) {
  11. let status = parseInt(e.status)
  12. // 默认错误信息为 error 对象上携带的 message
  13. const message = e.message
  14. // 对 status 进行处理,指定错误页面文件名
  15. if(status >= 400){
  16. switch(status){
  17. case 400:
  18. case 404:
  19. case 500:
  20. fileName = status;
  21. break;
  22. // 其它错误 指定渲染 other 文件
  23. default:
  24. fileName = 'other'
  25. }
  26. }
  27. }
  28. }
  29. }

也就是说,对于不同的情况,会展示不同的错误页面:

  1. ├─ 400.html
  2. ├─ 404.html
  3. ├─ 500.html
  4. ├─ other.html

这几个页面文件我们会在后面创建,接下来我们开始讲述下页面渲染的问题。

渲染页面逻辑

首先我们创建默认的错误页面模板文件 mi-http-error/error.html,这里采用 nunjucks 语法。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Error - {{ status }}</title>
  5. </head>
  6. <body>
  7. <div id="error">
  8. <h1>Error - {{ status }}</h1>
  9. <p>Looks like something broke!</p>
  10. {% if (env === 'development') %}
  11. <h2>Message:</h2>
  12. <pre>
  13. <code>
  14. {{ error }}
  15. </code>
  16. </pre>
  17. <h2>Stack:</h2>
  18. <pre>
  19. <code>
  20. {{ stack }}
  21. </code>
  22. </pre>
  23. {% endif %}
  24. </div>
  25. </body>
  26. </html>

因为牵涉到文件路径的解析,我们需要引入 path 模块。另外,还需要引入 nunjucks 工具来解析模板。pathnode 模块,我们只需从 npm 上安装nunjucks 即可。

安装 nunjucks 模块来解析模板文件:

  1. npm i nunjucks -S

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

  1. // 引入 path nunjucks 模块
  2. const Path = require('path')
  3. const nunjucks = require('nunjucks')
  4. module.exports = () => {
  5. // 此处代码省略,与之前一样
  6. }

为了支持自定义错误文件目录,原来调用中间件的代码需要修改一下。我们给中间件传入一个配置对象,该对象中有一个字段 errorPageFolder,表示自定义错误文件目录。

修改 middleware/index.js

  1. // app.use(miHttpError())
  2. app.use(miHttpError({
  3. errorPageFolder: path.resolve(__dirname, '../errorPage')
  4. }))

注意: 代码中,我们指定了 /errorPage 为默认的模板文件目录。

修改 mi-http-error/index.js,处理接收到的参数:

  1. const Path = require('path')
  2. const nunjucks = require('nunjucks')
  3. module.exports = (opts = {}) => {
  4. // 400.html 404.html other.html 的存放位置
  5. const folder = opts.errorPageFolder
  6. // 指定默认模板文件
  7. const templatePath = Path.resolve(__dirname, './error.html')
  8. let fileName = 'other'
  9. return async (ctx, next) => {
  10. try {
  11. await next()
  12. if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
  13. } catch (e) {
  14. let status = parseInt(e.status)
  15. const message = e.message
  16. if(status >= 400){
  17. switch(status){
  18. case 400:
  19. case 404:
  20. case 500:
  21. fileName = status;
  22. break;
  23. default:
  24. fileName = 'other'
  25. }
  26. }else{// 其它情况,统一返回为 500
  27. status = 500
  28. fileName = status
  29. }
  30. // 确定最终的 filePath 路径
  31. const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
  32. }
  33. }
  34. }

路径和参数准备好之后,我们需要做的事情就剩返回渲染的页面了。

修改 mi-http-error/index.js,对捕捉到的不同错误返回相应的视图页面:

  1. const Path = require('path')
  2. const nunjucks = require('nunjucks')
  3. module.exports = (opts = {}) => {
  4. // 增加环境变量,用来传入到视图中,方便调试
  5. const env = opts.env || process.env.NODE_ENV || 'development'
  6. const folder = opts.errorPageFolder
  7. const templatePath = Path.resolve(__dirname, './error.html')
  8. let fileName = 'other'
  9. return async (ctx, next) => {
  10. try {
  11. await next()
  12. if (ctx.response.status === 404 && !ctx.response.body) ctx.throw(404);
  13. } catch (e) {
  14. let status = parseInt(e.status)
  15. const message = e.message
  16. if(status >= 400){
  17. switch(status){
  18. case 400:
  19. case 404:
  20. case 500:
  21. fileName = status;
  22. break;
  23. default:
  24. fileName = 'other'
  25. }
  26. }else{
  27. status = 500
  28. fileName = status
  29. }
  30. const filePath = folder ? Path.join(folder, `${fileName}.html`) : templatePath
  31. // 渲染对应错误类型的视图,并传入参数对象
  32. try{
  33. // 指定视图目录
  34. nunjucks.configure( folder ? folder : __dirname )
  35. const data = await nunjucks.render(filePath, {
  36. env: env, // 指定当前环境参数
  37. status: e.status || e.message, // 如果错误信息中没有 status,就显示为 message
  38. error: e.message, // 错误信息
  39. stack: e.stack // 错误的堆栈信息
  40. })
  41. // 赋值给响应体
  42. ctx.status = status
  43. ctx.body = data
  44. }catch(e){
  45. // 如果中间件存在错误异常,直接抛出信息,由其他中间件处理
  46. ctx.throw(500, `错误页渲染失败:${e.message}`)
  47. }
  48. }
  49. }
  50. }

上面所做的是使用渲染引擎对模板文件进行渲染,并将生成的内容放到 HttpResponse 中,展示在用户面前。感兴趣的同学可以去中间件源码中查看 error.html 查看模板内容(其实是从 koa-error 那里拿来稍作修改的)。

在代码的最后,我们还有一个异常的抛出 ctx.throw(),也就是说,中间件处理时候也会存在异常,所以我们需要在最外层做一个错误监听处理。

修改 middleware/index.js

  1. const path = require('path')
  2. const ip = require("ip")
  3. const bodyParser = require('koa-bodyparser')
  4. const nunjucks = require('koa-nunjucks-2')
  5. const staticFiles = require('koa-static')
  6. const miSend = require('./mi-send')
  7. const miLog = require('./mi-log')
  8. const miHttpError = require('./mi-http-error')
  9. module.exports = (app) => {
  10. app.use(miHttpError({
  11. errorPageFolder: path.resolve(__dirname, '../errorPage')
  12. }))
  13. app.use(miLog(app.env, {
  14. env: app.env,
  15. projectName: 'koa2-tutorial',
  16. appLogLevel: 'debug',
  17. dir: 'logs',
  18. serverIp: ip.address()
  19. }));
  20. app.use(staticFiles(path.resolve(__dirname, "../public")))
  21. app.use(nunjucks({
  22. ext: 'html',
  23. path: path.join(__dirname, '../views'),
  24. nunjucksConfig: {
  25. trimBlocks: true
  26. }
  27. }));
  28. app.use(bodyParser())
  29. app.use(miSend())
  30. // 增加错误的监听处理
  31. app.on("error", (err, ctx) => {
  32. if (ctx && !ctx.headerSent && ctx.status < 500) {
  33. ctx.status = 500
  34. }
  35. if (ctx && ctx.log && ctx.log.error) {
  36. if (!ctx.state.logged) {
  37. ctx.log.error(err.stack)
  38. }
  39. }
  40. })
  41. }

下面,我们增加对应的错误渲染页面:

创建 errorPage/400.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>400</title>
  5. </head>
  6. <body>
  7. <div id="error">
  8. <h1>Error - {{ status }}</h1>
  9. <p>错误码 400 的描述信息</p>
  10. {% if (env === 'development') %}
  11. <h2>Message:</h2>
  12. <pre>
  13. <code>
  14. {{ error }}
  15. </code>
  16. </pre>
  17. <h2>Stack:</h2>
  18. <pre>
  19. <code>
  20. {{ stack }}
  21. </code>
  22. </pre>
  23. {% endif %}
  24. </div>
  25. </body>
  26. </html>

创建 errorPage/404.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>404</title>
  5. </head>
  6. <body>
  7. <div id="error">
  8. <h1>Error - {{ status }}</h1>
  9. <p>错误码 404 的描述信息</p>
  10. {% if (env === 'development') %}
  11. <h2>Message:</h2>
  12. <pre>
  13. <code>
  14. {{ error }}
  15. </code>
  16. </pre>
  17. <h2>Stack:</h2>
  18. <pre>
  19. <code>
  20. {{ stack }}
  21. </code>
  22. </pre>
  23. {% endif %}
  24. </div>
  25. </body>
  26. </html>

创建 errorPage/500.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>500</title>
  5. </head>
  6. <body>
  7. <div id="error">
  8. <h1>Error - {{ status }}</h1>
  9. <p>错误码 500 的描述信息</p>
  10. {% if (env === 'development') %}
  11. <h2>Message:</h2>
  12. <pre>
  13. <code>
  14. {{ error }}
  15. </code>
  16. </pre>
  17. <h2>Stack:</h2>
  18. <pre>
  19. <code>
  20. {{ stack }}
  21. </code>
  22. </pre>
  23. {% endif %}
  24. </div>
  25. </body>
  26. </html>

创建 errorPage/other.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>未知异常</title>
  5. </head>
  6. <body>
  7. <div id="error">
  8. <h1>Error - {{ status }}</h1>
  9. <p>未知异常</p>
  10. {% if (env === 'development') %}
  11. <h2>Message:</h2>
  12. <pre>
  13. <code>
  14. {{ error }}
  15. </code>
  16. </pre>
  17. <h2>Stack:</h2>
  18. <pre>
  19. <code>
  20. {{ stack }}
  21. </code>
  22. </pre>
  23. {% endif %}
  24. </div>
  25. </body>
  26. </html>

errorPage 中的页面展示内容,可以根据自己的项目信息修改,以上仅供参考。

至此,我们基本完成了用来处理『请求错误』的中间件。而这个中间件并不是固定的形态,大家在真实项目中,还需要多考虑自己的业务场景和需求,打造出适合自己项目的中间件。

下一节中,我们将学习下规范与部署——制定合适的团队规范,提升开发效率。

上一篇:iKcamp新课程推出啦~~~~~iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源

推荐: 翻译项目Master的自述:

1. 干货|人人都是翻译项目的Master

2. iKcamp出品微信小程序教学共5章16小节汇总(含视频)

iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 错误处理的更多相关文章

  1. iKcamp团队制作|基于Koa2搭建Node.js实战项目教学(含视频)☞ 环境准备

    安装搭建项目的开发环境 视频地址:https://www.cctalk.com/v/15114357764004 文章 Koa 起手 - 环境准备 由于 koa2 已经开始使用 async/await ...

  2. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 记录日志

    沪江CCtalk视频地址:https://www.cctalk.com/v/15114923883523 log 日志中间件 最困难的事情就是认识自己. 在一个真实的项目中,开发只是整个投入的一小部分 ...

  3. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 解析JSON

    视频地址:https://www.cctalk.com/v/15114923886141 JSON 数据 我颠倒了整个世界,只为摆正你的倒影. 前面的文章中,我们已经完成了项目中常见的问题,比如 路由 ...

  4. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 处理静态资源

    视频地址:https://www.cctalk.com/v/15114923882788 处理静态资源 无非花开花落,静静. 指定静态资源目录 这里我们使用第三方中间件: koa-static 安装并 ...

  5. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 视图Nunjucks

    视频地址:https://www.cctalk.com/v/15114923888328 视图 Nunjucks 彩虹是上帝和人类立的约,上帝不会再用洪水灭人. 客户端和服务端之间相互通信,传递的数据 ...

  6. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 代码分层

    视频地址:https://www.cctalk.com/v/15114923889408 文章 在前面几节中,我们已经实现了项目中的几个常见操作:启动服务器.路由中间件.Get 和 Post 形式的请 ...

  7. iKcamp|基于Koa2搭建Node.js实战(含视频)☞ 规范与部署

    沪江CCtalk视频地址:https://www.cctalk.com/v/15114923889450 规范与部署 懒人推动社会进步. 本篇中,我们会讲述三个知识点 定制书写规范 开发环境运行 如何 ...

  8. iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 中间件用法

    中间件用法--讲解 Koa2 中间件的用法及如何开发中间件

  9. iKcamp团队制作|基于Koa2搭建Node.js实战(含视频)☞ 路由koa-router

    路由koa-router--MVC 中重要的环节:Url 处理器

随机推荐

  1. RESTful 知识点

    REST(英文:Representational State Transfer,简称REST) 对于资源的具体操作类型,由HTTP动词表示. 常用的HTTP动词有下面五个(括号里是对应的SQL命令). ...

  2. HTTP报头:通用报头,请求报头,响应报头和实体报头

    缓存控制优先级从高到低分别是Pragma -> Cache-Control -> Expires 报头 每一个报头都是由 [名称 + ":" + 空格 + 值 + ] ...

  3. Microsoft Dynamics CRM4.0 JScript 过滤lookup 出现 Microsoft Dynamics CRM 窗口无法打开,可能已被弹出窗口阻止程序所阻止。

    一.现象:JScript过滤lookup字段,选择lookup字段出现下图的情况: 出现:Microsoft Dynamics CRM 窗口无法打开,可能已被弹出窗口阻止程序所阻止.请将这台Micro ...

  4. WPF Demo5

    <Application x:Class="Demo5.App" xmlns="http://schemas.microsoft.com/winfx/2006/xa ...

  5. 服务端REST与SOAP的探讨(转)

    声明: 闲来逛论坛看到一篇不错的文章,阅读后受益匪浅. 本文从一个简单的应用场景出发,使用REST和SOAP两种不同的架构风格实现,通过对REST与SOAP Web服务具体对比,旨在帮助读者更深刻理解 ...

  6. 终于完成了 源码 编译lnmp环境

    经过了大概一个星期的努力,终于按照海生的编译流程将lnmp环境源码安装出来了 nginx 和php 主要参考 http://hessian.cn/p/1273.html mysql 主要参考 http ...

  7. PHP CI框架如何去掉 sql 里的反引号

    在使用CI框架的时候, 经常的Active Record 类,这时候会出现一个问题 使用Active Record 类组成的sql 中,为了防止sql注入,会自动的在表名,字段名 自动添加反引号 当然 ...

  8. bzoj2262: 平行宇宙与虫洞

    Description 量子力学指出,宇宙并非只有一种形态. 根据量子理论,一件事件发生之后可以产生不同的后果,而所有可能的后果都会形成自己的宇宙. 我们可以把一个宇宙看成一个时间轴,虫洞可以看成不同 ...

  9. 使用XML-RPC进行远程文件共享

    这是个不错的练习,使用python开发P2P程序,或许通过这个我们可以自己搞出来一个P2P下载工具,类似于迅雷.XML-RPC是一个远程过程调用(remote procedure call,RPC)的 ...

  10. JQUERY dialog的用法详细解析

    本篇文章主要是对JQUERY中dialog的用法进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助 今天用到了客户端的对话框,把 jQuery UI 中的对话框学习了一下. 准备 jQ ...