Koa源码系列之koa-compose
source link: http://www.wclimb.site/2019/12/11/Koa源码系列之koa-compose/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
从今天开始阅读学习一下 Koa
源码, Koa
对前端来说肯定不陌生,使用 node
做后台大部分会选择 Koa
来做, Koa
源码的代码量其实很少,接下来让我们一层层剥离,分析其中的源码
Koa用法
const Koa = require("koa"); const app = new Koa(); app.use(async ctx => { ctx.body = { a: 1 }; }); app.listen(3000);
浏览器打开 http://localhost:3000 可以看到返回了一个对象 {a:1}
洋葱模型
Koa最经典的就是基于洋葱模型的HTTP中间件处理流程,可以看下图
看下面代码是否能理解
const Koa = require("koa"); const app = new Koa(); app.use(async (ctx,next) => { console.log(1); setTimeout(()=>{ next() console.log(2) },1000) }); app.use(async (ctx,next) => { console.log(3); next() console.log(4) }); app.use(async (ctx,next) => { console.log(5); setTimeout(()=>{ console.log(6) },1000) next() console.log(7) }); app.listen(3000);
访问 http://localhost:3000 输出
1 3 // 1秒后开始输出 5 7 4 2 6 // 1秒后开始输出
不知道看到这你是否能够明白,不明白也没关系,我们可以深入源码来分析具体的实现
koa源码
https://github.com/koajs/koa/blob/master/lib/application.js#L77
执行 app.listen(3000)
;会走以下代码
/** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */ listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }、
https://github.com/koajs/koa/blob/master/lib/application.js#L141
我们接着看 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; }
在上面 callback
执行了 compose
方法来处理中间件, compose
方法就是今天我们需要重点讲的方法,等会再说,我们知道 Koa
大部分情况都是在处理中间件,那么它们是怎么拿到中间件的呢?
上面可以看到有一个 this.middleware
,中间件肯定是放在这里面的,于是我们搜索,在最上层可以在构造函数里看到初始化的时候,它把 this.middleware = []
;置为一个数组,我们使用中间件的时候是通过 app.use
来使用的。继续寻找 use
方法
https://github.com/koajs/koa/blob/master/lib/application.js#L120
use
方法
/** * 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; }
每次我们调用 app.use
, koa
都是把这个方法 push
到 middleware
数组里面。感觉说得有点啰嗦了,流程大体就是这样。
定义中间件数组 -> 收集中间件放到middleware数组里 -> 通过compose方法处理中间件达到洋葱模式
koa-compose
https://github.com/koajs/compose/blob/master/index.js
compose
是引用的 koa-compose
包,查看源码发现关键只有20几行
/** * 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) } } } }
我们默许中间件传入的是数组函数,进一步剥离,精简的代码如下
function compose (middleware) { return function (context, next) { 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
返回的是一个函数,首先执行了 dispatch(0)
,函数内容返回的都是Promise对象。
最初执行 dispatch(0)
;通过递归的方式不断的运行中间件,每个中间件都接收了两个参数,分别是 context
和 next
, context
其实就是 koa
的 ctx
,如果我们不传递 next
方法,后面的中间件就不会继续执行下去。
之前的代码我们可以初略的想象一下以下代码
function fn1(context, next) { console.log(1); next(); console.log(2); } function fn2(context, next) { console.log(3); next(); console.log(4); } function fn3(context, next) { console.log(5); next(); console.log(6); } function compose() { return fn1('', () => { return fn2('', () => { return fn3('', () => { }); }); }); }
输出:
其实就是一层层嵌套执行中间件的方法,执行完 next
再往上执行
------------------------------------------------------------------------------------- | | | fn1 | | +-----------------------------------------------------------+ | | | | | | | fn2 | | | | +---------------------------------+ | | | | | | | | | | | fn3 | | | | | | | | | ----------------------------------------------------------------------------------------------------> | | | | | | | 1 | 3 | 5 6 | 4 | 2 | | | | | | | | | +---------------------------------+ | | | | | | | +-----------------------------------------------------------+ | | | +----------------------------------------------------------------------------------+
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK