前言
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的数据,一般是来处理文件上传的,文件上传下面会详细讲到。
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的源码核心代码阅读
我们带着几个目标去阅读源码,这样很多问题都迎刃而解了。
- koa是通过什么方式导出的,为什么要new一个Koa呢?
- app.listen 是在哪里调用的原生http.createServer方法的呢?
- app.use 发生了什么?
- 中间件最后是怎样执行的呢?(这里是重点)
- 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呢?
我们阅读源码的时候,第一时间去看package.json
文件,一般这里有程序的入口文件,可以很轻松的找到入口文件。(其中一个this.middleware = []
比较重要)
我们从入口文件进来,可以查看到通过CJS的方式导出了一个Application
的一个类,所以我们koa接收的就是这个类,我们通过new的方式实例化这个app对象,然后执行构造器,进行一系列的初始化操作。
2.哪里调用的原生http.createServer方法的呢?
因为我们实例化了一个app对象,通过app.listen
的方式调用开启服务,所以这个类中必定有一个listen方法,我们如图所示,可以看到在这里调用的http.createServer
方法,并且返回这个方法,再把参数传入进去,server是this.callback执行后的返回值,this.callback我们等下在来看。
3.app.use发生了什么
我们调用app.use传入回调函数,所以必然有use方法,首先我们可以看到,前面两步做了一些边界处理,不是一个函数直接抛出一个异常,以及不是一个生成器对象提示信息,并且给它转换成fn,最后直接把我们传入的中间件push进原来初始化的数组里面。
4.中间件是怎么执行的?
首先这部分要从 http.createServer
说起,它是执行的this.callback函数的返回值,我们要从callback开始看。
callback这个返回了一个handleRequest函数,拿到req,res对象包装成一个ctx也就是上下文,express中间件是有三个参数,(req,res,next),而Koa里面把(req,res)包装成了一个对象。另外一个关键的就是这个fn函数,handleRequest引用了fn变量,这里是一个闭包,而核心的就是这个componse函数。
传入中间件数组,首先进行一些边界处理,主动调用了一下dispatch,这里是函数的柯里化的实现,取出当前下标的fn,然后进行最后返回了一个promise,并且对i下标进行了+1的操作,下次调用就会调用下一个中间件,从而实现递归调用,这里有一点我们要注意,因为是一个promise,会等到所有中间件全部执行完毕,最后调用.then方法的时候,一整个数组完全走完了,才会进行相应。
5.Koa的中间件同步实现
首先我有一个需求,那就是利用同步的的方式实现三个中间件字符串的累加,最终把这个累加的字符串返回给客户端,但是我有有要求,一定要在第一个中间件进行返回,废话少说,我们直接上代码。
我们看到这里我们会发现,我通过很巧妙的方式,把相应给客户端的那个信息放到了第一个中间件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的方式来实现之前需求,等到这一个中间件完全执行完的时候,才会开始执行第二个中间件...依次类推。
三、图解洋葱模型
用简单的话来描述就是中间件的执行过程,其实洋葱模型这个概念不只是在Koa里面有,在Express里面同步操作的话,也是符合洋葱模型这一概念的。当我们注册了中间件,然后调用了next()方法,会从上到下一层一层调用中间件,当我执行到最后一个中间件的时候,又会从下到上执行,最终响应数据给客户端。