最近在復習node的基礎知識,於是看了看koa2的源碼,寫此文分享一下包括了Koa2的使用、中間件及上下文對象的大致實現原理。
koa的github地址:https://github.com/koajs/koa.git
Koa2的安裝和簡單使用
需要 nodev7.6.0 或者更高的版本,為了支持 ES2015 and async
安裝
npm install koa
Hello koa
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
中文的api文檔:https://github.com/guo-yu/koa-guide
簡單分析koa的代碼
打開koa的源碼,核心文件共四個在lib目錄下,application.js,context.js,request.js,response.js
application.js
app的入口文件,就是一個構造函數
簡潔的代碼
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();
}
};
開始的流程:
const app = new Koa();
然后通過 listen來啟動服務:
const server = http.createServer(this.callback());
server.listen(...args);
看一下原生的啟動方法:
// 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那個方法:
//處理中間件的使用,后面詳細說明
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;
啟動服務:
server.listen(...args);
到此服務就起來了。在來看看中間件的使用原理:
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等。