上文咱們讀了koa源碼中的application模塊,瞭解其核心實現原理,其中在html
// application.js
module.exports = class Application extends Emitter{
...
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== 模塊,一樣利用刪減法。node
const proto = module.exports = {
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const statuses = require('statuses');
...
}
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
...
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
...
複製代碼
delegate 把 response 和 request 下面的方法和屬性都掛載到proto上,而後把它暴露給application,這裏的proto就是context。git
// delegator
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}
Delegator.prototype.method = function(name){
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
複製代碼
Delegator 函數傳入proto和target並分別緩存,而後調用method方法,把全部的方法名push到methods數組裏,同時對proto下每個傳入的方法名配置成一個函數,函數內部是具體目標對象的方法。詳細源碼請看node-delegatesgithub
// application.js
module.exports = class Application extends Emitter{
...
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;
}
}
複製代碼
Object.create 傳入了context暴露出的proto,proto做爲指定的原型對象在它的原有基礎上生成新的對象(context),同時request和response也利用Object.create建立一個新的對象並把它掛載到context上。這樣,在context不只能訪問到request response內部的方法屬性外還能訪問它們自身。編程
而後context,req,res互相掛載,這樣就能很便利的去訪問他們內部的方法和屬性。數組
Object.create 解釋看這裏Object.createpromise
總結: content.js 主要就是提供了對request和response對象的方法與屬性便捷訪問能力。緩存
// request.js
module.exports = {
get header() {...},
set header(val) {...},
get headers() {...},
set headers(val) {...},
get url() {...},
set url(val) {...},
get origin() {...},
get href() {...}
...
};
複製代碼
從代碼咱們能夠看到,request.js 封裝了請求相關的屬性以及方法,再把對象暴露給application,經過 application.js 中的createContext方法,代理對應的 request 對象。bash
具體源代碼看這裏 request.jscookie
和request.js同樣,封裝了響應相關的屬性以及方法,這裏就不貼代碼了。
具體源代碼看這裏 response.js
接下來咱們分析中間件,首先咱們要先理解什麼是中間件,先來看段代碼:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
ctx.type = 'text/html; charset=utf-8'
ctx.body = 'hello world'
})
app.listen(8081)
複製代碼
在 koa 中,要應用一箇中間件,咱們使用 app.use(),咱們要理解一個概念,就是在koa中,一切皆是中間件。再來一段代碼:
const Koa = require('koa')
const app = new Koa()
const mid1 = async(ctx, next) => {
ctx.body = 'Hello '
await next()
ctx.body = ctx.body + 'OK'
}
const mid2 = async(ctx, next) => {
ctx.type = 'text/html; charset=utf-8'
await next()
}
const mid3 = async(ctx, next) => {
ctx.body = ctx.body + 'World '
await next()
}
app.use(mid1)
app.use(mid2)
app.use(mid3)
app.listen(8085)
複製代碼
打印出==Hello World OK==,從執行結果來看,首先執行mid1中的代碼,在遇到await next()以後會把控制權交給下一個中間件處理,直到全部的中間件都執行完畢,而後再回來繼續執行剩下的業務代碼。到這裏咱們就對koa的中間件執行特色有所瞭解了。
// application
use(fn) {
...
this.middleware.push(fn);
return this;
}
複製代碼
在前面的代碼中,咱們看到中間件在使用過程當中會不斷加到堆棧中,執行順序也會按照先進先出的原則執行。可是koa中間件爲何能夠依次執行?並在執行過程當中能夠暫停下來走後面的流程而後再回來繼續執行?這裏咱們就要用到koa-compose了。
compose這裏用到了純函數,關於純函數能夠去看下函數式編程相關概念,首先純函數無反作用,既不依賴,也不會改變全局狀態。這樣函數之間能夠達到自由組合的效果。
咱們先用一段js代碼來模擬下這個執行原理
function tail(i) {
if(i > 3) return i
console.log('修改前', i);
return arguments.callee(i + 1)
}
tail(0)
// 修改前 0
// 修改前 1
// 修改前 2
// 修改前 3
複製代碼
經過這種方式在每次調用的時候把這個函數的執行返回,它執行後的結果就是下一次調用的入參,這個返回的函數負責執行下一個流程,一直執行到邊界條件爲止。
而後再看compose核心代碼
// koa-compose
module.exports = compose
function compose (middleware) { // 接收中間件函數數組
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') // 判斷入參middleware是否爲數組
for (const fn of middleware) { // 判斷數組內每一項是不是function
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) { // 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')) // 判斷next是否屢次調用
index = i
let fn = middleware[i] // 下表爲0,默認第一個中間件
if (i === middleware.length) fn = next // 說明已調用到最後一箇中間件,這裏next爲undified
if (!fn) return Promise.resolve() // next取反爲true,直接返回一個代碼執行完畢的resolve
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1) //遞歸調用,next將結果傳遞給下一個中間件
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
能夠看到compose是一個閉包函數,返回匿名函數再執行的最終結果返回的是一個promise對象。
compose內部存儲了全部的中間件,經過遞歸的方式不斷的運行中間件。
再回到application來看
// application.js
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 生成上下文對象
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
fnMiddleware 是經過 handleResponse 傳入下來的,而後在callback回調執行的時候生成上下文對象ctx,而後把ctx傳給了handleRequest,另外一個參數fn就是compose處理以後返回的匿名函數,對應就是compose裏return Promise.resolve(fn(context, function next (){}
這裏的context和next。
fnMiddleware第一次執行的時只傳入了ctx,next爲undified,對應的就是compose裏直接return dispatch(0)
,這時候尚未執行第一個中間件,在它內部才傳入了next。
compose的做用其實就是把每一個不相干的中間件串在一塊兒,而後來組合函數,把這些函數串聯起來依次執行,上一個函數的輸出結果就是下一個函數的入參。
Compose 是一種基於 Promise 的流程控制方式,能夠經過這種方式對異步流程同步化,解決以前的嵌套回調和 Promise 鏈式耦合。
--至此koa2的源碼學習就所有完成了--