koa2中间件koa和koa-compose源码分析原理(一)
koa是基于nodejs平台的下一代web开发框架,它是使用generator和promise,koa的中间件是一系列generator函数的对象。
当对象被请求过来的时候,会依次经过各个中间件进行处理,当有yield next就跳到下一个中间件,当中间件没有 yield next执行的时候,然后就会逆序执行前面那些中间件剩下的逻辑代码,比如看如下的demo:
const Koa = require('koa');
const app = new Koa(); app.use(async (ctx, next) => {
console.log(11111);
await next();
console.log(22222);
}); app.use(async (ctx, next) => {
console.log(33333);
await next();
console.log(44444);
}); app.use(async (ctx, next) => {
console.log(55555);
await next();
console.log(66666);
});
app.listen(3001);
console.log('app started at port 3000...'); // 执行结果为 11111 33333 55555 66666 44444 22222
当我们在浏览器访问 http://localhost:3001 的时候,会分别输出 11111 33333 55555 66666 44444 22222,如上代码是如下执行的:请求的时候,会执行第一个use里面的异步函数代码,先打印出 11111,然后碰到 await next() 函数,就执行第二个中间件,就会打印 33333, 然后又碰到 await next()后,就会跳转到下一个中间件,因此会打印 55555, 然后再碰到 awaitnext() 方法后,由于下面没有中间件了,因此先会打印 666666, 然后依次逆序返回上面未执行完的代码逻辑,然后我们就会打印44444,再依次就会打印 22222 了。
它的结构网上都叫洋葱结构,当初为什么要这样设计呢?因为是为了解决复杂应用中频繁的回调而设计的级联代码,它并不会把控制权完全交给一个中间件的代码,而是碰到next就会去下一个中间件,等下面所有中间件执行完成后,就会再回来执行中间件未完成的代码,我们上面说过koa是由一系列generator函数对象的,如果我们不使用koa的async语法的话,我们可以再来看下使用generator函数来实现如下:
const Koa = require('koa');
const app = new Koa(); app.use(function *(next) {
// 第一步进入
const start = new Date;
console.log('我是第一步');
yield next; // 这是第五步进入的
const ms = new Date - start;
console.log(ms + 'ms');
}); app.use(function *(next) {
// 这是第二步进入的
const start = new Date;
console.log('我是第二步');
yield next;
// 这是第四步进入的
const ms = new Date - start;
console.log('我是第四步' + ms);
console.log(this.url);
}); // response
app.use(function *() {
console.log('我是第三步');
this.body = 'hello world';
});
app.listen(3001);
console.log('app started at port 3000...');
执行的结果如下所示:
koa-compose 源码分析
源码代码如下:
'use strict' /**
* Expose compositor.
*/ module.exports = compose /**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/ 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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
koa部分源码如下:
module.exports = class Application extends Emitter {
/**
* Initialize a new `Application`.
*
* @api public
*/ constructor() {
super(); this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
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');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
} /**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback() {
const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
}; return handleRequest;
} /**
* Handle request in callback.
*
* @api private
*/ 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);
} /**
* Initialize a new context.
*
* @api private
*/ createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
} listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
如上就是koa部分主要代码,和 koa-compose 源码;首先先看 koa的源码中 use 方法,use方法的作用是把所有的方法存入到一个全局数组middleware里面去,然后返回 this,目的使函数能链式调用。我们之前做的demo如下这样的:
const Koa = require('koa');
const app = new Koa(); app.use(function *(next) {
// 第一步进入
const start = new Date;
console.log('我是第一步');
yield next; // 这是第五步进入的
const ms = new Date - start;
console.log(ms + 'ms');
}); app.use(function *(next) {
// 这是第二步进入的
const start = new Date;
console.log('我是第二步');
yield next;
// 这是第四步进入的
const ms = new Date - start;
console.log('我是第四步' + ms);
console.log(this.url);
}); // response
app.use(function *() {
console.log('我是第三步');
this.body = 'hello world';
});
app.listen(3001);
console.log('app started at port 3000...');
我们来理解下,我们会把 app.use(function *(){}) 这样的函数会调用use方法,然后use函数内部会进行判断是不是函数,如果不是函数会报错,如果是函数的话,就转换成 async 这样的函数,然后才会依次存入 middleware这个全局数组里面去,存入以后,我们需要怎么调用呢?我们下面会 使用 app.listen(3001), 这样启动一个服务器,然后我们就会调用 koa中的listen这个方法,端口号是3001,listen方法上面有代码,我们复制下来一步步来理解下;如下基本代码:
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
如上代码,它是通过node基本语法创建一个服务器,const server = http.createServer(this.callback()); 这句代码就会执行 callback这个方法,来调用,可能看这个方法,我们不好理解,这个方法和下面的基本方法是类似的;如下node基本代码:
var http = require("http")
http.createServer(function(req,res){
res.writeHead(200,{'Content-Type':'text/html'});
res.write("holloe world")
res.end("fdsa");
}).listen(8000);
如上代码我是创建一个8000服务器,当我们访问 http://localhost:8000/ 的时候,我们会调用 http.createServer 中的function函数代码,然后会打印数据,因此该方法是自动执行的。因此上面的listen方法也是这个道理的,会自动调用callback()方法内部代码执行的,因此koa中的callback代码如下:
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
callback() {
const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
}; return handleRequest;
}
然后会调用 const fn = compose(this.middleware); 这句代码,该compose 代码会返回一个函数,compose函数代码(也就是koa-compose源码)如下:
'use strict' /**
* Expose compositor.
*/ module.exports = compose /**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/ 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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose 函数代码 传入一个数组的中间件 middleware, 首先判断是不是数组,然后判断是不是函数,该参数 middleware 就是我们把use里面的所有函数存入该数组中的。该函数会返回一个函数。
然后继续往下执行 callback中的代码,如下代码执行:
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
}; return handleRequest;
首先会创建一个上下文对象 ctx,具体怎么创建可以看 koa源码中的 createContext 这个方法,然后会调用 koa中的handleRequest(ctx, fn)这个方法, 该方法传递二个参数,第一个是ctx,指上下文对象,第二个是 compose 函数中返回的函数,koa中的 handleRequest函数代码如下:
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);
}
最后一句代码 fnMiddleware(ctx).then(handleResponse).catch(onerror); 中的 fnMiddleware(ctx) 就会调用koa-compose 中返回的函数的代码,compose 函数返回的代码如下函数:
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
依次执行 app.use(function(req, res) {}) 中内这样的函数,会执行如上 dispatch方法,从0开始,也就是说从第一个函数开始执行,然后就会执行完成后,会返回一个promise对象,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); dispatch.bind(null, i + 1)) 该函数的作用是循环调用dispatch方法,返回promise对象后,执行then方法就会把值返回回来,因此执行所有的 app.use(function(req, res) {}); 里面这样的function方法,dispatch(i + 1) 就是将数组指针移向下一个,执行下一个中间件的代码,然后一直这样到最后一个中间件,这就是一直use,然后next方法执行到最后的基本原理,但是我们从上面知道,我们执行完所有的use方法后,并没有像洋葱的结构那样?那怎么回去的呢?其实回去的代码其实就是函数压栈和出栈,比如我们可以看如下代码就可以理解其函数的压栈和出栈的基本原理了。
如下函数代码:
function test1() {
console.log(1)
test2();
console.log(5)
return Promise.resolve();
}
function test2() {
console.log(2)
test3();
console.log(4)
} function test3() {
console.log(3)
return;
}
test1();
打印的顺序分别为 1, 2, 3, 4, 5;
如上代码就是koa的执行分析的基本原理了。
koa2中间件koa和koa-compose源码分析原理(一)的更多相关文章
- Flask框架(三)—— 请求扩展、中间件、蓝图、session源码分析
Flask框架(三)—— 请求扩展.中间件.蓝图.session源码分析 目录 请求扩展.中间件.蓝图.session源码分析 一.请求扩展 1.before_request 2.after_requ ...
- redux中的compose源码分析
1. redux中compose用来组合各种中间件来实现链式调用,例子如下 compose( applyMiddleware, devTools, persistState, createStore ...
- vue-router 结合源码分析原理
路由响应过程: 浏览器发出请求 服务器监听到num端口(或443)有请求过来,并解析url路径 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等) 浏览器根 ...
- redux middleware 源码分析
原文链接 middleware 的由来 在业务中需要打印每一个 action 信息来调试,又或者希望 dispatch 或 reducer 拥有异步请求的功能.面对这些场景时,一个个修改 dispat ...
- 【转载】Android异步消息处理机制详解及源码分析
PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbob ...
- Koa源码分析(三) -- middleware机制的实现
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...
- Koa源码分析(二) -- co的实现
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: Koa源码分析(一) -- generator Koa源码分析(二) -- co的实现 Koa源码分 ...
- Koa源码分析(一) -- generator
Abstract 本系列是关于Koa框架的文章,目前关注版本是Koa v1.主要分为以下几个方面: 1. Koa源码分析(一) -- generator 2. Koa源码分析(二) -- co的实现 ...
- 《分享》Koa2源码分析
曾经在公司内部做的一起关于koa源码的分享,希望对你有帮助: koa2 源码分析整理 koa2(2.4.1版本)源码主要包含四个js,包括application.js, context.js, req ...
随机推荐
- 洛谷P5205 【模板】多项式开根(多项式sqrt)
题意 题目链接 Sol 这个就很没意思了 求个ln,然后系数除以2,然后exp回去. #include<bits/stdc++.h> #define Pair pair<int, i ...
- 13张动图助你彻底看懂马尔科夫链、PCA和条件概率!
13张动图助你彻底看懂马尔科夫链.PCA和条件概率! https://mp.weixin.qq.com/s/ll2EX_Vyl6HA4qX07NyJbA [ 导读 ] 马尔科夫链.主成分分析以及条件概 ...
- 如何用ABP框架快速完成项目(8) - 用ABP一个人快速完成项目(4) - 能自动化就不要手动 - 使用自动化测试(BDD/TDD)
做为一个程序员, 深深知道计算机自动化的速度是比人手动的速度快的, 所以”快速”完成项目的一个重要武器就是: 能自动化就不要手动. BDD/TDD有很多优势, 其中之一就是自动化, 我们这节文章先 ...
- Mysql sql 功能分类
分类 DDL:数据定义语言,用于定义数据库对象,比如创建表,列,库等 DML:数据操作语言,用于添加.删除.修改数据 DQL:数据查询语言,用于查询(结果集是虚拟表,放在内存中) DCL:数据控制语言 ...
- mysql数据库的备份和恢复
Mysql数据库的备份和恢复 1.备份单个数据库 mysql数据库自带了一个很好用的备份命令,就是mysqldump,它的基本使用如下: 语法:mysqldump –u <用户名> -p ...
- 简单 PHP + MySQL 数据库动态网站制作 -- 摘抄
在这篇文章中,我尽量用最浅显易懂的语言来说明使用 PHP, MySQL 制作一个动态网站的基本技术.阅读本文需要简单的 HTML 基础知识和(任一编程语言的)编程基础知识(例如变量.值.循环.语句块的 ...
- WordCount作业修改
WordCount作业修改 github地址 需求说明 基本需求 功能说明 PSP 代码实现 字符总数查询 单词数查询 行数查询 总结 一.需求说明 1.基本需求 WordCount的需求可以概括为: ...
- matlab练习程序(点云表面法向量)
思路还是很容易想到的: 1.首先使用KD树寻找当前点邻域的N个点,这里取了10个,直接调用了vlfeat. 2.用最小二乘估计当前邻域点组成的平面,得到法向量. 3.根据当前邻域点平均值确定邻域质心, ...
- Git多人协作常用命令
Git多人协作工作模式: 首先,可以试图用git push origin branch-name推送自己的修改. 如果推送失败,则因为远程分支比你的本地更新早,需要先用git pull试图合并. 如果 ...
- [20190101]块内重整.txt
[20190101]块内重整.txt --//我不知道用什么术语表达这样的情况,我仅仅一次开会对方这么讲,我现在也照用这个术语.--//当dml插入数据到数据块时,预留一定的空间(pctfree的百分 ...