看了koa的源码,才发现它竟如此简洁,真正作到了小而美的框架。一个字,amazing!javascript
koa的小,在于直接使用nodejs原生的http模块来提供web服务,很简单;html
koa的美,在于经过中间件来实现复杂的功能(中间件之间的衔接是经过上下文变量ctx);java
原生http服务node
let http = require('http')
let server = http.createServer((req, res)=>{
res.end('hello world.')
})
server.listen(3000)
复制代码
koagit
const Koa = require('koa')
const app = new Koa()
app.use((ctx, next)=>{
ctx.body = 'hello world.'
})
app.listen(3000)
复制代码
在use函数中,koa收集中间件;github
在listen函数中, koa调用http.createServer来建立web server服务(在其回调中处理上下文变量ctx、处理中间件的调用);web
myKoa
├── app.js //调用lib中的application.js获得koa服务
├── lib
│ ├── application.js //定义Application类,建立web服务,处理中间件
│ ├── context.js //上下文对象,代理一些属性,好比ctx.body就是代理的ctx.response.body
│ ├── request.js //请求对象
│ └── response.js //响应对象
└── package.json复制代码
代码链接:mykoa在github上的连接json
app.js
数组
let Koa = require('./lib/application')
let app = new Koa()
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
app.listen(3000, ()=>{
console.log('listening on 3000');
})
复制代码
当在浏览器上访问localhost:3000,在server中打印输出是:1 2 3 4 5 6浏览器
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
let Stream = require('stream')
class Application extends EventEmitter{
constructor(){
super()
this.middlewares = []
this.context = context
this.request = request
this.response = response
}ge
use(fn){//收集中间件
this.middlewares.push(fn)
}
compose(){//合成中间件,准确的说是串联中间件
return async ctx =>{
function createNext(middleware, oldNext){
return async ()=>{
await middleware(ctx, oldNext)
}
}
let len = this.middlewares.length
let next = async ()=>{
return Promise.resolve()
}
for (let i=len-1; i>=0; i--) {
let currentMiddleware = this.middlewares[i]
next = createNext(currentMiddleware, next)
}
await next()
}
}
createContext(req, res){
const ctx = Object.create(this.context)
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx
request.response = response
response.request = request
return ctx
}
handleRequest(ctx, fn){
let res = ctx.res
fn(ctx)//调用中间件
.then(()=>{
let bodyType = typeof ctx.body
if (bodyType == 'object') {
res.setHeader('Content-type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream) {
ctx.body.pipe(res)
} else if (bodyType == 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-type', 'text/html;charset=utf8')
res.end(ctx.body)
} else {
res.end('not found')
}
})
.catch(err=>{
this.emit('error', err)
res.statusCode = 500
res.end('server error')
})
}
callback(){
const fn = this.compose()//将中间件合成
return (req,res)=>{//返回web服务的回调
console.log('callback....')
const ctx = this.createContext(req, res)//获得上下文变量ctx
return this.handleRequest(ctx, fn)//处理请求
}
}
listen(...args){
let server = http.createServer(this.callback())//建立web服务
return server.listen(...args)
}
}
module.exports = Application
复制代码
listen和use函数没啥好说的,关键仍是compse函数的理解,这才是koa的核心
在koa源码中使用的是koa-compose中间件来实现的,这里参考的是别人手写的一个compose函数,很精妙。
compose(){
return async ctx =>{
function createNext(middleware, oldNext){
return async ()=>{
await middleware(ctx, oldNext)
}
}
let len = this.middlewares.length
let next = async ()=>{
return Promise.resolve()
}
for (let i=len-1; i>=0; i--) {
let currentMiddleware = this.middlewares[i]
next = createNext(currentMiddleware, next)
}
await next()
}
}
复制代码
compose函数的关键是怎么依次调用中间件,不对,准确的说法应该是串联中间件;
串联好中间件后,只要在handleRequest中调用第一个中间件就能够了。
上述代码的关键是createNext函数,将下一个应该被调用的middleware包裹在async函数中;
倒着遍历中间件数组,就能够把下一个应该被调用的middleware放在上个调用的next中。
说的可能有点晕,我把代码转化为一个调用图,就明白了,以app.js中的中间件调用为例:
发现async/await功能真的是超级好用啊。
koa的源码真的很短,也有不少很好的解析,本身看一遍,写一遍,仍是超有收获的。