koa原始碼學習筆記
我們的koa程式可以簡化為以下三步
const app = new koa()
app.use(middleware)
app.listen(port)
搞清楚這三步分別做了什麼?讓我們翻開koa原始碼一看究竟.開啟node_modules/koa/package.json,可以看到程式的入口
"main": "lib/application.js"
application.js主要做了三件事
- 建立app物件,該物件繼承了Emitter,並且繼承了context,req,res物件
module.exports = class Application extends Emitter { constructor() { super(); this.middleware = []; //中介軟體陣列 this.context = Object.create(context); //context掛載到app this.request = Object.create(request); //request掛載到app this.response = Object.create(response); //response掛載到app } }
分別來看看這lib下的context.js, request.js, response.js三個檔案
// context.js 就暴露了一個物件 const proto = module.exports = { }; // 使用delegate將request和response物件掛載到proto上 delegate(proto, 'response') // 這裡 .method('attachment') .method('flushHeaders') ... 省略若干 ..... .access('status') .getter('writable'); delegate(proto, 'request') // 這裡 .method('acceptsLanguages') .method('acceptsEncodings') ... 省略若干 ..... .access('querystring') .getter('ip');
delegate來自於node_modules/delegates/index.js,也很簡單,就定義了5個方法method, access, getter, setter, fluent
function Delegator(proto, target) { this.proto = proto; this.target = target; } // method, 通過apply將this繫結在target,也就是上面的request和response物件上 Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; };
request.js和response.js:裡面都是一系列的getter setter方法, 繼承自req和res物件
//request.js
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val;
},
... 省略若干 .....
}
因此有
ctx.body = "Hi Allen" //實際上呼叫的是response.js裡面的set body
- use的時候將回調函式push到middlewares中介軟體陣列中
use(fn) {
this.middleware.push(fn);
return this;
}
- listen的時候,建立http server,監聽埠,當埠發生變化時候執行相應的回撥,
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
我們來看下this.callback,該函式執行的時候返回handleRequest,handleRequest首先建立了呼叫了createContext,將req和res上的方法屬性掛在了ctx上, 其次呼叫handleRequest將ctx物件傳給中介軟體函式,
callback() {
const fn = compose(this.middleware); //注意這裡
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
這裡的this.context來自於context.js檔案,context.js用引入delegate.js,因此,原型鏈查詢的方向是context->context.js暴露出來的物件->delegate.js暴露出來的物件, 可以看到context可以直接訪問request和response上的屬性,也可以通過context.request來訪問, (至於為什麼要有request.response, response.request這一步有些疑惑,還望高人指出)
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.state = {};
return context;
}
handleResponse在中介軟體執行結束之後再執行
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
去除掉引數校驗之後,response返回客戶端的需要的資料
function respond(ctx) {
const res = ctx.res;
let body = ctx.body;
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
中介軟體的執行順序問題: 按use的順序執行,碰到next()就傳遞控制權,next執行完之後再返回執行
const koa = require('koa')
const logger = require('koa-logger')
const app = new koa();
const indent = (n)=> {
return new Array(n).join(' ')
}
const mid1 = () => {
return async (ctx, next) => {
ctx.body = `<h3>請求 => 第一層中介軟體</h3>`
await next()
ctx.body += `<h3>響應 => 第一層中介軟體</h3>`
}
}
const mid2 = () => {
return async (ctx, next) => {
ctx.body += `<h3>${indent(4) }請求 => 第二層中介軟體</h3>`
await next()
ctx.body += `<h3>${ indent(4)} 響應 => 第二層中介軟體</h3>`
}
}
const mid3 = () => {
return async (ctx, next) => {
ctx.body += `<h3>${ indent(8)}請求 => 第三層中介軟體</h3>`
await next()
ctx.body += `<h3>${ indent(8)} 響應 => 第三層中介軟體</h3>`
}
}
app.use(logger())
app.use(mid1())
app.use(mid2())
app.use(mid3())
app.use((ctx,next) => {
ctx.body += `<p style='color: red'>${indent(12)}koa 核心業務處理'</p>`
})
app.listen(3001)
返回的結果是這樣的,類似一種"U型結構"
請求 => 第一層中介軟體
請求 => 第二層中介軟體
請求 => 第三層中介軟體
koa 核心業務處理'
響應 => 第三層中介軟體
響應 => 第二層中介軟體
響應 => 第一層中介軟體
koa中的中介軟體之所以有這樣的能力,在於koa-compose的包裝.koa-compose使用的遞迴的形式進行呼叫,並且使用尾遞迴進行了優化.翻開koa-compose/index.js的原始碼
function compose (middleware) {
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
掌握koa等框架並不難,難的是http協議,資源,請求流程的優化設定,這些屬於網路通訊的硬知識,需要花時間去學習掌握