原文連接javascript
koa 是由 Express 原班人馬打造的,相比 Express 的大而全,koa 致力於成爲一個更小、更富有表現力、更健壯的 Web 框架,適合做爲 web 服務框架的基石。java
koa1 經過組合不一樣的 generator,能夠避免嵌套地獄,並極大地提高錯誤處理的效率。koa2 使用了最新的 async await
generator 語法糖,使得開發更高效。node
koa 不在內核方法中綁定任何中間件,但確很輕易集成中間件,只須要 use 方法傳入一箇中間件函數,就能方便獲取請求響應等上下文信息和下一個中間件,使得中間件的使用井井有理。git
koa 源碼在 lib 文件下四個文件中,接下來一一介紹每一個模塊文件的內容。github
lib/
├── application.js
├── context.js
├── request.js
└── response.js
複製代碼
use()
添加訂閱中間件,內部使用一個數組維護中間件;listen()
node http 起一個服務;callback()
返回一個 http 服務回調函數 cb。
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
複製代碼
實際上是如下的語法糖web
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
複製代碼
const Koa = require('koa');
const app = new Koa();
// logger 中間件
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
logger 中間件 await next()
時會暫停下面代碼的執行,直到 response 中間件執行完畢。npm
注意到 response 沒有執行 next,此時已沒有下一個中間件,但即便執行也不會報錯,由於內部處理爲一個 Promise.resolve 的 promise。數組
注意在一箇中間件中屢次(2次及以上)執行 next() 會報錯。promise
若是 logger 中間件不執行 next,那麼 response 中間件不會被執行。也即 ctx.body
不會執行,application 中的 handleRequest 默認設置node http res.statusCode = 404,npm statuses 中維護了經常使用的 code 碼文本提示音,例如 404: Not Found
。瀏覽器
ctx.body
實際上是調用了 koa response 對象的 body set 方法,賦值給 _body 屬性而且根據值設置 http 狀態碼。最後是在中間件 resolve 後調用 application 中的私有 respond 函數,執行了 node http res.end()。
const http = require('http');
const Emitter = require('events');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Koa extends Emitter {
constructor() {
super();
}
listen() {}
use() {}
callback() {}
handleRequest() {}
createContext() {}
}
module.exports = Koa;
複製代碼
let proto = {};
module.exports = proto;
複製代碼
const request = {};
module.exports = request;
複製代碼
const response = {};
module.exports = response;
複製代碼
constructor() {
super();
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
this.fn = null;
}
複製代碼
簡單提一下爲何要使用 Object.create
,例如避免改動 this.context.x
而影響 context.x
(除非你 this.context.__proto__.x
,顯然沒人會刻意這麼去作)。
if(!Object.create) {
Object.create = function(proto) {
function F(){}
F.prototype = proto;
return new F;
}
}
複製代碼
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
use(fn) {
this.fn = fn;
return this;
}
複製代碼
callback() {
return (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx);
};
}
複製代碼
handleRequest(ctx) {
this.fn(ctx);
ctx.res.end(ctx.body);
}
複製代碼
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;
return context;
}
複製代碼
const parse = require('parseurl');
const request = {
get url() {
return this.req.url;
},
get path() {
return parse(this.req).pathname;
},
get query() {
return parse(this.req).query;
}
};
複製代碼
const response = {
get body() {
return this._body;
},
set body(val) {
this.res.statusCode = 200;
this._body = val;
}
};
複製代碼
const Koa = require('./application');
const app = new Koa();
app.use(ctx => {
console.log(ctx.req.url);
console.log(ctx.request.req.url);
console.log(ctx.response.req.url);
console.log(ctx.request.url);
console.log(ctx.request.path);
console.log(ctx.request.query);
console.log(ctx.url);
console.log(ctx.path);
console.log(ctx.query);
ctx.body = 'hello world';
});
app.listen(3000);
複製代碼
localhost:3000/path?x=1&y=2
,console 輸出/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
undefined
undefined
undefined
複製代碼
能夠看出,可使用 koa context、request、response 來訪問 node req 的屬性,也能夠直接訪問 request 對象上定義的方法。
建議是避免操做 node http 的 req 或 res。
衆所周知,koa 是支持 context 實例代理訪問 koa request、response 上的方法的。
koa 使用了 __defineSetter__
和 __defineGetter__
來實現,提示這兩個方法已被標準廢棄,這裏使用 Object.defineProperty
來實現。
注意 Object.defineProperty
只設置 get 方法 enumerable
和 configurable
默認都是 false
。
function defineGetter(prop, name) {
Object.defineProperty(proto, name, {
get() {
return this[prop][name];
},
enumerable: true,
configurable: true,
});
}
function defineSetter(prop, name) {
Object.defineProperty(proto, name, {
set(val) {
this[prop][name] = val;
},
enumerable: true,
configurable: true,
});
}
defineGetter('request', 'url');
defineGetter('request', 'path');
defineGetter('request', 'query');
defineGetter('response', 'body');
defineSetter('response', 'body');
複製代碼
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
複製代碼
ctx.body = 'hello world'
也不是新添加屬性,而是訪問 response 上的 body set 方法。
constructor() {
- this.fn = null;
+ this.middleware = [];
}
use(fn) {
- this.fn = fn;
+ this.middleware.push(fn);
}
複製代碼
function compose(middleware) {
return function (context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
if(i <= index) throw new Error('next() 在中間件中被調用2次以上');
index = i;
let fn = middleware[i];
if(i === middleware.length) fn = next;
if(!fn) return;
return fn(context, dispatch.bind(null, i + 1));
}
}
}
複製代碼
callback() {
+ const fn = compose(this.middleware);
return (req, res) => {
const ctx = this.createContext(req, res);
- return this.handleRequest(ctx);
+ return this.handleRequest(ctx, fn);
};
}
- handleRequest(ctx) {
+ handleRequest(ctx, fnMiddleware) {
- this.fn(ctx);
+ fnMiddleware(ctx);
ctx.res.statusCode = 200;
ctx.res.end(ctx.body);
}
複製代碼
app.use((ctx, next) => {
console.log(ctx.url);
next();
});
app.use((ctx, next) => {
ctx.body = 'hello world';
next();
});
複製代碼
function compose(middleware) {
return function (context, next) {
let index = -1;
return dispatch(0);
function dispatch(i) {
- if(i <= index) throw new Error('next() 在中間件中被調用2次以上');
+ if(i <= index) return Promise.reject(new Error('next() 在中間件中被調用2次以上'));
index = i;
let fn = middleware[i];
if(i === middleware.length) fn = next;
- if(!fn) return;
+ if(!fn) return Promise.resolve();
- return fn(context, dispatch.bind(null, i + 1));
+ try {
+ return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
+ } catch (err) {
+ return Promise.reject(err);
+ }
}
}
}
複製代碼
handleRequest(ctx, fnMiddleware) {
- fnMiddleware(ctx);
- ctx.res.statusCode = 200;
- ctx.res.end(ctx.body);
+ fnMiddleware(ctx).then(() => {
+ ctx.res.statusCode = 200;
+ ctx.res.end(ctx.body);
+ });
}
複製代碼
app.use(async (ctx, next) => {
await new Promise(resolve => {
setTimeout(() => {
console.log(ctx.url);
resolve();
}, 500);
});
next();
});
app.use((ctx, next) => {
ctx.body = 'hello world';
next();
});
複製代碼
這樣一個簡單的 koa 的主要功能就實現了,行文爲了簡單,不少錯誤處理等細節都忽略了,這在正式的產品中是大忌,但願當心謹慎。