Koa学习笔记

127 阅读8分钟

运行环境

node 14.x 及以上版本

初体验

  • 新建一个空文件
  • 执行 npm i koa
  • 在根目录新建 app.js
// 引入koa
const Koa = require('koa')

// 创建koa实例
const app = new Koa()

// 创建一个中间件 所有的请求和响应都可以在这个中间件中处理
//  ctx: context 是koa提供的上下文对象
app.use(ctx => {
  // ctx.request 请求   ctx.response 响应
  ctx.response.body = 'hello world'
})

// 启动服务器
app.listen(3000, () => {
  console.log('服务运行在 http://localhost:3000');
})
  • 在终端运行node app.js
  • 在浏览器访问http://localhost:3000

Koa中处理get请求

...
app.use(ctx => {
  console.log(ctx.request.query);
  console.log(ctx.request.querystring);
  ctx.response.body = 'hello world'
})
...
  • ctx.request.query 获取url中参数的对象形式
  • ctx.request.querystring 获取url中参数的字符串形式

Koa中别名声明

许多 context 的访问器和方法为了便于访问和调用,简单的委托给他们的 ctx.request 和 ctx.response 所对应的等价方法, 比如说 ctx.type 和 ctx.length 代理了 response 对象中对应的方法,ctx.path 和 ctx.method 代理了 request 对象中对应的方法。

...
app.use(ctx => {
  console.log(ctx.request.query);
  console.log(ctx.request.querystring);
  ctx.response.body = 'hello world'
})
// 等价于
app.use(ctx => {
  console.log(ctx.query);
  console.log(ctx.querystring);
  ctx.body = 'hello world'
})
...

并不是request 或 response 所有的属性/方法都有别名 详情参照官网 www.koajs.com.cn/ 搜索Request aliasesResponse aliases

Koa中处理post请求

监听data和end事件

...
app.use(ctx => {
  // ctx.request  ctx.response  koa的请求和响应对象
  // ctx.req  ctx.res   nodejs原生的请求和响应对象

  let paramsStr = '' // 参数字符串
  //(1)监听原生 nodejs request 对象的data事件
  //     每有一段数据过来,都会触发一次data,数据量大,一次请求,会触发多次data事件
  ctx.req.on('data', (data) => {
    paramsStr += data
  })
  // (2) 监听 原生nodejs request对象的end事件
  //     一旦所有数据接收完成,触发end事件
  ctx.req.on('end', () => {
    // name=zs&age=16
    console.log(paramsStr)
  })

  ctx.response.body = 'hello world'
})
...

对post请求的字符串参数做处理

...
ctx.req.on('end', () => {
    // console.log(paramsStr) // name=zs&age=16
    
    const params = new URLSearchParams(paramsStr) 
    console.log(params); // URLSearchParams { 'name' => 'zs', 'age' => '16' }
    // params.get(key) 根据键获取
    // params.has(key) 根据键判断
    // params.keys(key) 拿到所有的键 返回的是ES6 Iterator 可供for of 遍历
  })
...

响应一个页面

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

const fs = require('fs')
const path = require('path')

// 封装一个读取html文件的工具函数  return promise对象
function getHTMLFile (filePath) {
  return new Promise((reslove, reject) => {
    fs.readFile(path.join(__dirname, filePath), (err, data) => {
      if (err) reject(err)
      // console.log(data) // 读取的数据格式,是Buffer格式 要用toString方法 转成可用的字符串
      reslove(data.toString())
    })
  })
}

app.use(async ctx => {
  ctx.body = await getHTMLFile('./test.html')
})
app.listen(3000, () => {
  console.log('服务 http://localhost:3000');
})

处理静态资源

...
const fs = require('fs')
const path = require('path')
// 封装一个读取静态资源图片的函数
function getImageFile(filePath) {
  return new Promise((resolve, reject) => {
    fs.readFile(path.join(__dirname, filePath), (err, data) => {
      if (err) return reject(err)
      resolve(data) // 这里保持Buffer格式,因为图片就是二进制数据,不需要转成字符串
    })
  })
}
app.use(async ctx => {
  // 重要:需要正确的设置静态资源文件(图片)的Content-Type响应头,否则在浏览器中,只会下载文件,不能查看图片
  ctx.set('Content-Type', 'image/png')
  ctx.body = await getImageFile('./static/001.png')
})
...

中间件

中间件,是一系列用来对请求进行处理函数

洋葱圈模型

在 Koa 中可以为 Koa 实例添加多个中间件,让一个请求可以经过多个中间件函数的处理。

中间件函数的格式为:

function someMiddleware(ctx, next) {
  // ... 对请求进行处理的逻辑 ... 
}
app.use(someMiddleware)

每个中间件函数都有两个参数:

  • ctx - 请求上下文对象,它包含和请求和响应相关的数据和操作
  • next - 是一个函数,调用后会执行下一个中间件

创建好中间件函数后,可以将它添加到 Koa 实例上

Koa 中间件的执行模型

6383319-09c1061cf194e0b8.png

上面为 Koa 框架的中间件模型示意图,看上去像个洋葱,所以我们称它为《洋葱圈模型》。

它的含义就是说:

Koa 中间件的执行就像洋葱一样,最早被 use 的中间件会放在最外层,而后续被 use 的中间件会往里层放;

当接收到一个请求的时候,处理顺序是:

  1. 从左到右从洋葱的最外层到最里层,也就是从最早 use 的中间件到最后 use 的中间件依次执行;
  2. 在到达最里层中间件后,会继续向右逐层往外执行,直到最外层中间件,然后返回 response

示例代码:

app.use(async (ctx, next) => {
    console.log('>>>>>>>> A 111')
    await next()
    console.log('>>>>>>>> A 222')
})

app.use(async (ctx, next) => {
    console.log('>>>>>>>> B 111')
    await next()
    console.log('>>>>>>>> B 222')
})

当接收请求后,我们可以看到控制台打印如下结果:

>>>>>>>> A 111
>>>>>>>> B 111
>>>>>>>> B 222
>>>>>>>> A 222

由此可知,一般情况下 Koa 的中间件都会执行两次:

  • 调用 next 之前为第一次。在调用 next 后,会把控制权传递给往里层的下一个中间件
  • 当里层不再有任何中间件、或未调用 next 函数时,就开始依次往外层中间件执行,执行的是外层中间件中调用 next 函数之后的代码

Koa洋葱圈设计理解

两个现象:

  • 中间件的执行了两次
  • 执行顺序奇怪,以next函数为分界点:先use的中间件,next前的逻辑先执行,但next后的逻辑反而后执行

思考: 为什么 Koa 要这么设计? 正常不应该是中间件按顺序从开始到结束执行吗?

说明:

  • 如果说使用中间件的场景, 不存在前后依赖的情况,从头到尾按顺序链式调用 => 完全没问题。

  • 但是, 如何存在依赖的情况呢? 比如: 前一个中间件部分代码, 依赖于下一个中间件的处理结果?

    链式一次执行就无法实现了!

结论:

  • next 前的逻辑 : 进行前期处理
  • 调用next,将控制流交给下个中间件,并await其完成,直到后面没有中间件或者没有next函数执行为止
  • 完成后一层层回溯执行各个中间件的后期处理(next 后的逻辑)

用 koa-body 处理 POST 传参

我们自己来处理获取 POST 请求的参数比较繁琐,实际开发中可以使用封装好的开源中间件

我们可以在 Koa 官方wiki 上找到很多优秀的开源中间件。而 koa-body 是一个专门用于获取通过请求体传递的数据的中间件。

使用步骤:

  1. 安装依赖包
npm i koa-body
  1. 引入并使用 koa-body
// 引入 koa-body
const koaBody = require('koa-body')

// ...

// 为 Koa 实例设置 koa-body 中间件
app.use(koaBody())

app.use(async ctx => {
  // 通过 ctx.request.body 获取请求体参数
  console.log('请求体参数', ctx.request.body) // 请求体参数: { name: 'zs', age: '16' }
  ctx.body = 'Hello'
})

用 koa-views 和 EJS 渲染页面

在 Koa 中,我们也可以使用模板引擎,通过模板语法将数据动态渲染成html页面内容,然后发送到客户端去。

koa-views 是一个支持使用多种模板引擎来渲染页面的中间件

使用步骤:

  1. 安装 koa-views 中间件 和 ejs 模板引擎
npm i koa-views ejs
  1. 引入并使用中间件
// 引入 koa-views
var views = require('koa-views');

// 配置和应用 koa-views 中间件(这里配置使用了 ejs 模板引擎,以及模板文件的存放目录)
// views方法:
// 参数1:配置模板存放的目录;
// 参数2:配置项,可以配置后缀名
app.use(views(__dirname + '/views', { extension: 'ejs' }))

// 引入 koa-views 后,就可以使用 ctx.render 函数渲染 ejs 模板文件了
app.use(async ctx => {
  // render 函数的第一个参数是模板文件名;第二个参数是要渲染到模板中的动态数据
  await ctx.render('test', {
    name: '小明',
    age: 18,
    books: ['三国演义', '红楼梦', '西游记', '水浒传']
  })
})

模板文件 views/test.ejs

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div>姓名:<%= name %></div>
    <div>年龄:<%= age %></div>
    <ul>
        <% books.forEach(function (book) { %>
            <li><%= book %></li>
        <% }) %>
    </ul>
</body>

</html>

用koa-static处理静态资源

安装依赖包

npm i koa-static

使用

...
const koaStatic = require('koa-static')
// public 目录 直接开放给用户(用户可以直接通过路径,访问到文件夹内的所有文件)
app.use(koaStatic('./public'))
...

示例

使用koa-body 和 koa-static做登录和上传文件的接口

...
const koaStatic = require('koa-static')
app.use(koaStatic('./public'))

const koaBody = require('koa-body')
// app.use(koaBody()) 对普通的post请求,可以进行参数处理 但是不好处理图片(没有配全参数)
app.use(koaBody({
  // 开启文件上传支持
  multipart: true,
  // 配置文件上传相关
  formidable: {
    // 文件上传目录
    uploadDir: './public/upload',
    // 默认不保留后缀名 需要保留后缀名
    keepExtensions: true
  }
}))

// 编写接口 测试
app.use(ctx => {
  // ctx.method ctx.url
  // 1. 登录接口
  if (ctx.method === 'POST' && ctx.url === '/login') {
    console.log('进行登录');
    console.log(ctx.request.body);
    const { username, password } = ctx.request.body
    if (username === 'admin' && password === '123456') {
      ctx.body = {
        status: 200,
        msg: '登录成功'
      }
    } else {
      ctx.body = {
        status: 201,
        msg: '登录失败'
      }
    }
  }
  // 2. 上传文件
  if (ctx.method === 'POST' && ctx.url === '/upload') {
    console.log('进行了文件上传');
    console.log(ctx.request.files.photo.path.replace('public', ''));
    ctx.body = {
      status: 200,
      msg: '上传文件成功',
      imgUrl: ctx.request.files.photo.path.replace('public', '')
    }
  }
})
...

用 koa-router 实现后端路由

自己实现不同的路由示例:

const Koa = require('koa')
const app = new Koa()
​
app.use(async ctx => {
  // 获取客户端请求的 URL 路径
  const url = ctx.url
  const method = ctx.method
​
  // 根据路径来判断具体要做的业务逻辑
  if (method === 'GET' && url === '/login') {
    ctx.body = '这是登录页'
  } else if (method === 'POST' && url === '/login') {
    ctx.body = '登录处理成功'
  } else if (method === 'GET' && url === '/register') {
    ctx.body = '这是注册页'
  } else if (method === 'POST' && url === '/register') {
    ctx.body = '注册处理成功'
  } else {
    ctx.body = '404 Not Found'
  }
})
​
app.listen(3000, () => {
  console.log('请访问 http://localhost:3000')
})

以上做法的弊端是:随着路由路径的增加,中间件代码变得很复杂。

而借助 koa-router 中间件,可以很清晰的创建和管理多个路由。

使用步骤:

  1. 安装依赖包
npm i @koa/router
  1. 引入 koa-router 并创建 Router 实例
// 引入 koa-router
const Router = require('@koa/router')
​
// 创建 Router 实例
const router = new Router()
  1. 在 Router 实例上创建路由处理器
// 静态路由 GET /login
router.get('/login', async ctx => {
    ctx.body = '这是登录页'
})
​
// 静态路由 POST /login
router.post('/login', async ctx => {
    ctx.body = '登录处理成功'
})
​
// 动态路由 GET /articles/123
router.get('/articles/:id', async ctx => {
    const id = ctx.params.id
    console.log(">>>>>>>> 动态路由参数 ID:", id);
    ctx.body = `ID 为 ${id} 的内容`
})
  1. 根据 router 生成路由相关的实际中间件函数,并设置给 Koa 实例
app.use(router.routes())