Koa做爲下一代Web開發框架,不只讓咱們體驗到了async/await語法帶來同步方式書寫異步代碼的酸爽,並且自己簡潔的特色,更加利於開發者結合業務自己進行擴展。node
本文從如下幾個方面解讀Koa源碼:git
利用NodeJS能夠很容易編寫一個簡單的應用程序:github
const http = require('http')
const server = http.createServer((req, res) => {
// 每一次請求處理的方法
console.log(req.url)
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello NodeJS')
})
server.listen(8080)
複製代碼
注意:當瀏覽器發送請求時,會附帶請求/favicon.ico。express
而Koa在封裝建立應用程序的方法中主要執行了如下流程:json
// application.js
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// 組織中間件
const fn = compose(this.middleware);
// 未監聽異常處理,則採用默認的異常處理方法
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
// 生成context上下文對象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 默認狀態碼爲404
res.statusCode = 404;
// 中間件執行完畢以後 採用默認的 錯誤 與 成功 的處理方式
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
首先咱們要知道NodeJS中的res和req是http.IncomingMessage和http.ServerResponse的實例,那麼就能夠在NodeJS中這樣擴展req和res:api
Object.defineProperties(http.IncomingMessage.prototype, {
query: {
get () {
return querystring.parse(url.parse(this.url).query)
}
}
})
Object.defineProperties(http.ServerResponse.prototype, {
json: {
value: function (obj) {
if (typeof obj === 'object') {
obj = JSON.stringify(obj)
}
this.end(obj)
}
}
})
複製代碼
而Koa中則是自定義request和response對象,而後保持對res和req的引用,最後經過getter和setter方法實現擴展。瀏覽器
// application.js
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; // 保存原生req對象
context.res = request.res = response.res = res; // 保存原生res對象
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// 最終返回完整的context上下文對象
return context;
}
複製代碼
因此在Koa中要區別這兩組對象:閉包
// request.js
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
複製代碼
此時已經能夠採用這樣的方式訪問header屬性:app
ctx.request.header
複製代碼
可是爲了方便開發者調用這些屬性和方法,Koa將response和request中的屬性和方法代理到context上。框架
經過Object.defineProperty能夠輕鬆的實現屬性的代理:
function access (proto, target, name) {
Object.defineProperty(proto, name, {
get () {
return target[name]
},
set (value) {
target[name] = value
}
})
}
access(context, request, 'header')
複製代碼
而對於方法的代理,則須要注意this的指向:
function method (proto, target, name) {
proto[name] = function () {
return target[name].apply(target, arguments)
}
}
複製代碼
上述就是屬性代理和方法代理的核心代碼,這基本算是一個經常使用的套路。
代理這部分詳細的源碼,能夠查看node-delegates, 不過這個包時間久遠,有一些老方法已經廢除。
在上述過程的源碼中涉及到不少JavaScript的基礎知識,例如:原型繼承、this的指向。對於基礎薄弱的同窗,還須要先弄懂這些基礎知識。
首先須要明確是:中間件並非NodeJS中的概念,它只是connect、express和koa框架衍生的概念。
在connect中,開發者能夠經過use方法註冊中間件:
function use(route, fn) {
var handle = fn;
var path = route;
// 不傳入route則默認爲'/',這種基本是框架處理參數的一種套路
if (typeof route !== 'string') {
handle = route;
path = '/';
}
...
// 存儲中間件
this.stack.push({ route: path, handle: handle });
// 以便鏈式調用
return this;
}
複製代碼
use方法內部獲取到中間件的路由信息(默認爲'/')和中間件的處理函數以後,構建成layer對象,而後將其存儲在一個隊列當中,也就是上述代碼中的stack。
connect中間件的執行流程主要由handle與call函數決定:
function handle(req, res, out) {
var index = 0;
var stack = this.stack;
...
function next(err) {
...
// 依次取出中間件
var layer = stack[index++]
// 終止條件
if (!layer) {
defer(done, err);
return;
}
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// 路由匹配規則
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
...
call(layer.handle, route, err, req, res, next);
}
next();
}
複製代碼
handle函數中使用閉包函數next來檢測layer是否與當前路由相匹配,匹配則執行該layer上的中間件函數,不然繼續檢查下一個layer。
這裏須要注意next中檢查路由的方式可能與想象中的不太同樣,因此默認路由爲'/'的中間件會在每一次請求處理中都執行。
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
try {
if (hasError && arity === 4) {
// 錯誤處理中間件
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// 請求處理中間件
handle(req, res, next);
return;
}
} catch (e) {
// 記錄錯誤
error = e;
}
// 將錯誤傳遞下去
next(error);
}
複製代碼
在經過call方法執行中間件方法的時候,採用try/catch捕獲錯誤,這裏有一個特別須要注意的地方是,call內部會根據是否存在錯誤以及中間件函數的參數決定是否執行錯誤處理中間件。而且一旦捕獲到錯誤,next方法會將錯誤傳遞下去,因此接下來普通的請求處理中間件即便經過了next中的路由匹配,仍然會被call方法給過濾掉。
下面是layer的處理流程圖:
上述就是connect中間件設計的核心要點,總結起來有以下幾點:
Koa中間件與connect中間件的設計有很大的差別:
固然,Koa中也是採用use方法註冊中間件,相比較connect省去路由匹配的處理,就顯得很簡潔:
use(fn) {
this.middleware.push(fn);
return this;
}
複製代碼
而且use支持鏈式調用。
Koa中間件的執行流程主要經過koa-compose中的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!')
}
/** * @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)
}
}
}
}
複製代碼
看到這裏本質上connect與koa實現中間件的思想都是遞歸,不難看出koa相比較connect實現得更加簡潔,主要緣由在於:
上述就是connect中間件與Koa中間件的實現原理,如今在再看Koa中間件的這張執行流程圖,應該沒有什麼疑問了吧?!
對於同步代碼,經過try/catch能夠輕鬆的捕獲異常,在connect中間件的異常捕獲則是經過try/catch完成。
對於異步代碼,try/catch則沒法捕獲,這時候通常能夠構造Promise鏈,在最後的catch方法中捕獲錯誤,Koa就是這樣處理,而且在catch方法中發送error事件,以便開發者自定義異常處理邏輯。
this.app.emit('error', err, this);
複製代碼
前面也談到Koa利用async/await語法帶來同步方式書寫異步代碼的酸爽,另外也讓錯誤處理更加天然:
// 也能夠這樣自定義錯誤處理
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
ctx.status = err.status || 500
ctx.body = err
}
})
複製代碼
相信看到這裏,再回憶一下以前遇到的那些問題,你應該會有新的理解,而且再次使用Koa時會更加駕輕就熟,這也是分析Koa源碼的目的之一。