start
基于 koa 2.11 按以下流程分析:
const Koa = require('koa'); const app = new Koa(); const one = (ctx, next) => { console.log('1-Start'); next(); ctx.body = { text: 'one' }; console.log('1-End'); } const two = (ctx, next) => { console.log('2-Start'); next(); ctx.body = { text: 'two' }; console.log('2-End'); } const three = (ctx, next) => { console.log('3-Start'); ctx.body = { text: 'three' }; next(); console.log('3-End'); } app.use(one); app.use(two); app.use(three); app.listen(3000);
app.use()
use 方法定义在 koa/lib/application.js
中:
use(fn) { // check middleware type, must be a function if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); // 兼容 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'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); // 存储中间 this.middleware.push(fn); return this; }
this.middleware
这就是一个数组,用来存放所有中间件,然后按顺序执行。
this.middleware = [];
app.listen()
这个方法定义在 koa/lib/application.js
中:
listen(...args) { debug('listen'); // 创建 http 服务并监听 const server = http.createServer(this.callback()); return server.listen(...args); }
this.callback()
callback() { // 处理中间件 const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { // 创建 Context const ctx = this.createContext(req, res); // 执行中间件处理请求和响应 return this.handleRequest(ctx, fn); }; return handleRequest; }
this.handleRequest
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); // 将响应发出的函数 const handleResponse = () => respond(ctx); onFinished(res, onerror); // 这里会将 ctx 传给中间件进行处理, // 当中间件流程走完后, // 会执行 then 函数将响应发出 return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
respond(ctx)
function respond(ctx) { // 省略其他代码 // ... // 发出响应 res.end(body); }
捋一捋流程,由上面的代码可以知道,存放中间的数组是通过 compose
方法进行处理,然后返回一个fnMiddleware
函数,接着将 Context 传递给这个函数来进行处理,当fnMiddleware
执行完毕后就用respond
方法将响应发出。
compose(this.middleware)
compose 函数通过koa-compose
引入:
const compose = require('koa-compose');
compose 定义在koajs/compose/index.js
下
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!') } return function (context, next) { // 这个 index 是标识上一次执行的中间件是第几个 let index = -1 // 执行第一个中间件 return dispatch(0) function dispatch (i) { // 检查中间件是否已经执行过, // 举个例子,当执行第一个中间件时 dispatch(0), // i = 0, index = -1, 说明没有执行过, // 然后 index = i, 而 index 通过闭包保存, // 如果执行了多次,就会报错 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 通过传入的索引从数组中获取中间件 let fn = middleware[i] // 如果当前索引等于中间件数组的长度, // 说明已经中间件执行完毕, // fn 为 fnMiddleware(ctx) 时没有传入的第二个参数, // 即 fn = undefined if (i === middleware.length) fn = next // fn 为 undefined, 返回一个已经 reolved 的 promise if (!fn) return Promise.resolve() try { // 执行中间件函数并将 dispatch 作为 next 函数传入 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
结束执行流程
现在来捋一下 fnMiddleware
的执行流程:
// fnMiddleware 接收两个参数 function (context, next) { // .... } // 将 context 传入,并没有传入 next, // 所以第一次执行时是没有传入 next 的 fnMiddleware(ctx).then(handleResponse).catch(onerror);
next == undefined
时会结束中间件执行,流程如下:
function dispatch (i) { //... // 通过传入的索引从数组中获取中间件, // 但是因为已经执行完了所有中间件, // 所以当前 i 已经等于数组长度, // 即 fn = undefined let fn = middleware[i] // 如果当前索引等于中间件数组的长度, // 说明已经中间件执行完毕, // 又因为 fnMiddleware(ctx) 时没有传入的第二个参数 next, // 所以 fn = undefined if (i === middleware.length) fn = next // fn 为 undefined, 返回一个已经 reolved 的 promise // 中间件执行流程结束 if (!fn) return Promise.resolve() // ... }
中间件执行流程
上面先说了结束流程,现在说一下如何顺序执行,形成洋葱模型:
function dispatch (i) { // ...省略其他代码 try { // 分步骤说明 // 首先通过 bind 将 dispatch 构建为 next 函数 const next = dispatch.bind(null, i + 1); // 将 ctx, next 传入执行当前中间件, // 当在中间件中调用 next() 时, // 本质上是调用 diapatch(i + 1), // 也就是从数组中获取下一个中间件进行执行, // 在这时,会中断当前中间件的执行流程转去执行下一个中间件, // 只有当下一个中间件执行完毕,才会恢复当前中间件的执行 const result = fn(context, next); // 中间件执行完毕,返回已经 resolve 的 promise, // 那么上一个中间件接着执行剩下的流程, // 这样就形成了洋葱模型 return Promise.resolve(result); } catch (err) { return Promise.reject(err) } }
开头的例子执行结果如下:
const one = (ctx, next) => { console.log('1-Start'); next(); ctx.body = { text: 'one' }; console.log('1-End'); } const two = (ctx, next) => { console.log('2-Start'); next(); ctx.body = { text: 'two' }; console.log('2-End'); } const three = (ctx, next) => { console.log('3-Start'); ctx.body = { text: 'three' }; next(); console.log('3-End'); } // 1-Start // 2-Start // 3-Start // 3-End // 2-End // 1-End // 而 ctx.body 最终为 { text: 'one' }
next()
没有调用 next()
// 没有调用 next() 函数 app.use((ctx, next) => { console.log('Start'); ctx.body = { text: 'test' }; console.log('End'); });
因为 next 函数本质上就是通过dispatch(i + 1)
来调用下一个中间件,如果没有调用 next 函数,就无法执行下一个中间件,那么就代表当前中间件流程执行结束。
多次调用 next()
app.use((ctx, next) => { console.log('Start'); ctx.body = { text: 'test' }; // 多次调用 next 函数 next(); // 本质上是 dispatch(i + 1) next(); // 本质上是 dispatch(i + 1) console.log('End'); });
这里假设 next
为 dispatch(3)
,那么 index
就为 2,第一次执行 next 函数时,会发生如下逻辑:
// index == 2 // i == 3 // 不会报错 if (i <= index) return Promise.reject(new Error('next() called multiple times')) // 赋值后 index 为 3 了 index = i
假设第三个中间件是最后一个中间件,那么执行完第一次 next 函数会立即执行第二个 next 函数,依然执行这个逻辑,但是 index 已经为 3 了,所以会导致报错:
// index == 3 // i == 3 // 报错 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i
来源:https://www.cnblogs.com/guolao/p/12290246.html