控制器简介
什么是控制器?
- 拿到路由分配的任务,并执行
- 在Koa中,是一个中间件
控制器的作用
- 获取HTTP请求参数
- 处理业务逻辑(如获取数据,计算数据,存储数据等)
- 根据不同的情况发送不同的HTTP响应
获取HTTP请求参数
请求参数分为好几种:
- Query String,如?q=keyword(查询字符串参数往往是可选的)
- Router Params,如/users/:id(id就是路由参数,路由参数是必选的)
- Body(请求体),如{name:"小风车"}(Body在RESRful api中往往用JSON来表示)
- Header(请求头),如Accept(客户端可以接受哪种媒体格式)、Cookie(用来认证)
发送HTTP响应
发送HTTP响应分为三种方面
- 发送Status,如200/400等
- 发送Body,如{name:"小风车"}(这里的Body指的是返回内容)
- 发送Header(响应头),如Allow(代表允许的HTTP方法)、Content-Type(告诉客户端返回的格式应用哪种方式解析)
编写控制器最佳实践
- 每个资源的控制器放在不同的文件里
- 尽量使用类+类方法的形式编写控制器
- 严谨的错误处理(比如传入的参数都应校验,不能相信客户端传来的参数,而且一些逻辑上的错误也要检查并报出相应的错误信息)
学习断点调试获取HTTP请求参数
学习断点调试,就可以打个断点看一下在koa上如何获取各个参数。
在想要断点的语句添加断点,运行访问
可以看到左边调试栏出现大量参数,这里就有需要的HTTP请求参数。
ctx.query
:Query String
ctx.params
:Router Params
ctx.request.body
(需要安装中间件koa-bodyparser才会解析请求体):Body
请求创建新用户
可以看到在左边变量栏的ctx.request.body有我们请求的Body参数
ctx.header
:Header
发送HTTP响应
其实在之前一些实践中,已经学会了发送HTTP响应
发送Status:设置ctx.status
发送Status:设置ctx.status
发送Body:设置ctx.body
发送Header:使用ctx.set()
例如设置头信息"Allow":允许GET、POST请求方法
usersRouter.get('/',(ctx)=>{
ctx.set('Allow','GET,POST')
ctx.body = [{name:'小风车'}];
});
可以看到我们刚才设置的头信息“Allow”
编写控制器最佳实践
根据编写控制器最佳实践,重构下RESTful 项目的组织目录结构,使代码变得清爽,可读性更高。
- 每个资源的控制器放在不同的文件里
app文件夹——存放代码文件,如果安装了nodemon
插件自动重启
node.js文件,还需要在package.json修改启动路径:"start": "nodemon app/index.js"
app->routes文件夹——存放各个路由文件
重构首页路由home.js、用户路由users.js
const Router = require('koa-router');
const router = new Router();
router.get('/',(ctx)=>{
ctx.body = '<h1>这是主页</h1>'
})
//把实例化的路由导出
module.exports = router;
const Router = require('koa-router');
const router = new Router({prefix:'/users'});
const db = [{name:"小风车"}]
router.get('/',(ctx)=>{
ctx.body = db
});
//增
router.post('/',(ctx)=>{
db.push(ctx.request.body);
ctx.body = ctx.request.body;
});
//访问特定用户
router.get('/:id',(ctx)=>{
ctx.body = db[ctx.params.id*1]
});
//修改指定用户整体信息
router.put('/:id',(ctx)=>{
db[ctx.params.id*1] = ctx.request.body;
ctx.body = ctx.request.body;
});
//删除指定用户
router.delete('/:id',(ctx)=>{
db.splice(ctx.params.id*1,1);
ctx.status = 204
});
module.exports = router;
创建routes->index.js:编写代码批量读取一个目录下的文件
const fs = require('fs');
module.exports= (app)=>{
//同步读取目录
//当前目录表示__dirname
fs.readdirSync(__dirname).forEach(file=>{
if(file === 'index.js'){ return; }
//提取实例
const route = require(`./${file}`);
//注册
app.use(route.routes()).use(route.allowedMethods());
})
}
然后在index.js引入该方法批量地注册到app上
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router')
const app = new Koa();
//引入函数
const routing = require('./routes');
app.use(bodyparser());
//将router注册到app里
//批量读取文件,然后批量注册
routing(app);
app.listen(8080,()=>console.log('程序启动在 8080 端口了'));
可以看到重构路由之后可以正常使用
- 尽量使用类+类方法的形式编写控制器
创建app->controllers文件夹存放控制器。
控制器本质是中间件,中间件本质是函数,为了更合理组织这些控制器,最好采用类+类方法
的形式进行编写。
以home.js为例
class HomeCtl {
index(ctx){
ctx.body = '<h1>这是主页</h1>';
}
}
//导出实例化的控制器
module.exports = new HomeCtl();
然后在路由home.js使用它
const Router = require('koa-router');
const router = new Router();
//导出实例化的方法
const {index} = require('../controllers/home')
router.get('/',index);
//把实例化的路由导出
module.exports = router;
可以看到成功访问主页
同理构建用户控制器users.js
const db = [{name:"小风车"}]
class UsersCtl{
//获取用户列表
find(ctx){
ctx.body = db;
}
//获取特定用户
findById(ctx){
ctx.body = db[ctx.params.id
1];
}
//创建用户
create(ctx){
db.push(ctx.request.body);
ctx.body = ctx.request.body;
}
//更新用户
update(ctx){
db[ctx.params.id1] = ctx.request.body;ctx.body = ctx.request.body;
}
//删除用户
delete(ctx){
db.splice(ctx.params.id*1,1);
ctx.status = 204
}
}
module.exports = new UsersCtl();
然后在路由users.js使用它
const Router = require('koa-router');
const router = new Router({prefix:'/users'});
//delete是关键字,取别名
const {find,findById,create,update,delete:del} = require('../controllers/users');
router.get('/',find);
router.post('/',create);
router.get('/:id',findById);
router.put('/:id',update);
router.delete('/:id',del);
module.exports = router;
错误处理简介
错误处理是编程语言或计算机硬件里的一种机制,用来处理软件或信息系统中出现的异常状况。
异常状况有哪些?
-
运行时错误
,都返回500(运行时错误是建立在语法没有错的基础上,假如写的程序出现错误就会直接报语法错误了,程序就断掉了。)
比如,在运行时求一个undefined的属性就会出现运行时错误
。
-
逻辑错误
,如找不到(404)、先决条件失败(412)、无法处理的实体(参数格式不对,422)等
找不到某个网页或接口——返回404;
请求某个特定用户,请求的用户id根本不存在,这个先决条件就失败了——返回412
请求体里的参数格式不对——返回422,代表无法处理的实体
使用错误处理的好处
- 防止程序挂掉(在JavaScript中是使用
try catch
语法) - 告诉用户错误信息
- 便于开发者调试
Koa自带的错误处理
制造404、412、500三种错误,使用Koa自带的错误处理
404错误
:客户端造成的问题,例如尝试请求没有定义的接口/authors,Koa自带的错误处理会自动返回404
412错误
:例如查找不存在的id
findById(ctx){
//请求id大于等于存储用户的数组长度了
if(ctx.params.id*1 >= db.length){
//手动报错
ctx.throw(412,'先决条件失败:id大于等于数组长度了');
}
ctx.body = db[ctx.params.id*1];
}
Koa自带的错误处理会自动返回我们给出的报错状态码和错误信息
500错误
:通常是运行时错误,我们可以在语法没有错误的基础上构造一个运行时错误
例如:使用一个未声明undefined
的a
//获取用户列表
find(ctx){
a.b
ctx.body = db;
}
Koa自带的错误处理会自动返回500,而且还会在程序打印的日志打印出它的错误堆栈
自己编写错误处理中间件
Koa默认的错误处理有个缺点就是返回的是以文本形式的错误信息。在Restful API的最佳实践中, 要求使用JSON格式返回信息。
如果我们想要JSON格式的错误信息,我们可以自己编写一个错误处理中间件,放在执行顺序的最前面, 来对后面执行的代码进行错误处理,并且返回JSON格式的错误信息。
在通过断点调试
时,可知404错误
没有走过中间件,在最前面就报错了,自定义的错误处理无法捕捉到它;500错误
时,err.status和err.statusCode都为undefined,需要手动加短路语法设置状态码,让它返回500错误
app.use(async(ctx,next)=>{
try{
await next();
}catch(err){//捕获状态
ctx.status = err.status || err.statusCode || 500;
ctx.body = {
message:err.message
}
}
});
制造一个412
错误,可以看到错误信息就以JSON格式显示出来了。
使用koa-json-error进行错误处理
koa-json-error是一款比较优秀的错误处理中间件,这个中间件是专门为纯JSON的应用准备的,非常符合RESTful API。它还有许多丰富的功能,比如返回错误堆栈信息到客户端,方便开发调试使用;还可以配置错误信息,哪条错误信息不想返回到客户端时,可以选择禁用;还可以让400错误和404错误也返回JSON。
- 安装koa-json-error
npm install koa-json-error --save
- 使用koa-json-error的默认配置处理错误
const error = require('koa-json-error')
app.use(error())![](动画6.gif)
可以看到不仅返回JSON格式的错误还返回了堆栈信息
- 修改配置使其在生产环境下禁用错误堆栈的返回
返回信息中错误堆栈信息返回到客户端其实是不安全的,只能在开发阶段用一下,应该修改配置使其在生产环境下禁用错误堆栈的返回。
修改代码
app.use(error({
//postFormat定制返回格式
postFormat:(e,{stack,rest})=>process.env.NODE_ENV === 'production'? rest : {stack, rest}
}))
安装跨平台环境变量:npm i cross-env --save-dev
然后在package.json中设置运行环境
"scripts": {
"start": "cross-env NODE_ENV=production node app",
"dev":"nodemon app"
},
运行npm star
t就是在生产
环境下;运行npm run dev
就是在开发
环境下。
配置完毕后,在生产环境下,可以看到错误堆栈信息已经没有了
使用koa-parameter校验参数
在前面介绍到有一种异常错误是无法处理的实体(422错误)
,即参数格式不对。那么怎么校验参数呢?——需要使用koa-parameter
校验参数
- 安装koa-parameter
npm i koa-parameter --save
- 使用koa-parameter校验参数
在index.js
引入注册
const parameter = require('koa-parameter');
//将app传进去就可以全局使用
app.use(parameter(app));
以创建用户为例
//创建用户
create(ctx){
//校验请求体的name位字符串类型并且是必选的
ctx.verifyParams({
//必选:required 删掉也是默认为true
name:{ type:'string',required:true },
age:{ type:'number',required:false }
});
db.push(ctx.request.body);
ctx.body = ctx.request.body;
}
- 制造
422错误
来测试校验结果
可以看到如果不满足格式,就会自动返回422状态码
并且把非常详细的错误信息返回给客户端。