最近使用koa2搭建博客,async/await異步流程控制確實比較優雅,可是在使用koa2過程當中也遇到很多的問題,如何編寫中間件,如何替換express中間件爲koa中間件,還有在實現服務端渲染的時候因爲koa對於response有本身的封裝,當時也花了不少時間去調bug。以爲以上問題不少也是歸根於本身對框架的不熟悉引發的,因此花了點時間閱讀了下koa源碼,分享下閱讀時收穫的東西以及koa2框架相關的分析。
javascript
下圖是我從node_modules目錄下截的,koa核心就在這兩部分,一個koa
自己,一箇中間件的合成流程控制koa-compose
vue
這裏先簡略介紹koa2的各部分吧,後面再講流程java
這個就是koa的入口主要文件,暴露應用的class
, 這個class繼承自node自帶的events
,這裏就能夠看出跟koa1.x很大的不一樣,koa2大量使用es6的語法,這裏就是一個例子,調用的時候就跟koa1.x有區別node
var koa = require('koa');
// koa 1.x
var app = koa();
// koa 2.x
// 使用class必須使用new來調用
var app = new koa();複製代碼
application就是應用,暴露了一些公用的api,好比兩個常見的,一個是listen
,一個是use
, listen就是調用http.createServer
,傳入callback,固然這個callback就是核心,它裏面包含了中間件的合併,上下文的處理,對res
的特殊處理(後面說流程會細說,這裏先粗略講講),use
的話用得就更多了,中間件每每是web框架的主要部分,可是use
其實就是很簡單起到收集中間件的做用而已,重點在於如何組合它們,如何設計請求到來時如何調用中間件,這些東西其實都在koa-compose
裏git
這部分就是koa的應用上下文ctx,其實就一個簡單的對象暴露,裏面的重點在delegate
,這個就是代理,這個就是爲了開發者方便而設計的,好比咱們要訪問ctx.repsponse.status
可是咱們經過delegate
,能夠直接訪問ctx.status
訪問到它(這個實現也不是很複雜,後面再講)es6
這兩部分就是對原生的res、req的一些操做了,大量使用es6的get
和set
的一些語法,去取headers
或者設置headers
、還有設置body
等等,這些就不詳細介紹了,有興趣的讀者能夠自行看源碼github
咱們就先從簡單的koa應用開始web
// 這裏就先不用async/await
// 它們並非必須的
var koa = require('koa');
var app = new koa();
app.use((ctx, next) => {
console.log(1)
next();
console.log(5)
});
app.use((ctx, next) => {
console.log(2)
next();
console.log(4)
});
app.use((ctx, next) => {
console.log(3)
ctx.body = 'Hello World';
});
app.listen(3000);
// 訪問http://localhost:3000
// 打印出一、二、三、四、5複製代碼
上述簡單的應用打印出一、二、三、四、5,這個其實就是koa中間件控制的核心,一個洋蔥結構,從上往下一層一層進來,再從下往上一層一層回去,乍一看很複雜,爲何不直接一層一層下來就結束呢,就像express/connect同樣,咱們就只要next就去下一個中間件,幹嗎還要回來?express
其實這就是爲了解決複雜應用中頻繁的回調而設計的級聯代碼,並不直接把控制權徹底交給下一個中間件,而是碰到next去下一個中間件,等下面都執行完了,還會執行next如下的內容api
解決頻繁的回調,這又有什麼依據呢?舉個簡單的例子,假如咱們須要知道穿過中間件的時間,咱們使用koa能夠輕鬆地寫出來,可是使用express呢,能夠去看下express reponse-time的源碼,它就只能經過監聽header被write out的時候而後觸發回調函數計算時間,可是koa徹底不用寫callback,咱們只須要在next後面加幾行代碼就解決了(直接使用.then()均可以)
// koa-guide v1的示例代碼就是計算中間件穿越時間
var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
// (1) 進入路由
var start = new Date;
yield next;
// (5) 再次進入 x-response-time 中間件,記錄2次經過此中間件「穿越」的時間
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
// (6) 返回 this.body
});
// logger
app.use(function *(next){
// (2) 進入 logger 中間件
var start = new Date;
yield next;
// (4) 再次進入 logger 中間件,記錄2次經過此中間件「穿越」的時間
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// response
app.use(function *(){
// (3) 進入 response 中間件,沒有捕獲到下一個符合條件的中間件,傳遞到 upstream
this.body = 'Hello World';
});
app.listen(3000);複製代碼
這裏你應該就對如何實現感興趣了,這裏目光就得轉到koa-compose
,
其實代碼就這麼點
const Promise = require('any-promise')
// 這裏使用any-promise是爲了兼容低版本node
module.exports = compose
function compose (middleware) {
// 傳入的middleware必須是一個數組
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 傳入的middleware的每個元素都必須是函數
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
let index = -1 // 這裏維護一個index的閉包
return dispatch(0) // 從數組的第一個元素開始`dispatch`
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() //這兩行就是來處理最後一箇中間件還有next的狀況的,實際上是能夠直接resolve出來的
try {
// 這裏就是傳入next執行中間件代碼了
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}複製代碼
我稍微註釋了一下代碼
其實這部分要跟application.js中的callback 結合起來看
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
}複製代碼
而callback的做用就是http.createServer(app.callback()).listen(...)
這裏開始講重點,koa-compose
從語義上看就是組合,其實就是對koa中間件的組合,它返回了一個promise,執行完成後就執行koa2對res的特殊處理,最後res.end()
固然咱們關心的是如何對中間件組合,其實就是傳入一個middleware數組
而後第一次取出數組的第一個元素,傳入context和next代碼,執行當前這個元素(這個中間件)
// 這裏就是傳入next執行中間件代碼了
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))複製代碼
其實後面根本沒用到resolve的內容,這部分代碼等價於
fn(context, function next () {
return dispatch(i + 1)
})
return Promise.resolve()複製代碼
核心就在於dispatch(i + 1)
,不過也很好理解嘛,就是將數組指針移向下一個,執行下一個中間件的代碼
而後一直這樣到最後一箇中間件,假如最後一箇中間件還有next那麼下面這兩段代碼就起做用了
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()複製代碼
由於middleware沒有下一個了,而且其實外面那個next是空的,因此其實就能夠return結束了
這裏其實直接return就好了,這裏的return Promise.resolve()實際上是沒有用的,真正return出外面的是調用第一個中間件的resolve
嗯嗯,這樣其實就結束了,一整套的中間件調用
讀者可能還想問,不是洋蔥結構嗎?那怎麼回去的呢?其實回去的代碼其實就是函數壓棧和出棧
看完如下代碼你就懂了(其實這就是koa的中間件原理)
function a() {
console.log(1)
b();
console.log(5)
return Promise.resolve();
}
function b() {
console.log(2)
c();
console.log(4)
}
function c() {
console.log(3)
return;
}
a();
// 輸出一、二、三、四、5複製代碼
一圖勝千言
首先就是咱們的app.js代碼,初始的時候就是咱們new了個koa實例,而後開始寫各類use
,寫個app.listen(3000);
use其實就是把你寫的函數一個一個收集到一個middleware數組,listen
的話就是http.createServer(app.callback()).listen(...)
當一個請求過來的時候,由http.createServer
的callback知道,它是能夠傳入req、res的,因此其實從這個入口能夠拿到req、res,koa拿到後就createContext
建立應用上下文,根據context.js、request.js、response.js建立,而且進行屬性代理delegate
請求到來執行了中間件的一系列流程,使用koa-compose
將傳入的middleware組合起來,而後返回了一個promise, 其實真正傳入http.createServer callback的就下面(我簡寫了)
http.createServer((req, res) => {
// ... 經過req,res建立上下文
// fn是`koa-compose`返回的promise
return fn(ctx).then(handleResponse).catch(onerror);
})複製代碼
咱們上一部分能夠看到一個handleResponse
,它是什麼?其實咱們到這裏尚未res.end()
, koa前面其實都是使用ctx.body = xxx
,那它是怎麼write回res的呢,這部分邏輯就在function respond(){},handleResponse
就如下一句
const handleResponse = () => respond(ctx);複製代碼
respond到底作了什麼呢,其實它就是判斷你以前中間件寫的body的類型,作一些處理,而後使用res.end(body)
到這裏就結束了,返回了頁面
讀者到這裏能夠再看看那張圖就比較清晰了
在源碼閱讀時大量看到Object.create()的用法,這個又跟new X()
有什麼區別呢
引用下stackoverflow答案
stackoverflow.com/questions/4…
new Test():
1.create new Object()
obj
2.set obj.__proto__
to Test.prototype
3.return Test.call(obj) || obj
;// normally obj is returned but constructors in JS can return a value
Object.create( Test.prototype )
1.create new Object()
obj
2.set obj.__proto__
to Test.prototype
3.return obj;
能夠看到其實new和Object.create前兩個步驟都是同樣,區別在第三步,其實就是Object.create
不會執行構造函數,咱們來兩段更直接的代碼
var a = new A();
var b = Object.create(B.prototype)
function A() {
console.log('a')
}
function B() {
console.log('b')
}
// 咱們能夠看到只輸出了a,可是b並無輸出複製代碼
var a = new A();
var b = Object.create(B.prototype)
function A() {
return {}
}
function B() {
return {}
}
console.log(a)
console.log(b)
// 能夠看到a就是{}
// b是一個對象,它的__proto__指向B.prototype複製代碼
這樣應該就很清晰了,Object.create()特殊還在於它的第二個參數,就像Object.defineProperties()
能夠定義新的屬性或修改現有屬性
koa2裏有一堆屬性代理,爲了方便開發者更容易訪問到一些屬性,koa設計了一些屬性代理,能夠用ctx.body
之類的去訪問ctx.response.body
,調用也很簡單,諸如如下
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove'
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');複製代碼
其實這個實現很簡單
咱們簡單來講下method代理,其餘同理
這裏咱們用obj.foo來替代obj.request.foo
function method(proto, target, name) {
proto[name] = function() {
return proto[target][name].apply(proto[target], arguments)
}
}
var obj = {};
obj.request = {
foo: function(bar) {
console.log(bar)
console.log(this)
return bar;
}
}
method(obj, 'request', 'foo')
obj.foo('123')
// 輸出123
// this輸出obj.request複製代碼
謝謝閱讀~
歡迎follow我哈哈github.com/BUPT-HJM
歡迎繼續觀光個人新博客~(老博客近期可能遷移)
個人博客也是關於koa2的一個實踐,歡迎star😸
github.com/BUPT-HJM/vu…
歡迎關注