# Koa(下一代web框架)

koa (opens new window) (中文网 (opens new window))是基于 Node.js 平台的下一代 web 开发框架,致力于成为应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石;利用async 函数丢弃回调函数,并增强错误处理,koa 没有任何预置的中间件,可快速的编写服务端应用程序。

# 核心概念

微信图片_20200713125342.jpg

  • Koa Application(应用程序)
  • Context(上下文)
  • Request(请求)、Response(响应)

# 初识 koa

// 创建一个 acquaintance 的文件夹
$ mkdir acquaintance

// 进入创建的文件夹
$ cd acquaintance

// 初始化 package.json
$ npm init -y

// 安装 koa,安装完之后
$ npm i koa

TIP

可以在 package.json 中查看安装的所有依赖

在工程目录里创建一个 app.js,代码如下:

const Koa = require('koa') // 引入koa
const app = new Koa() // 实例化koa

app.use(ctx => {
  ctx.body = 'hi koa'
})

// 启动应用程序  参数:端口号
app.listen(3003)

在终端中使用node app.js命令(前提是终端中的路径必须指向所创建工程文件夹的路径) 打开浏览器访问:http://localhost:3003 此时浏览器中就会输出hi koa,如下图:

WARNING

上面代码虽然轻松实现了一个 web 服务器,但是返回的数据和所请求都是固定的;并不适应真实的业务场景,比如:获取请求接口时的参数、方法、修改一次代码就要在终端中重新运行启动命令等;

由于使用的node app.js启动,所以每次更改都要重新启动,这样给我们开发带来了极大的不便利,所以我们可以使用一些第三方依赖来自动监听文件的变化并重新启动,开发环境可以使用nodemon 首先安装npm i nodemon -D,也可以全局安装此依赖,生产环境的话可以使用pm2 安装之后在package.jsonscripts中添加启动方式;如下

{
  "name": "acquaintance",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.13.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.7"
  }
}

在终端中执行命令:npm run dev,这样就不用我们每次修改都重新启动,执行之后就会在终端中提示,如下:

TIP

执行命令时,终端的路径必须指向当前程序

# 路由

路由即是路径处理函数之间的对应关系!

Koa中的路由作用:

  • 处理不同的 URL
  • 处理不同的 HTTP 方法 (GETPOSTPUTDELETEPATCHOPTIONS)
  • 解析 URL 上的参数

HTTP 协议而言,路由可以理解为,根据不同的 HTTP 请求,返回不同的响应;

使用方法:

  • 安装依赖:@koa/router
  • 定义路由
    • 实例化@koa/router
    • 注册router
    • 定义接口
  1. 安装依赖:@koa/router (opens new window)
$ npm i @koa/router
  1. 定义路由
  • 在 app.js 中引入@koa/router,然后再实例化
const Router = require('@koa/router')
const router = new Router({ prefix: '/api/v1' }) // 实例化的时候可以自定义一个接口前缀
  • 注册 router
app.use(router.routes()).use(router.allowedMethods())
  • 定义接口
router.get('/', async ctx => {
  ctx.body = {
    status: 200,
    message: 'hi @koa/router'
  }
})

router.get('/user', async ctx => {
  ctx.body = {
    status: 200,
    message: 'success',
    data: {
      nickname: 'Forest',
      age: 18,
      jobs: '前端攻城狮',
      skills: '搬砖'
    }
  }
})

完整代码如下:

const Koa = require('koa')
const Router = require('@koa/router')
const app = new Koa()
const router = new Router({ prefix: '/api/v1' }) // 添加接口前缀

router.get('/', async ctx => {
  ctx.body = {
    status: 200,
    message: 'hi @koa/router'
  }
})

router.get('/user', async ctx => {
  ctx.body = {
    status: 200,
    message: 'success',
    data: {
      nickname: 'Forest',
      age: 18,
      jobs: '前端攻城狮',
      skills: '搬砖'
    }
  }
})

app.use(router.routes()).use(router.allowedMethods())
// 启动应用程序  参数:端口号
app.listen(3003)

在浏览器中请求:http://localhost:3003/api/v1http://localhost:3003/api/v1/user,结果如下图:

# 中间件

中间件其实就是一个个函数,中间件是一系列的中间过程执行的函数,它可以通过app.use()注册;

  • 在 koa 中只会自动执行第一个中间件,后面的都需要我们自己调用。

  • koa 在执行中间件的时候都会携带两个参数context(可简化为ctx)和next,其中:

    • context是 koa 的上下文对象
    • next就是下一个中间件函数,也就是洋葱模型;

所谓洋葱模型,就是指每一个 Koa 中间件都是一层洋葱圈,它即可以掌管请求进入,也可以掌管响应返回。

换句话说:外层的中间件可以影响内层的请求和响应阶段,内层的中间件只能影响外层的响应阶段。

4c4b9807221b34a21ab0b4548a8f739.jpg

执行顺序按照 app.use()的顺序执行,中间件可以通过 await next()来执行下一个中间件,同时在最后一个中间件执行完成后,依然有恢复执行的能力。即,通过洋葱模型,await next()控制调用 “下游”中间件,直到 “下游”没有中间件且堆栈执行完毕,最终流回“上游”中间件。

下面这段代码的结果就能很好的诠释,示例:

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

app.use(async (ctx, next) => {
  console.log(`this is a middleware 1`)
  await next()
  console.log(`this is a middleware 1 end `)
})

app.use(async (ctx, next) => {
  console.log(`this is a middleware 2`)
  await next()
  console.log(`this is a middleware 2 end `)
})

app.use(async (ctx, next) => {
  console.log(`this is a middleware 3`)
  await next()
  console.log(`this is a middleware 3 end `)
})

app.listen(3004)

运行结果:

this is a middleware 1
this is a middleware 2
this is a middleware 3
this is a middleware 3 end
this is a middleware 2 end
this is a middleware 1 end

原理:中间件是如何执行的?

// 通过 createServer 方法启动一个 Node.js 服务
listen(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

Koa 框架通过 http 模块的 createServer 方法创建一个 Node.js 服务,并传入 this.callback() 方法, this.callback() 方法源码精简实现如下:

function compose(middleware) {
  // 这里返回的函数,就是上文中的 fnMiddleware
  return function (context, next) {
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 取出第 i 个中间件为 fn
      let fn = middleware[i]

      if (i === middleware.length) fn = next

      // 已经取到了最后一个中间件,直接返回一个 Promise 实例,进行串联
      // 这一步的意义是保证最后一个中间件调用 next 方法时,也不会报错
      if (!fn) return Promise.resolve()

      try {
          // 把 ctx 和 next 方法传入到中间件 fn 中,并将执行结果使用 Promise.resolve 包装
          // 这里可以发现,我们在一个中间件中调用的 next 方法,其实就是dispatch.bind(null, i + 1),即调用下一个中间件
          return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
          return Promise.reject(err)
      }
    }
  }
}
callback() {
  // 从 this.middleware 数组中,组合中间件
  const fn = compose(this.middleware);

  // handleRequest 方法作为 `http` 模块的 `createServer` 方法参数,
  // 该方法通过 `createContext` 封装了 `http.createServer` 中的 `request` 和 `response`对象,并将这两个对象放到 ctx 中
  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    // 将 ctx 和组合后的中间件函数 fn 传递给 this.handleRequest 方法
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  // on-finished npm 包提供的方法,该方法在一个 HTTP 请求 closes,finishes 或者 errors 时执行
  onFinished(res, onerror);
  // 将 ctx 对象传递给中间件函数 fnMiddleware
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

将 Koa 一个中间件组合和执行流程梳理为以下步骤:

  • 通过 compose 方法组合各种中间件,返回一个中间件组合函数 fnMiddleware
  • 请求过来时,会先调用 handleRequest 方法,该方法完成:
  • 调用 createContext 方法,对该次请求封装出一个 ctx 对象;
  • 接着调用 this.handleRequest(ctx, fnMiddleware)处理该次请求。
  • 通过 fnMiddleware(ctx).then(handleResponse).catch(onerror)执行中间件。

# 传参(取参)方式

  • 在 params 中取值,eg:http://localhost:3003/api/v1/user/1
// 前端请求
await axios.post('http://localhost:3003/api/v1/user/1')

// 服务端
router.post('/user/:id',async ctx => {
    //获取url的id
  cosnt { id } = ctx.params; // { id: 1 }
})
  • 在 query 中取值,也就是获取问号后面的。
// 前端
await axios.post('http://localhost:3003/api/v1/user?name=Forest&age=18')

// 服务端
router.post('/user', async ctx => {
  //获取url的id
  const { name, age } = ctx.request.query // { name: Forest, age: 18 }
})
  • 获取 header 中的参数:
//请求接口时设置请求头
axios
  .post(
    'http://localhost/user?name=Forest&age=18',
    {
      headers: {
        Author: 'token'
      }
    }
    //......
  )
  .then(res => console.log('res:', res))

//在服务端获取则是:
router.post('/user', async ctx => {
  //获取 url 的 id
  const { Author } = ctx.request.header // { Author: 'token' }
})
  • 获取 body 中的数据,在服务端获取 body 中的一些数据只能用一些外部的插件;如:koa-bodykoa-bodyparser 等等。 就以 koa-body 为例,首先安装 npm i koa-body -S,再引入:
// 服务端
const body = require('koa-body);

//然后在注册中间件:
app.use(body());

//在服务端获取则是:
router.post('/user', async ctx => {
    const res = ctx.request.body; // { name: 'Foreset', age: 18 }
});


//请求时有两种传参方式,一种是 json,另一种是 fromData;以 json 为例
axios.post('http://localhost/user', {name: 'Foreset', age: 18}).then(res => {
    console.log('res:', res)
});

# 创建 RESTful 接口

  • 路由 koa-router

  • 协议解析 koa-body

  • 跨域处理 @koa/cors

  • JSON美化 koa-json

koa的设计思想就是将数据处理都交给中间件:

npm install -S koa-router koa-body @koa/cors koa-json

案例:

//index.js

//引入对应的包
const Koa = require('koa')
const Router = require('koa-router')
const cors = require('@koa/cors')
const koaBody = require('koa-body')
const koaJson = require('koa-json')

const app = new Koa()
const router = new Router()

//prefix 访问路径都变为 ~/apiaa/ 为
router.prefix('/apiaa')

//访问 host/apiaa 返回文字 Hello World
router.get('/', ctx => {
  console.log(ctx)
  ctx.body = 'Hello World'
})

//访问 host/apiaa/api 就将get的参数的name和age返回回去 
router.get('/api', ctx => {
  //获取get的参数
  const params = ctx.request.query
  console.log(params.name)
  ctx.body = {
    name: params.name,
    age: params.age
  }
})

//访问 host/apiaa/async 服务器处理2秒后,返回Hello 2s later
router.get('/async', async (ctx) => {
  let result = await new Promise((resolve) => {
    setTimeout(function () {
      resolve('Hello 2s later')
    }, 2000)
  })
  ctx.body = result
})

// post接口,将post的参数再返回给请求
router.post('/post', async (ctx) => {
  let { body } = ctx.request
  console.log(body)
  console.log(ctx.request)
  ctx.body = {
    ...body
  }
})

app.use(koaBody())
app.use(cors())
//get接口,加上&pretty则返回格式化的接口
app.use(koaJson({ pretty: false, param: 'pretty' }))
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000)

TIP

koa-body 中间件的引入顺序必须在 router 之前,否则获取不了 post 请求携带的数据

深度截图_dde-desktop_20200713194258.png

params传参:

深度截图_dde-desktop_20200713194533.png

POST传参:

深度截图_选择区域_20200713194851.png

# 小案例

任务描述:

通过header里面传递一个role属性admin,使用post请求,发送给koa这边的/api/user接口json数据为{name: “imooc”, email: ["imooc@test.com](mailto:"imooc@test.com)"}

具体返回格式与要求如下:

POSTMan中发送请求

情景一:无name或者email

//img.mukewang.com/climg/5d5e477f0001e9eb05000237.jpg**

情景二:Header中无admin或者role不等于admin

//img.mukewang.com/climg/5d5e47930001d75205000291.jpg

//img.mukewang.com/climg/5d5e479d0001869a05000293.jpg

情景三:正常请求

//img.mukewang.com/climg/5d5e47ab0001dce005000315.jpg

效果图展示如下:

//img.mukewang.com/climg/5d5e47dd0001595a05000289.jpg 任务要求:

  1. koa侧判断role属性是否存在,是否是admin,不是,则返回status 401
  2. 判断email与name属性是否存在,并且不为空字符串
  3. 返回用户上传的数据,封装到data对象中,给一个code: 200,message: ‘上传成功’。

package.json:

{
  "name": "koa-homework",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "serve": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@koa/cors": "^3.1.0",
    "koa": "^2.13.0",
    "koa-body": "^4.2.0",
    "koa-router": "^9.1.0"
  }
}

app.js

const Koa = require('koa')
const Router = require('koa-router')
const cors = require('@koa/cors')
const koaBody = require('koa-body')

const app = new Koa()
const router = new Router()

router.prefix('/api')

router.post('/user', ctx => {
  require('./router/api/user')(ctx)
})

app.use(koaBody())
app.use(cors())
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000)