一次读懂Koa

832 阅读9分钟

前言

  Koa -- 基于 Node.js 平台的下一代 web 开发框架。前端最主流的web框架主要是Koa以及Express,Express与Koa相比,Express功能更完善,大部分功能都封装了在一起,而Koa最主要的源码只有1600多行,其他的一些功能而是采用注册中间件的方式,包括Next中间件方式实现的不同,下面跟着我一起学习一下Koa,它因为什么而强大?

一、Koa的基础用法

1.Koa初体验

//yarn init -y & yarn add koa
const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  ctx.response.body = "Hello koa";
});

app.listen(8000, () => {
  console.log("服务器启动成功~");
});

  我们通过CJS的方式引入模块,很明显koa包返回了一个类给我们。我们通过new的方式创建了一个app对象。在通过use的方式注册了中间件,传入一个回调函数,ctx,next为回调执行的形参,通过 ctx.response.body返回数据给客户端。把服务监听到本地的8000端口上。(之后的实例化app,以及监听在后面就跳过了,不赘述了)

2.koa注册中间件

// ...省略引入koa以及实例化app对象
app.use((ctx, next) => {
  if (ctx.request.url === '/login') {
    if (ctx.request.method === 'GET') {
      // ctx.body = "登录成功"  为什么调用这种方法可以 调用下面那种也可以?后续看源码 给大家分析
      ctx.response.body = "登录成功";
    }
  } else {
    ctx.response.body = "other request~";
  }
});

//...省略监听

  app.use()传入一个回调,作为注册的中间件。首先有一个ctx对象,也就是context(上下文对象),判断请求的url和请求的方式来判断,返回信息给客户端。我们这里通过ctx来判断请求的路由,但是在实际开发中,请求的接口会非常多,这样的话会显得非常的臃肿,一般会采用koa-router中间件来引入。

3.koa中路由的使用

//自行下载 yarn add koa-router
//user.router.js 
const Router = require('koa-router');
const router = new Router({prefix: "/users"});

router.get('/', (ctx, next) => {
  ctx.response.body = "User Lists~";
});

router.delete('/', (ctx, next) => {
  ctx.response.body = "delete request~";
});

module.exports = router;
// main.js
const Koa = require('koa');
const userRouter = require('./user.router.js');
const app = new Koa();

app.use(userRouter.routes());
app.use(userRouter.allowedMethods());

app.listen(8000, () => {
  console.log("koa路由服务器启动成功~");
});

  1. 首先对koa-router进行下载,初始化router实例,传入prefix也就是前缀,注册方法的时候就可以不写,注册了一个get请求以及delete请求,最后把router对象到处
  2. 引入router对象,userRouter.routes() (注册使用导入router对象),userRouter.allowedMethods() (当客户端请求服务端的时候,会告诉我们哪些有效的)

4.query-params(参数解析)

const Koa = require('koa');
const app = new Koa();
const Router = require('koa-router');

const userRouter = new Router({prefix: '/users'});

userRouter.get('/:id', (ctx, next) => {
  console.log(ctx.request.params);
  console.log(ctx.request.query);
})

app.use(userRouter.routes());

app.listen(8000, () => {
  console.log("参数处理服务器启动成功~");
});

  1. params 127.0.0.1:8000/users/1 1就可以称之为params
  2. query 127.0.0.1:8000/users?name=ice&age=22 name=ice&age=22称之为query
当客户端传入这样的参数时,通过以上方式进行解析获取。

5.(json-urlencoded-formdata)参数解析

const Koa = require('koa');
//yarn add koa-bodyparser
const bodyParser = require('koa-bodyparser');
//yarn add koa-multer
const multer = require('koa-multer');
const Router = require('koa-router');

const app = new Koa();
const upload = multer();

app.use(bodyParser());
app.use(upload.any());

app.use((ctx, next) => {
  console.log(ctx.request.body);
  console.log(ctx.req.body);
  ctx.response.body = "Hello koa";
});

app.listen(8000, () => {
  console.log("服务器启动成功~");
});

  1. koa-bodyparser这个库可以对json参数进行解析,自行下载并且注册这个中间件,其中json以及urlencoded通过koa-bodyparser的中间件进行解析,都是客户端通过body的方式传入的参数,具体的大家可以下载一个postman或者一些其他工具。
  2. koa-multer用来解析form-data的数据,一般是来处理文件上传的,文件上传下面会详细讲到。

1650731013(1).jpg

6.文件上传的处理

const path = require('path')
const Koa = require('koa')
const Router = require('koa-router')
const multer = require('koa-multer')

const app = new Koa()
//初始化storage
const storage = multer.diskStorage({
  destination(req,file,cb) {
    cb(null,'./upload/')
  },
  filename(req,file,cb) {
    cb(null,Date.now() + path.extname(file.originalname))
  }
})

const upload = multer({storage})
const uploadRouter = new Router({prefix:'/upload'})

uploadRouter.post('/avatar',upload.single('avatar'),(ctx,next) => {
  console.log(ctx.req.file)
  ctx.response.body = '文件上传成功'
})

app.use(uploadRouter.routes())

app.listen(8000,() => {
  console.log('服务启动成功')
})

  初始化storage的地方,其中包括两步,destination fn回调cb传入上传的路径,filename fn回调cb重命名文件名的名称,按照当前的时间戳以及源文件的后缀名进行上传。

二、Koa的源码核心代码阅读

  我们带着几个目标去阅读源码,这样很多问题都迎刃而解了。

  1. koa是通过什么方式导出的,为什么要new一个Koa呢?
  2. app.listen 是在哪里调用的原生http.createServer方法的呢?
  3. app.use 发生了什么?
  4. 中间件最后是怎样执行的呢?(这里是重点)
  5. koa是如何进行同步异步操作的?

从哪里入手? 如何有效阅读源码? 无从下手?

const Koa = require('koa'); 

//1.为什么要new?
const app = new Koa(); 

//3.app.use发生了什么
app.use((ctx, next) => {
  ctx.response.body = "Hello koa";
});

//2.app.listen在哪调用原生方法
app.listen(8000, () => {
  console.log("服务器启动成功~");
});

  首先我们回顾上面的代码,带着问题去阅读源码,首先要确保你下载好了Koa包,然后进入node_modules文件中

1.为什么要new一个Koa呢?

1650799182.jpg

  我们阅读源码的时候,第一时间去看package.json文件,一般这里有程序的入口文件,可以很轻松的找到入口文件。(其中一个this.middleware = [] 比较重要)

1.jpg

  我们从入口文件进来,可以查看到通过CJS的方式导出了一个Application的一个类,所以我们koa接收的就是这个类,我们通过new的方式实例化这个app对象,然后执行构造器,进行一系列的初始化操作。

2.哪里调用的原生http.createServer方法的呢?

2.jpg

  因为我们实例化了一个app对象,通过app.listen的方式调用开启服务,所以这个类中必定有一个listen方法,我们如图所示,可以看到在这里调用的http.createServer方法,并且返回这个方法,再把参数传入进去,server是this.callback执行后的返回值,this.callback我们等下在来看。

3.app.use发生了什么

3.jpg

  我们调用app.use传入回调函数,所以必然有use方法,首先我们可以看到,前面两步做了一些边界处理,不是一个函数直接抛出一个异常,以及不是一个生成器对象提示信息,并且给它转换成fn,最后直接把我们传入的中间件push进原来初始化的数组里面。

4.中间件是怎么执行的?

  首先这部分要从 http.createServer说起,它是执行的this.callback函数的返回值,我们要从callback开始看。

4.jpg

  callback这个返回了一个handleRequest函数,拿到req,res对象包装成一个ctx也就是上下文,express中间件是有三个参数,(req,res,next),而Koa里面把(req,res)包装成了一个对象。另外一个关键的就是这个fn函数,handleRequest引用了fn变量,这里是一个闭包,而核心的就是这个componse函数。

5.jpg

  传入中间件数组,首先进行一些边界处理,主动调用了一下dispatch,这里是函数的柯里化的实现,取出当前下标的fn,然后进行最后返回了一个promise,并且对i下标进行了+1的操作,下次调用就会调用下一个中间件,从而实现递归调用,这里有一点我们要注意,因为是一个promise,会等到所有中间件全部执行完毕,最后调用.then方法的时候,一整个数组完全走完了,才会进行相应。

6.jpg

5.Koa的中间件同步实现

  首先我有一个需求,那就是利用同步的的方式实现三个中间件字符串的累加,最终把这个累加的字符串返回给客户端,但是我有有要求,一定要在第一个中间件进行返回,废话少说,我们直接上代码。

7.jpg

  我们看到这里我们会发现,我通过很巧妙的方式,把相应给客户端的那个信息放到了第一个中间件next()函数的调用下面,是什么造成了这样的机制呢?
  因为在这个中间件当中代码是同步的,会一层一层执行完的,当我第一次调用next()的函数的时候,会紧接着调用第二个中间件,第二个中间件再次调用next()函数调用完,调用第三个中间件,最后在从第三个中间件返回到第一个中间件。所以我们把相应信息写到第一个中间件调用next()的后面是完全正确的。我们也看过componse的实现,其内部原理就是一直调用了dispatch方法,递归调用,等真正调用完了才会一层层返回。

6.Koa的中间件异步实现

  现在我的需求变了一下,因为有异步的的操作的加入,我们模拟一下网络请求,在第三个中间件中加入promise.then 累加一下字符串。然后在从第一个中间件中返回。

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

app.use((ctx,next) => {
  ctx.request.mes = 'hhh'
  next()
  //因为是异步操作,所以会直接跳过第三个中间件直接返回了
  ctx.body = ctx.request.mes
})

app.use((ctx,next) => {
  ctx.request.mes += 'xxx'
  next()

})

app.use((ctx,next) => {
  new Promise((res,rej) => {res(123)}).then(res => {
    ctx.request.mes += res
  })
})

app.listen(8000,() => {
  console.log('服务启动成功')
})

  当客户端发送请求的时候你会发现,直接跳过了第三个累加字符串的过程,因为它这里是一个异步的操作,如果不太明白同步异步,可以看一下我之前写的有关于异步的文章,现在我要对上面的代码进行改造从而实现之前的需求。

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

app.use(async (ctx,next) => {
  ctx.request.mes = 'hhh'
  await next()
  ctx.body = ctx.request.mes
})

app.use(async (ctx,next) => {
  ctx.request.mes += 'xxx'
  await next()
})

app.use((ctx,next) => {
  new Promise((res,rej) => {res(123)}).then(res => {
    ctx.request.mes += res
  })
})

app.listen(8000,() => {
  console.log('服务启动成功')
})

  我们看过koa-compose之前的源码,我们可以很清楚的知道,它返回的是一个promise,所以我们调用next的时候可以把它变为同步的方式,利用async await的方式来实现之前需求,等到这一个中间件完全执行完的时候,才会开始执行第二个中间件...依次类推。

三、图解洋葱模型

onion.2972bdca.png

  用简单的话来描述就是中间件的执行过程,其实洋葱模型这个概念不只是在Koa里面有,在Express里面同步操作的话,也是符合洋葱模型这一概念的。当我们注册了中间件,然后调用了next()方法,会从上到下一层一层调用中间件,当我执行到最后一个中间件的时候,又会从下到上执行,最终响应数据给客户端。

middleware.5fabc0c7.gif