在前文我們介紹過什麼是 Koa2 的基礎
簡單回顧下
什麼是 koa2
- NodeJS 的 web 開發框架
- Koa 可被視為 nodejs 的 HTTP 模組的抽象
原始碼重點
中介軟體機制
洋蔥模型
compose
原始碼結構
Koa2 的原始碼地址:https://github.com/koajs/koa
其中 lib 為其原始碼
可以看出,只有四個檔案:application.js
、context.js
、request.js
、response.js
application
為入口檔案,它繼承了 Emitter 模組,Emitter 模組是 NodeJS 原生的模組,簡單來說,Emitter 模組能實現事件監聽和事件觸發能力
刪掉註釋,從整理看 Application
建構函式
Application 在其原型上提供了 listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror 等八個方法,其中
- listen:提供 HTTP 服務
- use:中介軟體掛載
- callback:獲取 http server 所需要的 callback 函式
- handleRequest:處理請求體
- createContext:構造 ctx,合併 node 的 req、res,構造 Koa 的 引數——ctx
- onerror:錯誤處理
其他的先不要在意,我們再來看看 構造器 constructor
暈,這都啥和啥,我們啟動一個最簡單的服務,看看例項
const Koa = require('Koa')
const app = new Koa()
app.use((ctx) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('3000請求成功')
})
console.dir(app)
能看出來,我們的例項和構造器一一對應,
打斷點看原型
哦了,除去非關鍵欄位,我們只關注重點
Koa 的構造器上的 this.middleware、 this.context、 this.request、this.response
原型上有:listen、use、callback、handleRequest、createContext、onerror
注:以下程式碼都是刪除異常和非關鍵程式碼
先看 listen
...
listen(...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}
...
可以看出 listen 就是用 http 模組封裝了一個 http 服務,重點是傳入的 this.callback()
。好,我們現在就去看 callback 方法
callback
callback() {
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
它包含了中介軟體的合併,上下文的處理,以及 res 的特殊處理
中介軟體的合併
使用了 koa-compose
來合併中介軟體,這也是洋蔥模型的關鍵,koa-compose 的原始碼地址:https://github.com/koajs/compose。這程式碼已經三年沒動了,穩的一逼
function compose(middleware) {
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)
}
}
}
}
一晃眼是看不明白的,我們需要先明白 middleware 是什麼,即中介軟體陣列,那它是怎麼來的呢,構造器中有 this.middleware,誰使用到了—— use 方法
我們先跳出去先看 use 方法
use
use(fn) {
this.middleware.push(fn)
return this
}
除去異常處理,關鍵是這兩步,this.middleware
是一個陣列,第一步往 this.middleware
中 push 中介軟體;第二步返回 this 讓其可以鏈式呼叫,當初本人被面試如何做 promise 的鏈式呼叫,懵逼臉,沒想到在這裡看到了
回過頭來看 koa-compose 原始碼,設想一下這種場景
...
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
...
我們知道 它的執行是 123456
它的 this.middleware 的構成是
this.middleware = [
async (ctx, next) => {
console.log(1)
await next()
console.log(6)
},
async (ctx, next) => {
console.log(2)
await next()
console.log(5)
},
async (ctx, next) => {
console.log(3)
ctx.body = 'hello world'
console.log(4)
},
]
不要感到奇怪,函式也是物件之一,是物件就可以傳值
const fn = compose(this.middleware)
我們將其 JavaScript 化,其他不用改,只需要把最後一個函式改成
async (ctx, next) => {
console.log(3);
-ctx.body = 'hello world';
+console.log('hello world');
console.log(4);
}
逐行解析 koa-compose
這一段很重要,面試的時候常考,讓你手寫一個 compose ,淦它
//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中介軟體
//2. const fn = compose(this.middleware) 合併中介軟體
//3. fn() 執行中介軟體
function compose(middleware) {
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);
}
}
};
}
執行 const fn = compose(this.middleware)
,即如下程式碼
const fn = 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)
}
}
}
}
執行 fn()
,即如下程式碼:
const fn = 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 // index = 0
let fn = middleware[i] // fn 為第一個中介軟體
if (i === middleware.length) fn = next // 當弄到最後一箇中介軟體時,最後一箇中介軟體賦值為 fn
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
// 返回一個 Promise 例項,執行 遞迴執行 dispatch(1)
} catch (err) {
return Promise.reject(err)
}
}
}
}
也就是第一個中介軟體,要先等第二個中介軟體執行完才返回,第二個要等第三個執行完才返回,直到中介軟體執行執行完畢
Promise.resolve
就是個 Promise 例項,之所以使用 Promise.resolve
是為了解決非同步,之所以使用 Promise.resolve
是為了解決非同步
拋去 Promise.resolve
,我們先看一下遞迴的使用,執行以下程式碼
const fn = function () {
return dispatch(0);
function dispatch(i) {
if (i > 3) return;
i++;
console.log(i);
return dispatch(i++);
}
};
fn(); // 1,2,3,4
回過頭來再看一次 compose,程式碼類似於
// 假設 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
if (i === middleware.length) fn = next // fn3 沒有 next
if (!fn) return Promise.resolve() // 因為 fn 為空,執行這一行
function dispatch (0) {
return Promise.resolve(fn(context, function dispatch(1) {
return Promise.resolve(fn(context, function dispatch(2) {
return Promise.resolve()
}))
}))
}
}
}
這種遞迴的方式類似執行棧,先進先出
這裡要多思考一下,遞迴的使用,對 Promise.resolve 不用太在意
上下文的處理
上下文的處理即呼叫了 createContext
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
}
傳入原生的 request 和 response,返回一個 上下文——context,程式碼很清晰,不解釋
res 的特殊處理
callback 中是先執行 this.createContext,拿到上下文後,再去執行 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)
}
一切都清晰了
const Koa = require('Koa');
const app = new Koa();
console.log('app', app);
app.use((ctx, next) => {
ctx.body = 'hello world';
});
app.listen(3000, () => {
console.log('3000請求成功');
});
這樣一段程式碼,例項化後,獲得了 this.middleware、this.context、this.request、this.response 四大將,你使用 app.use() 時,將其中的函式推到 this.middleware。再使用 app.listen() 時,相當於起了一個 HTTP 服務,它合併了中介軟體,獲取了上下文,並對 res 進行了特殊處理
錯誤處理
onerror(err) {
if (!(err instanceof Error))
throw new TypeError(util.format('non-error thrown: %j', 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()
}
context.js
引入我眼簾的是兩個東西
// 1.
const proto = module.exports = {
inspect(){...},
toJSON(){...},
...
}
// 2.
delegate(proto, 'response')
.method('attachment')
.access('status')
...
第一個可以理解為,const proto = { inspect() {...} ...},並且 module.exports 匯出這個物件
第二個可以這麼看,delegate 就是代理,這是為了方便開發者而設計的
// 將內部物件 response 的屬性,委託至暴露在外的 proto 上
delegate(proto, 'response')
.method('redirect')
.method('vary')
.access('status')
.access('body')
.getter('headerSent')
.getter('writable');
...
而使用 delegate(proto, 'response').access('status')...
,就是在 context.js 匯出的檔案,把 proto.response 上的各個引數都代理到 proto 上,那 proto.response 是什麼?就是 context.response,context.response 哪來的?
回顧一下, 在 createContext 中
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.response 有了,就明瞭了, context.response = this.response,因為 delegate,所以 context.response 上的引數代理到了 context 上了,舉個例子
- ctx.header 是 ctx.request.header 上代理的
- ctx.body 是 ctx.response.body 上代理的
request.js 和 response.js
一個處理請求物件,一個處理返回物件,基本上是對原生 req、res 的簡化處理,大量使用了 ES6 中的 get 和 post 語法
大概就是這樣,瞭解了這麼多,怎麼手寫一個 Koa2 呢,請看下一篇——手寫 Koa2