A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.html
1.0 版本是经过组合不一样的 generator,能够免除重复繁琐的回调函数嵌套,并极大地提高错误处理的效率。前端
2.0版本Koa放弃了generator,采用Async 函数实现组件数组瀑布流式(Cascading)的开发模式。node
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
复制代码
核心代码就是lib目录下的四个文件git
koa的流程分为三个部分:初始化 -> 启动Server -> 请求响应github
初始化json
启动serverapi
请求响应数组
初始化
定义了三个对象,context
, response
, request
bash
request
定义了一些set/get访问器,用于设置和获取请求报文和url信息,例如获取query数据,获取请求的url(详细API参见Koa-request文档)服务器
response
定义了一些set/get操做和获取响应报文的方法(详细API参见Koa-response 文档)
context
经过第三方模块 delegate 将 koa 在 Response 模块和 Request 模块中定义的方法委托到了 context 对象上,因此如下的一些写法是等价的:
//在每次请求中,this 用于指代这次请求建立的上下文 context(ctx)
this.body ==> this.response.body
this.status ==> this.response.status
this.href ==> this.request.href
this.host ==> this.request.host
......
复制代码
为了方便使用,许多上下文属性和方法都被委托代理到他们的 ctx.request
或 ctx.response
,好比访问 ctx.type
和 ctx.length
将被代理到 response
对象,ctx.path
和 ctx.method
将被代理到 request
对象。
每个请求都会建立一段上下文,在控制业务逻辑的中间件中,ctx
被寄存在this
中(详细API参见 Koa-context 文档)
启动Server
- 初始化一个koa对象实例
- 监听端口
var koa = require('koa');
var app = koa()
app.listen(9000)
复制代码
解析启动流程,分析源码
application.js
是koa的入口文件// 暴露出来class,`class Application extends Emitter`,用new新建一个koa应用。
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false; // 是否信任proxy header,默认false // TODO
this.middleware = []; // 保存经过app.use(middleware)注册的中间件
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development'; // 环境参数,默认为 NODE_ENV 或 ‘development’
this.context = Object.create(context); // context模块,经过context.js建立
this.request = Object.create(request); // request模块,经过request.js建立
this.response = Object.create(response); // response模块,经过response.js建立
}
...
复制代码
Application.js
除了上面的的构造函数外,还暴露了一些公用的api,好比经常使用的 listen
和use
(use放在后面讲)。
做用: 启动koa server
语法糖
// 用koa启动server
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
// 等价于
// node原生启动server
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001); // on mutilple address
复制代码
// listen
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
复制代码
封装了nodejs的建立http server,在监听端口以前会先执行this.callback()
// callback
callback() {
// 使用koa-compose(后面会讲) 串联中间件堆栈中的middleware,返回一个函数
// fn接受两个参数 (context, next)
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
// this.callback()返回一个函数handleReqwuest,请求过来的时候,回调这个函数
// handleReqwuest接受参数 (req, res)
const handleRequest = (req, res) => {
// 为每个请求建立ctx,挂载请求相关信息
const ctx = this.createContext(req, res);
// handleRequest的解析在【请求响应】部分
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
复制代码
const ctx = this.createContext(req, res);
建立一个最终可用版的context
ctx上包含5个属性,分别是request,response,req,res,app
request和response也分别有5个箭头指向它们,因此也是一样的逻辑
补充了解 各对象之间的关系
最左边一列表示每一个文件的导出对象
中间一列表示每一个Koa应用及其维护的属性
右边两列表示对应每一个请求所维护的一些列对象
黑色的线表示实例化
红色的线表示原型链
蓝色的线表示属性
请求响应
回顾一下,koa启动server的代码
app.listen = function() {
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
复制代码
// callback
callback() {
const fn = compose(this.middleware);
...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
复制代码
callback()
返回了一个请求处理函数this.handleRequest(ctx, fn)
// handleRequest
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 请求走到这里标明成功了,http respond code设为默认的404 TODO 为何?
res.statusCode = 404;
// koa默认的错误处理函数,它处理的是错误致使的异常结束
const onerror = err => ctx.onerror(err);
// respond函数里面主要是一些收尾工做,例如判断http code为空如何输出,http method是head如何输出,body返回是流或json时如何输出
const handleResponse = () => respond(ctx);
// 第三方函数,用于监听 http response 的结束事件,执行回调
// 若是response有错误,会执行ctx.onerror中的逻辑,设置response类型,状态码和错误信息等
onFinished(res, onerror);
// 执行中间件,监听中间件执行结果
// 成功:执行response
// 失败,捕捉错误信息,执行对应处理
// 返回Promise对象
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
做用: 将函数推入middleware数组
use(fn) {
// 首先判断传进来的参数,传进来的不是一个函数,报错
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 判断这个函数是否是 generator
// koa 后续的版本推荐使用 await/async 的方式处理异步
// 因此会慢慢不支持 koa1 中的 generator,再也不推荐你们使用 generator
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
// 若是是 generator,控制台警告,而后将函数进行包装
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 将函数推入 middleware 这个数组,后面要依次调用里面的每个中间件
this.middleware.push(fn);
// 保证链式调用
return this;
}
复制代码
const fn = compose(this.middleware)
app.use([MW])仅仅是将函数推入middleware数组,真正让这一系列函数组合成为中间件的,是koa-compose,koa-compose是Koa框架中间件执行的发动机
'use strict'
module.exports = compose
function compose (middleware) {
// 传入的 middleware 必须是一个数组, 不然报错
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 循环遍历传入的 middleware, 每个元素都必须是函数,不然报错
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
// 若是中间件中没有 await next ,那么函数直接就退出了,不会继续递归调用
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
Koa2.x的compose方法虽然从纯generator函数执行修改为了基于Promise.all,可是中间件加载的中心思想没有发生改变,依旧是从第一个中间件开始,遇到await/yield next,就中断本中间件的代码执行,跳转到对应的下一个中间件执行期内的代码…一直到最后一个中间件,而后逆序回退到倒数第二个中间件await/yield next下部分的代码执行,完成后继续会退…一直会退到第一个中间件await/yield next下部分的代码执行完成,中间件所有执行结束
级联的流程,V型加载机制
koa-router 路由
对其实现机制有兴趣的能够戳看看 -> Koa-router路由中间件API详解
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
const Router = require('koa-router')
// 子路由1
let home = new Router()
home.get('/', async ( ctx )=>{
let html = `
<ul>
<li><a href="/page/helloworld">/page/helloworld</a></li>
<li><a href="/page/404">/page/404</a></li>
</ul>
`
ctx.body = html
})
// 子路由2
let page = new Router()
page.get('hello', async (ctx) => {
ctx.body = 'Hello World Page!'
})
// 装载全部子路由的中间件router
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())
// 加载router
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => {
console.log('[demo] route-use-middleware is starting at port 3000')
})
复制代码
koa-bodyparser 请求数据获取
获取GET请求数据有两个途径
是从上下文中直接获取
是从上下文的request对象中获取
对于POST请求的处理,koa2没有封装获取参数的方法须要经过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{"a":"1", "b":"2", "c":"3"})
对于POST请求的处理,koa-bodyparser中间件能够把koa2上下文的formData数据解析到ctx.request.body中
...
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
app.use( async ( ctx ) => {
if ( ctx.url === '/' && ctx.method === 'POST' ) {
// 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
let postData = ctx.request.body
ctx.body = postData
} else {
...
}
})
app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
复制代码
koa-static 静态资源加载
为静态资源访问建立一个服务器,根据url访问对应的文件夹、文件
...
const static = require('koa-static')
const app = new Koa()
// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'
app.use(static(
path.join( __dirname, staticPath)
))
app.use( async ( ctx ) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000')
})
复制代码
PS:广告一波,网易考拉前端招人啦~有兴趣的戳我投递简历