koa2框架学习

1,752 阅读8分钟

1 koa2简介

koa2是基于 Node.js 平台的下一代 web 开发框架。

koa2框架是基于Node.js平台的web开发框架,因提供了一套优化的方法,能够让开发者快速而愉快地编写服务端应用程序API而流行。

koa分1.0版本和2.0版本。koa和koa2中间件的思路是一样的,但是实现方式有所区别,koa是基于generator/yield的实现。和koa 1相比,koa2.0版本基于async/await实现,完全使用Promise并配合async来实现异步。

不同node版本对ES6支持的力度不同,所以需要node版本为v7.6.0及以上 来 支持 async/await方法。

2 koa基本使用

2.1 创建服务器

  1. 安装node,可以使用 nvm工具 来 安装 node
  2. 新建一个项目文件夹demo-koa
  3. 在根目录下创建package.json文件并配置,可以在终端通过npm init命令,该命令会提示配置包的相关信息,名称版本等等,都是包的基本配置信息。
  4. 终端进入项目根目录,执行npm install koa 或者 在package.json文件中添加koa依赖后执行 npm install
      "dependencies": {
        "koa": "^2.11.0"
      }
    
  5. 根目录下新建demo1.js文件,文件内容为
// demo1.js
const Koa = require('koa')
const app = new Koa()

const main = (ctx) => {
  ctx.body = 'Hello Koa' // ctx.response.body = 'Hello Koa'
}

app.use(main)
app.listen(3000)

运行脚本

node demo1.js

打开浏览器,访问 http://127.0.0.1:3000

这里使用原生http模块实现相同功能的逻辑

var http = require("http")
var server = http.createServer(function(req, res){
    res.writeHead(
        200,
        {"Content-type":"text/html;charset=UTF-8"})
    res.end("Hello Koa")
})

//运行服务器,监听3000端口
server.listen(3000, "127.0.0.1")

对比可以看到,koa的方法确实要简洁一些。不过这还只是koa的特点之一。

2.2 上下文(Context)对象

Koa 的 Context对象对原生node的request对象和response对象做了封装。
原生request对象表示http请求,包含了请求查询字符串、参数、http头部等属性。
原生response对象表示http请求,凡是需要向用户响应的操作,都需要通过该对象进行。

app.listen启动服务后,每一个请求都将创建一个 Context对象,用ctx标识符作为接收器引用。

app.use(async ctx => {
  ctx; // 这是 Context
  ctx.request; // 这是 koa Request,对node 的request做了进一步封装
  ctx.response; // 这是 koa Response, 对node 的response做了进一步封装
})

通过赋值这个ctx对象的属性type和body属性,可以控制返回给用户的内容。

const main = ctx => {
  // console.log(ctx.header)
  if (ctx.accepts('html')) {
    ctx.type = 'html';
    ctx.body = '<p>Hello World</p>';
  } else if (ctx.accepts('xml')) {
    ctx.type = 'xml';
    ctx.body = '<data>Hello World</data>';
  } else if (ctx.accepts('json')) {
    ctx.type = 'json';
    ctx.body = { data: 'Hello World' };
  } else {
    ctx.type = 'text';
    ctx.body = 'Hello World';
  }
};   

Context对象(ctx)的许多访问器和方法其实是直接委托给它的 ctx.request或 ctx.response的 ,例如 ctx.type 和 ctx.body 委托给 response 对象,ctx.path 和 ctx.method 委托给 request。也就是说ctx.body 和 ctx.response.body是相同的。

3 中间件

3.1 中间件概念

想要在收到HTTP请求后到返回响应结果之前的中间过程中(HTTP Request 和 HTTP Response 之间)做一些事情,如打印日志等,我们可以定义函数来实现功能。该函数就叫做中间件(middleware)。
简言之:koa支持自定义中间件函数在请求和响应做一些事情。

下面看下一个请求经过中间件最后生成响应的过程: 中间件:依次打印日志,记录处理时间,输出HTML

const Koa = require('koa')
const app = new Koa()

// 打印日志中间件
const logger = (ctx, next) => {
  console.log(`${ctx.request.method} ${ctx.request.url}`) // 打印URL
  next()
}
app.use(logger)

// 记录时间中间件
app.use((ctx, next) => {
  const start = new Date().getTime() // 当前时间
  next() // 调用下一个middleware
  const ms = new Date().getTime() - start // 耗费时间
  console.log(`Time: ${ms}ms`) // 打印耗费时间
});

// 返回结果中间件
const main = (ctx) => {
  ctx.body = 'Hello Koa' // ctx.response.body = 'Hello Koa'
}
app.use(main)
app.listen(3000)

app.use(middleWare)用来加载中间件。

访问 http://127.0.0.1:3000/index1.html, 命令行窗口会显示

GET /index1.html
Time: 1ms

Koa本身没有捆绑任何中间件(使得其本身轻巧),但是可以通过其提供的use方法来加载各种中间件(上面示例中的main方法也是中间件),这样一来可以在HTTP Request 和 HTTP Response 之间做很多的事情。这是koa简洁却功能强大的原因。

3.2 中间件栈

每个中间件默认接受两个参数

  • 第一个参数是 Context对象
  • 第二个参数是next函数,对于一个中间件函数来讲,next函数很关键
    • 无next调用
      • 不执行后续的一系列中间件
    • 有next调用,表示接着往下执行
      • 调用next函数之前,执行当前中间件逻辑
      • 调用next函数之后,该函数暂停,把执行权转交给下一个中间件,下一个中间件开始执行

多个中间件通过app.use(middleWare)加载后,形成一个“中间件栈”,执行顺序以中间件里的next函数调用为界限:

const koa = require('koa');
const app = new koa();

const one = (ctx, next) => {
  console.log('one next之前')
  next()
  console.log('one next之后')
}

const two = (ctx, next) => {
  console.log('two next之前')
  next()
  console.log('two next之后')
}
const three = (ctx, next) => {
  console.log('three next之前')
  next()
}
const four = (ctx, next) => {
  next()
  console.log('four next之后')
}
const five = (ctx, next) => {
  console.log('five next之前')
  next()
  console.log('five next之后')
}

app.use(one)
app.use(two)
app.use(three)
app.use(four)
app.use(five)
app.use(ctx => {
  console.log('返回结果');
  ctx.body = 'hello'
})

app.listen(3000)

打印顺序:

one next之前
two next之前
three next之前
five next之前
返回结果
five next之后
four next之后
two next之后
one next之后
  1. 最外层的中间件首先执行。
  2. 调用next函数,把执行权交给下一个中间件。
  3. ...
  4. 最内层的中间件最后执行。
  5. 执行结束后,把执行权交回上一层的中间件。
  6. ...
  7. 最外层的中间件收回执行权之后,执行next函数后面的代码。

中间件流程控制简单描述就是:中间件的执行是以next为界限,先执行本层中next以前的部分,遇到next后执行下一层,一层层下去。当最后一层中间件执行完毕后,再返回上一层执行next以后的部分,一层层回来。所以如果某一层没有next,说明到该层方法就执行完毕了,就开始返回上一层执行上一层的next之后的部分了。

如果中间件内部没有调用next函数,那么执行权就不会传递下去,而是向上返回了。
试试如果把five函数离得next去掉,则浏览器不会正常显示返回内容(Not Found)了。

这也就是koa所谓的洋葱模型,从外面一层层的深入,再一层层的穿出来。

3.3 异步中间件

异步中间件就是函数里包含了异步操作,中间件必须被写成 async 函数。 示例:fs.readFile异步读取文件

<html>
  <body>
    <h1>index1.html的内容</h1>
    <p>Hello Wrold</p>
  </body>
</html>
// demo5.js
const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async (ctx) => {
  const data = await fs.readFile('index1.html', 'utf8')
  ctx.response.type = 'html'
  ctx.response.body = data 
}

app.use(main)
app.listen(3000)

// 原fs模块读取文件后都要用回调函数来处理结果
// 而fs.promised可以用promised来代替回调函数
// 通过 npm install fs.promised 安装 fs.promised

// fs.readFile('index1.html', 'utf8', function (err, data) {
//   if (!err) {
//     console.log( data )
//   } else {
//     console.log("读取文件出错")
//   }
// })

执行node demo5.js后,浏览器访问http://127.0.0.1:3000/后展示index1.html网页

4 错误处理

4.1 ctx.throw

Koa 提供了ctx.throw()方法,用来抛出错误,这个时候网页展示 Internal Server Error

4.2 ctx.status

如果ctx.body为空,设置ctx.status

  • ctx.status=404, 网页展示 Not Found
  • ctx.status=500, 网页展示 Internal Server Error

4.3 try...catch捕获

可以为每个中间件写一个try...catch,也可以让最外层的中间件负责所有中间件的错误处理。

const Koa = require('koa')
const app = new Koa()
const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.body = {
      message: err.message
    };
  }
}

const one = (ctx, next) => {
  console.log('one next之前')
  ctx.throw("one 出错了")
  next()
}

const two = (ctx, next) => {
  console.log('two next之前')
  next()
  console.log('two next之后')
}

const main = (ctx) => {
  console.log("main")
  ctx.body = 'Hello Koa' // ctx.response.body = 'Hello Koa'
}

app.use(handler);
app.use(one);
app.use(two);
app.use(main)
app.listen(3000)

4.4 监听error事件

Koa继承了原生event模块的EventEmitter 类,所有运行过程中一旦出错,Koa 会触发一个error事件。监听这个事件,也可以处理错误。

const Koa = require('koa')
const app = new Koa()

app.on('error', (err, next) => { 
  console.error('server 出错了', err)
})

const main = ctx => { 
    ctx.throw(500)
}

app.use(main)
app.listen(3000)

需要注意的是,如果错误被try...catch捕获,就不会触发error事件。这时,必须调用ctx.app.emit(),手动释放error事件,才能让监听函数生效。

app.on('error', (err, next) => { 
  console.error('server 出错了', err)
})

const main = ctx => { 
    // ctx.throw(500)
    try {
      ctx.throw(500)
    } catch {
      ctx.status = 500
      ctx.app.emit('error', err, ctx) 
    }
}

5 Cookie

ctx.cookies 用来读写 Cookie。 ctx.cookies.set(key,value) 设置cookie ctx.cookies.get(key) 获取cookie

const Koa = require('koa')
const compose = require('koa-compose')
const app = new Koa()

const main = ctx => {
    const n = Number(ctx.cookies.get('view') || 0) + 1;
    ctx.cookies.set('view',n)
    ctx.body = n + ' views';
}

app.use(main)
app.listen(3000)

6 其他相关功能模块

  • koa-route 模块:路由判断
  • koa-static模块:方便静态资源
  • koa-body模块: 支持从 POST 请求的数据体里面提取键值对、文件上传(没理解用法。。)
  • koa-compose模块:将多个中间件合成为一个大的中间件,koa2的源码实现了就用的这个该模块的compose方法来将所有中间件合成为一个大的中间件后再处理的。

总结

koa2作为一个node.js的web开发框架,只提供了应用(Application)、上下文(Context),通过中间件机制来支持在请求和响应之间优雅的实现很多功能。

参考资料

koa2中文官方文档

www.ruanyifeng.com/blog/2017/0…

www.kancloud.cn/sophie_u/ko…