前言
這一篇應該就是這個系列的最後一篇了。之後的文章裏我會分享node的其他內容,作爲node的入門文章來說我覺得這幾個足以了。不放前置文章了,大家想看的自己往前翻就是了。
koa
前面寫了那麼些,我們需要明確的一點是:koa這個框架到底做了些什麼事情?
爲了說明這個問題,讓我們再次比較一下,原始node和koa是怎麼寫http的
// node
var http = require('http');
http.createServer((request, response)=> {response.end('Hello World\n');}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
//koa
const Koa = require('koa');
const app = new Koa();
app.use((context,next)=>{context.body='hello world';);
app.listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
我的理解是:1、他將中間件組織了起來以便程序對他們進行依次調用,2、他將請求(request)和響應(response)封裝成了一個上下文(context),使context可以使用request和response的方法和屬性。
分析
我們直接從源碼的角度進行分析。
在一個目錄下cmd輸入 npm install --save koa ,就會下載koa的相關包,這時候查看node_modules中,koa的源代碼只有四個:koa、koa-compose、koa-convert、koa-is-json
其中koa-is-json只有這麼一點代碼 忽略掉
function isJSON(body) {
if (!body) return false;
if ('string' == typeof body) return false;
if ('function' == typeof body.pipe) return false;
if (Buffer.isBuffer(body)) return false;
return true;
}
我們首先來看看koa是如何組織中間件的。我提前說一下結論,首先先在koa包中的app類中編寫一個方法use()將中間件添加到數組中,然後使用koa-compose包的中的函數將該數組中的函數組織成中間件。
//koa包 lib/application.js
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/tree/v2.x#old-signature-middleware-v1x---deprecated');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
我們知道,在node中,函數是作爲第一等公民的,所以函數是可以作爲數組中的一個成員的。當我們編寫了函數function func1(ctx,next){}並使用app.use(func1)的時候,實際上就是將func1這個函數放入了一個數組中。
接下來,你可以繼續添加,當你一旦使用app.listen(port)對端口進行監聽的時候,這時候koa就會使用koa-compose對數組中的函數進行組織。
// koa-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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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)
}
}
}
}
returne返回的這個函數有些難以看懂,其實他就是用promise寫一個處理邏輯,遞歸調用dispatch,按照middleware數組的順序往下一層一層地調用next來執行中間件(回調函數);
可以用async來改寫。
function compose(middleware) {
return dispatch(0);
async function dispatch(i) {
let fn = middleware[i];
try {
await fn(dispatch.bind(null, i + 1));
} catch (err) {
return err;
}
}
}
可以看出在調用邏輯上中間件和async調用沒有什麼本質上的差別。通過這種形式,我們就將順序操作組織爲層級操作。
再來看看koa對request和response的封裝。
在node_modules中打開koa包,可以看到他有四個文件(application.js、context.js、request.js、response.js),主要來看看application的實現。
首先在new了一個Koa實例出來後,application(app)構造函數
// application.js
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);
}
並沒有做什麼實質性的工作,只是根據另外三個文件創建了三個對象。
接下來,app.listen(端口號)。
執行了這一步,koa就會把服務器實例正式運行起來(包括剛纔對中間件的組織),具體代碼如下
// application.js
listen() {
const server = http.createServer(this.callback()); //這個http.createServer就是上文node的那種創建方式
return server.listen.apply(server, arguments); //這裏是用js的語法更改一下this的指向和傳入參數並執行
}
callback() {
const fn = compose(this.middleware); // 這個函數調用的就是上文所說的中間件組織
if (!this.listeners('error').length) this.on('error', this.onerror);
return (req, res) => { //返回一個參數爲request和response的函數給http.createServer()
res.statusCode = 404;
const ctx = this.createContext(req, res); //將request和response封裝成一個context
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror); // 依次執行中間件
};
}
createContext(req, res) {
const context = Object.create(this.context); //每次傳入來一個請求,都會複製出來一個新的context對象、request和response對象,讓他們擁有指向彼此的指針
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;
}
由於http模塊的作用,每次server在收到一個有效request請求之後,會產生一個request對象和response對象(上一篇講到的,不記得的可以回去看),這時候koa層面就會把這個request和response做一個合併的處理,讓他們都在ctx(context)對象中進行操作。
從代碼裏面可以看出,ctx裏保存着res(response)和req(request)對象的引用,而res對其他兩個也是如此。其實我個人認爲這樣只是單純有利於調試,對於代碼的組織來說似乎沒什麼作用。我們在java中也習慣了httpRequest處理請求,httpResponse返回消息的操作。
此外值得注意的一點是koa包中context.js文件
// context.js
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
...
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
...
這裏只需要知道他是使用了delegate委託的方法,將request和response中的方法代理到context中去,這就夠了。