爱能遮掩一切过错。
当我们在访问一个站点的时候,如果访问的地址不存在(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 错误。根据中间件的洋葱模型,需要做几件事:
修改middleware/index.js,引入mi-http-error 中间件,并将它放到洋葱模型的最外层
constpath=require('path')constip=require("ip")constbodyParser=require('koa-bodyparser')constnunjucks=require('koa-nunjucks-2')conststaticFiles=require('koa-static')constmiSend=require('./mi-send')constmiLog=require('./mi-log')// 引入请求错误中间件constmiHttpError=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())}修改mi-http-error/index.js,在中间件内部对内层的其它中间件进行错误监听,并对捕获catch 到的错误进行处理
module.exports=()=>{returnasync(ctx,next)=>{try{awaitnext();/** * 如果没有更改过 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=()=>{letfileName='other'returnasync(ctx,next)=>{try{awaitnext();/** * 如果没有更改过 response 的 status,则 koa 默认的 status 是 404 */if(ctx.response.status===404&&!ctx.response.body)ctx.throw(404);}catch(e){letstatus=parseInt(e.status)// 默认错误信息为 error 对象上携带的 messageconstmessage=e.message// 对 status 进行处理,指定错误页面文件名if(status>=400){switch(status){case400:case404:case500: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><divid="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 模块来解析模板文件:
修改mi-http-error/index.js,引入path 和nunjucks 模块:
// 引入 path nunjucks 模块constPath=require('path')constnunjucks=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,处理接收到的参数:
constPath=require('path')constnunjucks=require('nunjucks')module.exports=(opts={})=>{// 400.html 404.html other.html 的存放位置constfolder=opts.errorPageFolder// 指定默认模板文件consttemplatePath=Path.resolve(__dirname,'./error.html')letfileName='other'returnasync(ctx,next)=>{try{awaitnext()if(ctx.response.status===404&&!ctx.response.body)ctx.throw(404);}catch(e){letstatus=parseInt(e.status)constmessage=e.messageif(status>=400){switch(status){case400:case404:case500:fileName=status;break;default:fileName='other'}}else{// 其它情况,统一返回为 500status=500fileName=status}// 确定最终的 filePath 路径constfilePath=folder ?Path.join(folder,`${fileName}.html`) :templatePath}}}
路径和参数准备好之后,我们需要做的事情就剩返回渲染的页面了。
修改mi-http-error/index.js,对捕捉到的不同错误返回相应的视图页面:
constPath=require('path')constnunjucks=require('nunjucks')module.exports=(opts={})=>{// 增加环境变量,用来传入到视图中,方便调试constenv=opts.env||process.env.NODE_ENV||'development'constfolder=opts.errorPageFolderconsttemplatePath=Path.resolve(__dirname,'./error.html')letfileName='other'returnasync(ctx,next)=>{try{awaitnext()if(ctx.response.status===404&&!ctx.response.body)ctx.throw(404);}catch(e){letstatus=parseInt(e.status)constmessage=e.messageif(status>=400){switch(status){case400:case404:case500:fileName=status;break;default:fileName='other'}}else{status=500fileName=status}constfilePath=folder ?Path.join(folder,`${fileName}.html`) :templatePath// 渲染对应错误类型的视图,并传入参数对象try{// 指定视图目录nunjucks.configure(folder ?folder :__dirname)constdata=awaitnunjucks.render(filePath,{env:env,// 指定当前环境参数status:e.status||e.message,// 如果错误信息中没有 status,就显示为 messageerror:e.message,// 错误信息stack:e.stack// 错误的堆栈信息})// 赋值给响应体ctx.status=statusctx.body=data}catch(e){// 如果中间件存在错误异常,直接抛出信息,由其他中间件处理ctx.throw(500,`错误页渲染失败:${e.message}`)}}}}上面所做的是使用渲染引擎对模板文件进行渲染,并将生成的内容放到Http 的Response 中,展示在用户面前。感兴趣的同学可以去中间件源码中查看error.html 查看模板内容(其实是从koa-error 那里拿来稍作修改的)。
在代码的最后,我们还有一个异常的抛出ctx.throw(),也就是说,中间件处理时候也会存在异常,所以我们需要在最外层做一个错误监听处理。
修改middleware/index.js:
constpath=require('path')constip=require("ip")constbodyParser=require('koa-bodyparser')constnunjucks=require('koa-nunjucks-2')conststaticFiles=require('koa-static')constmiSend=require('./mi-send')constmiLog=require('./mi-log')constmiHttpError=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><divid="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><divid="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><divid="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><divid="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 中的页面展示内容,可以根据自己的项目信息修改,以上仅供参考。
至此,我们基本完成了用来处理『请求错误』的中间件。而这个中间件并不是固定的形态,大家在真实项目中,还需要多考虑自己的业务场景和需求,打造出适合自己项目的中间件。