導語:實現簡單的koa核心代碼,便於理解koa原理。順便學習koa代碼的那些騷操做node
建立一個 http 服務,只綁一箇中間件。建立 index.js
npm
/** index.js */
const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
ctx.body = 'Hello World'
})
app.listen(8080, function() {
console.log('server start 8080')
})
複製代碼
從這段代碼中能夠看出json
咱們再來看看 koa 源碼的目錄結構數組
|-- koa
|-- .npminstall.done
|-- History.md
|-- LICENSE
|-- Readme.md
|-- package.json
|-- lib
|-- application.js
|-- context.js
|-- request.js
|-- response.js
複製代碼
其中 application.js 是入口文件,打開後能夠看到是一個 class。context.js、request.js、response.js 都是一個對象,用來組成上下文 ctx
bash
先編寫 application.js 部分代碼。建立 myKoa 文件夾,咱們的koa代碼將會放在這個文件內。建立 myKoa/application.js
app
經過分析已經知道 application.js 導出一個 class,原形上至少有 listen 和 use 兩個方法。listen 建立服務並監聽端口號 http服務,use 用來收集中間件。實現代碼以下koa
/** myKoa/application.js */
const http = require('http')
module.exports = class Koa {
constructor() {
// 存儲中間件
this.middlewares = []
}
// 收集中間件
use(fn) {
this.middlewares.push(fn)
}
// 處理當前請求方法
handleRequest(req, res) { // node 傳入的 req、res
res.end('手寫koa核心代碼') // 爲了訪問頁面有顯示,暫時加上
}
// 建立服務並監聽端口號
listen(...arges) {
const app = http.createServer(this.handleRequest.bind(this))
app.listen(...arges)
}
}
複製代碼
代碼很簡單。use
把中間件存入 middlewares
。listen
啓動服務,每次請求到來調用 handleRequest
異步
context.js、request.js、response.js 都是一個對象,用來組成上下文 ctx
。代碼以下async
/** myKoa/context.js */
const proto = {}
module.exports = proto
複製代碼
/** myKoa/request.js */
module.exports = {}
複製代碼
/** myKoa/response.js */
module.exports = {}
複製代碼
三者的關係是: request.js、response.js 兩個文件的導出會綁定到 context.js 文件導出的對象上,分別做爲 ctx.request 和 ctx.response 使用。函數
koa 爲了每次 new Koa()
使用的 ctx 都是相互獨立的,對 context.js、request.js、response.js 導出的對象作了處理。源碼中使用的是 Object.create()
方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__
。一會在代碼中演示用法
建立ctx以前,再看一下ctx上的幾個屬性,和他們直接的關係。必定要分清哪些是node自帶的,哪些是koa的屬性
app.use(async ctx => {
ctx; // 這是 Context
ctx.req; // 這是 node Request
ctx.res; // 這是 node Response
ctx.request; // 這是 koa Request
ctx.response; // 這是 koa Response
ctx.request.req; // 這是 node Request
ctx.response.res; // 這是 node Response
});
複製代碼
爲何這個設計,在文章後面將會解答
開始建立 ctx。部分代碼以下
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {
// 存儲中間件
this.middlewares = []
// 綁定 context、request、response
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
// 建立上下文 ctx
createContext(req, res) {
const ctx = this.context
// koa 的 Request、Response
ctx.request = this.request
ctx.response = this.response
// node 的 Request、Response
ctx.request.req = ctx.req = req
ctx.response.res = ctx.res = res
return ctx
}
// 收集中間件
use(fn) {/* ... */}
// 處理當前請求方法
handleRequest(req, res) {
// 建立 上下文,準備傳給中間件
const ctx = this.createContext(req, res)
res.end('手寫koa核心代碼') // 爲了訪問頁面有顯示,暫時加上
}
// 建立服務並監聽端口號
listen(...arges) {/* ... */}
}
複製代碼
此時就建立了一個基礎的上下文 ctx。
實現一個 ctx.request.url
。開始前先考慮幾個問題
ctx.req.url
、ctx.request.url
、ctx.request.req.url
三者直接應該始終相等koa 是這樣作的
沒錯,就是 get 語法糖
/** myKoa/request.js */
module.exports = {
get url() {
return this.req.url
}
}
複製代碼
此時的
this
指向的是Object.create(request)
生成的對象,並非 request.js 導出的對象
接下來咱們實現 ctx.response.body = 'Hello World'
。當設置 ctx.response.body
時其實是把屬性存到了 ctx.response._body
上,當獲取 ctx.response.body
時只須要在 ctx.response._body
上取出就能夠了 。代碼以下
/** myKoa/response.js */
module.exports = {
set body(v) {
this._body = v
},
get body() {
return this._body
}
}
複製代碼
此時的
this
指向的是Object.create(response)
生成的對象,並非 response.js 導出的對象
koa 給咱們設置了不少別名,好比 ctx.body
就是 ctx.response.body
有了以前的經驗,獲取/設置屬性就比較容易。直接上代碼
/** myKoa/context.js */
const proto = {
get url() {
return this.request.req.url
},
get body() {
return this.response.body
},
set body(v) {
this.response.body = v
},
}
module.exports = proto
複製代碼
有沒有感受很簡單。固然koa上下文部分沒有到此結束。看 koa/lib/context.js 代碼,在最下面能夠看到這樣的代碼(從只挑選了access
方法)
delegate(proto, 'response')
.access('status')
.access('body')
delegate(proto, 'request')
.access('path')
.access('url')
複製代碼
koa 對 get/set 作了封裝。用的是 Delegator
第三方包。核心是用的 __defineGetter__
與 __defineSetter__
兩個方法。這裏爲了簡單易懂,只是簡單封裝兩個方法代替 Delegator
實現簡單的功能。
// 獲取屬性。調用方法如 defineGetter('response', 'body')
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
// 設置屬性。調用方法如 defineSetter('response', 'body')
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
複製代碼
myKoa/context.js 文件最終修改成
/** myKoa/context.js */
const proto = {}
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
// 請求
defineGetter('request', 'url')
// 響應
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = proto
複製代碼
這步很是簡單,只須要判斷 ctx.body
是否有值,並觸發 req.end()
就完成了。相關代碼以下
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中間件
use(fn) {/* ... */}
// 處理當前請求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
res.statusCode = 404 //默認 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}
// 建立服務並監聽端口號
listen(...arges) {/* ... */}
}
複製代碼
同理能夠處理 header
等屬性
中間件接受兩個參數,一個上下文ctx
,一個 next
方法。上下文ctx
已經寫好了,主要是怎麼實現next
方法。
寫一個dispatch方法,他的主要功能是:好比傳入下標0,找出數組中下標爲0的方法middleware
,調用middleware
並傳入一個方法next
,而且當next
調用時, 查找下標加1的方法。實現以下
const middlewares = [f1, f2, f3]
function dispatch(index) {
if (index === middlewares.length) return
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(next)
}
dispatch(0)
複製代碼
此時就實現了next
方法。
在koa中,是不容許一個請求中一箇中間件調用兩次next
。好比
app.use((ctx, next) => {
ctx.body = 'Hello World'
next()
next() // 報錯 next() called multiple times
})
複製代碼
koa 用了一個小技巧。記錄每次調用的中間件下標,當發現調用的中間件下標沒有加1(中間件下標 <= 上一次中間件下標)時,就報錯。修改代碼以下
const middlewares = [f1, f2, f3] // 好比中間件中有三個方法
let i = -1
function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(next)
}
dispatch(0)
複製代碼
中間件代碼基本完成,傳入 ctx 、加入 myKoa/application.js
文件。
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中間件
use(fn) {/* ... */}
// 處理中間件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
middleware(ctx, next)
}
dispatch(0)
}
// 處理當前請求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
this.compose(ctx)
res.statusCode = 404 //默認 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}
// 建立服務並監聽端口號
listen(...arges) {/* ... */}
}
複製代碼
到此就實現了同步中間件
koa 中使用異步中間件的寫法以下
app.use(async (ctx, next) => {
ctx.body = 'Hello World'
await next()
})
app.use(async (ctx, next) => {
await new Promise((res, rej) => setTimeout(res,1000))
console.log('ctx.body:', ctx.body)
})
複製代碼
上述代碼接受請求後大約1s 控制檯打印 ctx.body: Hello World
。能夠看出,koa是基於 async/await
的。指望每次 next() 後返回的是一個 Promise
。
同時考慮到中間件變爲異步執行,那麼handleRequest
應該等待中間件執行完再執行相關代碼。那麼compose
也應該返回Promise
能夠經過async
快速完成 普通函數 =》Promise 的轉化。
修改compose
代碼和handleRequest
代碼
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {/* ... */}
// 建立上下文 ctx
createContext(req, res) {/* ... */}
// 收集中間件
use(fn) {/* ... */}
// 處理中間件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
async function dispatch(index) {
if (index <= i) throw new Error('next() called multiple times')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
return middleware(ctx, next)
}
return dispatch(0)
}
// 處理當前請求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
const p = this.compose(ctx)
p.then(() => {
res.statusCode = 404 //默認 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch((err) => {
console.log(err)
})
}
// 建立服務並監聽端口號
listen(...arges) {/* ... */}
}
複製代碼
/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
module.exports = class Koa {
constructor() {
// 存儲中間件
this.middlewares = []
// 綁定 context、request、response
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
// 建立上下文 ctx
createContext(req, res) {
const ctx = this.context
// koa 的 Request、Response
ctx.request = this.request
ctx.response = this.response
// node 的 Request、Response
ctx.request.req = ctx.req = req
ctx.response.res = ctx.res = res
return ctx
}
// 收集中間件
use(fn) {
this.middlewares.push(fn)
}
// 處理中間件
compose(ctx) {
const middlewares = this.middlewares
let i = -1
async function dispatch(index) {
if (index <= i) throw new Error('multi called next()')
if (index === middlewares.length) return
i = index
const middleware = middlewares[index]
const next = () => dispatch(index+1)
return middleware(ctx, next)
}
return dispatch(0)
}
// 處理當前請求方法
handleRequest(req, res) {
const ctx = this.createContext(req, res)
const p = this.compose(ctx)
p.then(() => {
res.statusCode = 404 //默認 status
if (ctx.body) {
res.statusCode = 200
res.end(ctx.body)
} else {
res.end('Not Found')
}
}).catch((err) => {
console.log(err)
})
}
// 建立服務並監聽端口號
listen(...arges) {
const app = http.createServer(this.handleRequest.bind(this))
app.listen(...arges)
}
}
複製代碼
/** myKoa/context.js */
const proto = {}
function defineGetter(property, key) {
proto.__defineGetter__(key, function() {
return this[property][key]
})
}
function defineSetter(property, key) {
proto.__defineSetter__(key, function(v) {
this[property][key] = v
})
}
// 請求
defineGetter('request', 'url')
// 響應
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = proto
複製代碼
/** myKoa/request.js */
module.exports = {
get url() {
return this.req.url
}
}
複製代碼
/** myKoa/response.js */
module.exports = {
set body(v) {
this._body = v
},
get body() {
return this._body
}
}
複製代碼