内容大纲
Hello Koa
const Koa = require('koa'); //引入koa
const app = new Koa();// 实例化一个对象
// 中间件一
app.use(ctx => {
ctx.body = 'Hello Koa'; // 设置响应体
});
app.listen(3000); //在3000端口上启动koa服务
两个核心方法:
- use():引用中间件
- listen():启动服务
源码分析
概要
koa的源码清晰明了,有四个见名知义的文件:
- application.js :定义Koa对象,use()、listen()
- contex.js:koa中的上下文与Node中的req,res是怎样代理(映射)的
- request.js:定义node中的req对象属性的getter、setter,方便取用
- response.js:封装了node中的res
application.js
1 listen()
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
http.createServe()创建了一个server对象,每当有请求时都会执行callback()这个回调方法
2 callback()
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
// 这就是原生Node起服务时On('request',(req,res)=>{.....})
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
callback 返回了一个闭包 handleRequest
高潮渐渐出现了:
- const fn = compose(this.middleware);暂时理解为把所有的中间件copmose(压缩合成)了一个fn,对,就是把所有use()里注册对方法压缩成了一个fn,牛逼吧,这也是koa中间件的巧妙之处,想想怎么实现呢,闭包+递归,看下文详解
- const ctx = this.createContext(req, res); 创建了ctx,怎么创建的?看contex.js
- return this.handleRequest(ctx, fn);this.handleReques才是真正处理请求然后响应的方法
ctx上下文
- ctx.resquest:
- ctx.response:官方推荐所有的响应处理都在response 对象上进行,应避免使用node的对象和属性
- ctx.req: Node的request对象
- ctx.res: Node的reponse对象
3 this.handleRequest(ctx,fnMiddleware)
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
接收上下文ctx和合成的中间件fn,完成了最后的响应。此处有两个问题:
- respond():看源码后发现了方法内部其实是设置了响应体,发给客户端了
- res.end()
- res.pipe()
- fnMiddleware(ctx).then(handleResponse).catch(onerror);
- 最骚的一顿操作
- fnMiddleware这个合成的中间件居然是一个promise,这样所有中间件执行完后才会去处理reponse响应,同时也会捕获未处理的中间件的错误
- 又绕回到最神秘的compose()了
compose(middlewares)
回过头来慢慢体会compose,渐入佳境
(最高潮),compose()方法是koa、koa-router的核心方法
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
去繁从简,去掉参数校验,容错处理等看主流程
function compose (middleware) {
return function (context, next) {
return dispatch(0) //直接执行第一个中间件
function dispatch (i) {
let fn = middleware[i]
if (!fn) return Promise.resolve() //走完最后一个中间件
//执行一个中间件
fn(context, function next () {
return dispatch(i + 1) //递归调用 闭包
})
}
}
}
简易实现
调用demo
const chalk = require('chalk')
const Koa = require('./src/tiny-koa')
const util = require('util')
const port = 9001
const app = new Koa()
app.use(async (ctx, next) => {
console.log('1.1')
await next()
console.log('1.2')
})
app.use(async (ctx, next) => {
console.log('2.1')
await next()
console.log('2.2')
})
app.use(async (ctx, next) => {
console.log('3.1')
await next()
console.log('3.2')
})
app.listen(port, _ => {
console.log(chalk.red(`koa server started at ${port}`))
})
app.on('error',error=>{
console.log( util.inspect(error))
})
// process.on('uncaughtException', function (err) {
// console.error('Caught exception: ' +chalk.red(err));
// });
TinyKoa:简易实现koa
版本一:简易实现next调用
const http = require('http')
const EventEmiter = require('events')
class TinyKoa extends EventEmiter {
constructor() {
super();
this.middlewares = []
this.ctx = {}
}
use(fn) {
if (typeof fn !== 'function') {
throw new Error('the middleware must be a function')
}
this.middlewares.push(fn)
}
listen(...argvs) {
try {
const server = http.createServer(this.onRequest.bind(this))
server.listen(...argvs)
} catch (error) {
this.emit('error', error)
}
}
onRequest(req, res) {
let middlewares = this.middlewares
let ctx = this.ctx
ctx.request = req
ctx.response = res
dispatch(0)
function dispatch(idx) {
try {
let fn = middlewares[idx]
fn && fn(ctx, () => dispatch(idx + 1))
} catch (error) {
this.emit('error', error)
}
}
}
}
module.exports = TinyKoa
核心方法
onRequest(req, res) {
let middlewares = this.middlewares
let ctx = this.ctx
ctx.request = req
ctx.response = res
dispatch(0)
function dispatch(idx) {
try {
let fn = middlewares[idx]
fn && fn(ctx, () => dispatch(idx + 1))
} catch (error) {
this.emit('error', error)
}
}
}
context.js
context核心方法
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.access('body')
.access('lastModified')
.access('etag')
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsEncodings')
.method('accepts')
.method('get')
.access('querystring')
.access('search')
.access('method')
.access('query')
.access('path')
context.js 就是将res,req上的常用属性、方法通过delegate代理到ctx上
delegate类核心方法
Delegator原型上定义有method、access方法,都返回了this,实现了链式调用,jquery也是这样
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
/**
* Delegate method `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
/**
* Delegator accessor `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
- 方法代理:bind
- 属性代理:类似vue实现响应式的原理
- defineSetter
- defineGetter
request.js
- 对node原生对象req上一些属性和方法的封装
- get
- set
response.js
- 对node原生对象res上一些属性和方法的封装
- get
- set
洋葱圈模型
ci
- 中间件洋葱圈图
- 中间件执行顺序图