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的執行分析的基本原理了。
