最近讀了一下Koa2的源碼;在閱讀Koa2 (2.3.0) 的源碼的過程當中,個人感覺是整個代碼設計精巧,思路清晰,是一個小而精的 nodejs web服務框架。html
做爲web服務框架,都是要圍繞核心服務而展開的。那什麼是核心服務呢?其實就是接收客戶端的一個http的請求,對於這個請求,除了接收之外,還有解析這個請求。因此說會有node
HPPT:接收 -> 解析 -> 響應web
在響應客戶端的時候,也有不少種方式,好比返回一個html頁面,或者json文本。在解析請求和響應請求的中間,會有一些第三方的中間件,好比 日誌、表單解析等等來加強 koa 的服務能力,因此 koa 至少要提供 "請求解析"、"響應數據"、"中間件處理" 這三種核心能力的封裝,同時還須要有一個串聯他們執行環境的上下文(context)json
上下文能夠理解爲是http的請求週期內的做用域環境來託管請求響應和中間件,方便他們之間互相訪問。後端
以上分析是站在單個http請求的角度來看一個web服務能力。那麼站在整個網站,站在整個後端服務的角度來看的話,可以提供 "請求"、"響應"、"解析"、"中間件"、"http流程全鏈路" 這些服務能力的綜合體,能夠看作是一個應用服務對象。若是把這些全放到 koa 裏的話,那麼對應的就是:api
首先看下koa的目錄結構數組
// application.js
const isGeneratorFunction = require('is-generator-function'); // 判斷當前傳入的function是不是標準的generator function
const debug = require('debug')('koa:application'); // js調試工具
const onFinished = require('on-finished'); // 事件監聽,當http請求關閉,完成或者出錯的時候調用註冊好的回調
const response = require('./response'); // 響應請求
const compose = require('koa-compose'); // 中間件的函數數組
const isJSON = require('koa-is-json'); // 判斷是否爲json數據
const context = require('./context'); // 運行服務上下文
const request = require('./request'); // 客戶端的請求
const statuses = require('statuses'); // 請求狀態碼
const Cookies = require('cookies');
const accepts = require('accepts'); // 約定可被服務端接收的數據,主要是協議和資源的控制
const Emitter = require('events'); // 事件循環
const assert = require('assert'); // 斷言
const Stream = require('stream');
const http = require('http');
const only = require('only'); // 白名單選擇
const convert = require('koa-convert'); // 兼容舊版本koa中間件
const deprecate = require('depd')('koa'); // 判斷當前在運行koa的某些接口或者方法是否過時,若是過時,會給出一個升級的提示
複製代碼
以上是koa入口文件的依賴分析。接下來咱們進行源碼分析,首先咱們利用刪減法來篩出代碼的核心實現便可,不用上來就盯細節! 咱們只保留constructorbash
// application.js
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false; // 是否信任 proxy header 參數,默認爲 false
this.middleware = []; //保存經過app.use(middleware)註冊的中間件
this.subdomainOffset = 2; // 子域默認偏移量,默認爲 2
this.env = process.env.NODE_ENV || 'development'; // 環境參數,默認爲 NODE_ENV 或 ‘development’
this.context = Object.create(context); //context模塊,經過context.js建立
this.request = Object.create(request); //request模塊,經過request.js建立
this.response = Object.create(response); //response模塊,經過response.js建立
}
// ...
}
複製代碼
咱們能夠看到,這段代碼暴露出一個類,構造函數內預先聲明瞭一些屬性,該類繼承了Emitter,也就是說這個類能夠直接爲自定義事件註冊回調函數和觸發事件,同時能夠捕捉到其餘地方觸發的事件。cookie
除了這些基本屬性以外,還有一些公用的api,最重要的兩個一個是==listen==,一個是==use==。koa的每一個實例上都會有這些屬性和方法。app
// application.js
module.exports = class Application extends Emitter {
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);
}
listen() {
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
use(fn) {
this.middleware.push(fn);
return this;
}
}
複製代碼
listen 方法內部經過 http.createServer 建立了一個http服務的實例,經過這個實例去 listen 要監聽的端口號,http.createServer 的參數傳入了 this.callback 回調
// application.js
module.exports = class Application extends Emitter {
...
callback() {
const fn = compose(this.middleware); // 把全部middleware進行了組合,使用了koa-compose
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); // 返回了自己的回調函數
};
return handleRequest;
}
}
複製代碼
能夠看到,handleRequest 返回了自己的回調,接下來看 handleRequest 。
handleRequest 方法直接做爲監聽成功的調用方法。已經拿到了 包含 req res 的 ctx 和能夠執行全部中間件函數的 fn。 首先一進來默認設置狀態碼爲==404== . 而後分別聲明瞭 成功函數執行完成之後的成功 失敗回調方法。這兩個方法實際上就是再將 ctx 分化成 req res。 分別調這兩個對象去客戶端執行內容返回。 ==context.js request.js response.js== 分別是封裝了一些對 ctx req res 操做相關的屬性,咱們之後再說。
// application.js
module.exports = class Application extends Emitter {
...
handleRequest(ctx, fnMiddleware) {
const res = ctx.res; // 拿到context.res
res.statusCode = 404; // 設置默認狀態嗎404
const onerror = err => ctx.onerror(err); // 設置onerror觸發事件
const handleResponse = () => respond(ctx); // 向客戶端返回數據
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
複製代碼
失敗執行的回調
onerror(err) {
assert(err instanceof Error, `non-error thrown: ${err}`);
if (404 == err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
複製代碼
成功執行的回調
function respond(ctx) {
...
}
複製代碼
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
咱們拆分理解,首先 return fnMiddleware(ctx) 返回了一箇中間件數組處理鏈路,then(handleResponse) 等到整個中間件數組所有完成以後把返回結果經過 then 傳遞到 handleResponse。
// 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;
}
}
複製代碼
這裏咱們不用去太深刻去摳代碼,理解原理就行。createContext 建立 context 的時候,還會將 req 和 res 分別掛載到context 對象上,並對req 上一些關鍵的屬性進行處理和簡化 掛載到該對象自己,簡化了對這些屬性的調用。咱們經過一張圖來直觀地看到全部這些對象之間的關係。
createContext 簡單理解就是掛載上面的對象,方便整個上下游http能及時訪問到進出請求及特定的行爲。
// application.js
module.exports = class Application extends Emitter {
...
}
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
const res = ctx.res;
if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status; // 賦值服務狀態碼
if ('HEAD' == ctx.method) { // 請求頭方法判斷
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
// status body
if (null == body) {
body = ctx.message || String(code);
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// 經過判斷body類型來調用,這裏的res.end就是最終向客戶端返回數據的動做
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// 返回爲json數據
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
複製代碼
respond 函數是 handleRequest 成功處理的回調,內部作了合理性校驗,諸如狀態碼,內容的類型判斷,最後向客戶端返回數據。
以上就是咱們對application.js文件的分析,經過上面的分析,咱們已經能夠大概得知Koa處理請求的過程:當請求到來的時候,會經過 req 和 res 來建立一個 context (ctx) ,而後執行中間件。