最近在複習node的基礎知識,因而看了看koa2的源碼,寫此文分享一下包括了Koa2的使用、中間件及上下文對象的大體實現原理。node
須要 nodev7.6.0 或者更高的版本,爲了支持 ES2015 and asyncgit
npm install koa
const Koa = require('koa'); const app = new Koa(); // response app.use(ctx => { ctx.body = 'Hello Koa'; }); app.listen(3000);
打開koa的源碼,核心文件共四個在lib目錄下,application.js,context.js,request.js,response.jsgithub
app的入口文件,就是一個構造函數npm
簡潔的代碼 module.exports = class Application extends Emitter { 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); } //listen端口方法 listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } toJSON() { return only(this, [ 'subdomainOffset', 'proxy', 'env' ]); } inspect() { return this.toJSON(); } //中間件使用的use方法 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; } //上下文等關鍵代碼 callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest; } //建立上下文 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.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); request.ip = request.ips[0] || req.socket.remoteAddress || ''; context.accept = request.accept = accepts(req); context.state = {}; return context; } //處理報錯 onerror(err) { assert(err instanceof Error, `non-error thrown: ${err}`); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, ' ')); console.error(); } };
開始的流程:api
const app = new Koa();
而後經過 listen來啓動服務:promise
const server = http.createServer(this.callback()); server.listen(...args);
看一下原生的啓動方法:cookie
// http server 例子 var server = http.createServer(function(serverReq, serverRes){ var url = serverReq.url; serverRes.end( '您訪問的地址是:' + url ); }); server.listen(3000);
對比發現this.callback()就是用來建立上下文和處理req和res的,接着看this.callback那個方法:app
//處理中間件的使用,後面詳細說明 const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; // 建立上下文 const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); //中間件返回promise對象,成功執行handleResponese,錯誤用onerror處理, return fn(ctx).then(handleResponse).catch(onerror); }; 返回callback函數 return handleRequest;
啓動服務:dom
server.listen(...args);
到此服務就起來了。在來看看中間件的使用原理:koa
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 || '-'); //使用把中間件推送到middleware中保存 this.middleware.push(fn); //返回this,爲了連續調用 return this; }
保存到this.middleware,在this.callback進程了處理:
const fn = compose(this.middleware);
看一下compose是怎麼處理middleware,代碼在const compose = require('koa-compose');
'use strict' module.exports = compose 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) { // 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) } } } }
經過上面巧妙的遞歸調用,執行完全部的中間件函數,返回繼續啓動流程,建立上下文,處理res,req等。