過去我不瞭解太陽,那時我過的是冬天——聶魯達html
koa的用法能夠說很是傻瓜,咱們快速過一下:node
首先映入眼簾的不是假山,是hello worldgit
const Koa = require('koa');
const app = new Koa();
app.use((ctx, next) => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
不用框架時的寫法github
let http = require('http')
let server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(4000)
複製代碼
對比發現,相對原生,koa多了兩個實例上的use、listen方法,和use回調中的ctx、next兩個參數。這四個不一樣,幾乎就是koa的所有了,也是這四個不一樣讓koa如此強大。web
簡單!http的語法糖,實際上仍是用了http.createServer(),而後監聽了一個端口。express
比較簡單!利用 上下文(context) 機制,將原來的req,res對象合二爲一,並進行了大量拓展,使開發者能夠方便的使用更多屬性和方法,大大減小了處理字符串、提取信息的時間,免去了許多引入第三方包的過程。(例如ctx.query、ctx.path等)編程
重點!koa的核心 —— 中間件(middleware)。解決了異步編程中回調地獄的問題,基於Promise,利用 洋蔥模型 思想,使嵌套的、糾纏不清的代碼變得清晰、明確,而且可拓展,可定製,藉助許多第三方中間件,可使精簡的koa更加全能(例如koa-router,實現了路由)。其原理主要是一個極其精妙的 compose 函數。在使用時,用 next() 方法,從上一個中間件跳到下一個中間件。json
注:以上加粗部分,下面都有詳細介紹。api
koa有多簡單?簡單到只有四個文件,算上大量的空行和註釋,加起來不到1800行代碼(有用的也就幾百行)。數組
因此,學習koa源碼並非一個痛苦的過程。豪不誇張的說,搞定這四個文件,手寫下面的100多行代碼,你就能徹底理解koa。爲了防止大段代碼的出現,我會講的很詳細。
模仿官方,咱們創建一個koa文件夾,並建立四個文件:application.js,context.js,request.js,response.js。 經過查看package.json能夠發現,application.js爲入口文件。
context.js是上下文對象相關,request.js是請求對象相關,response.js是響應對象相關。
首先,梳理一下思路,原理無非就是use的時候拿到一個回調函數,listen的時候執行這個函數。
此外,use回調函數的參數ctx拓展了不少功能,這個ctx其實就是原生的req、res通過一系列處理產生的。
其實,第一句不許確,use能夠屢次,因此是多個回調函數,用戶第二個參數next()跳到下一個,把多個use的回調函數按照規則順序執行。
那麼,看起來就很簡單了,難點只有兩個:一個是如何將原生req和res加工成ctx,另外一個是如何實現中間件。
第一個,ctx其實就是一個上下文對象,request和response兩個文件用來拓展屬性,context文件實現代理,咱們會手寫相關源碼。
第二個,源碼中的中間件由一箇中間件執行模塊koa-compose實現,這裏咱們會手寫一個。
結合上面hello world,能夠明確,koa是一個類,實例上主要兩個方法,use和listen。
上面說過,listen是http的語法糖,因此要引入http模塊。
Koa有一套錯誤處理機制,須要監聽實例的error事件。因此要引入events模塊繼承EventEmitter。再引入另外三個自定義模塊。
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
class Koa extends EventEmitter {
constructor () {
super()
}
use () {
}
listen () {
}
}
module.exports = Koa
複製代碼
這三個模塊,其實都是一個對象,爲了代碼能跑通,這裏先簡單導出一下。
context.js
let proto = {} // proto同源碼定義的變量名
module.exports = proto
複製代碼
request.js
let request = {}
module.exports = request
複製代碼
response.js
let response = {}
module.exports = response
複製代碼
開始寫Koa類裏面的代碼,先實現建立服務的功能:一、listen方法建立一個http服務並監聽一個端口。二、use方法把回調傳入。
class Koa extends EventEmitter {
constructor () {
super()
this.fn
}
use (fn) {
this.fn = fn // 用戶使用use方法時,回調賦給this.fn
}
listen (...args) {
let server = http.createServer(this.fn) // 放入回調
server.listen(...args) // 由於listen方法可能有多參數,因此這裏直接解構全部參數就能夠了
}
}
複製代碼
這樣就能夠啓動一個服務了,測試一下:
let Koa = require('./application')
let app = new Koa()
app.use((req, res) => { // 還沒寫中間件,因此這裏還不是ctx和next
res.end('hello world')
})
app.listen(3000)
複製代碼
下面先解決ctx,ctx是一個上下文對象,裏面綁定了不少請求和相應相關的數據和方法,例如ctx.path、ctx.query、ctx.body()等等等等,極大的爲開發提供了便利。
思路是這樣的:用戶調用use方法時,把這個回調fn存起來,建立一個createContext函數用來建立上下文,建立一個handleRequest函數用來處理請求,用戶listen時將handleRequest放進createServer回調中,在函數內調用fn並將上下文對象傳入,用戶就獲得了ctx。
class Koa extends EventEmitter {
constructor () {
super()
this.fn
this.context = context // 將三個模塊保存,全局的放到實例上
this.request = request
this.response = response
}
use (fn) {
this.fn = fn
}
createContext(req, res){ // 這是核心,建立ctx
// 使用Object.create方法是爲了繼承this.context但在增長屬性時不影響原對象
const ctx = Object.create(this.context)
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
// 請仔細閱讀如下眼花繚亂的操做,後面是有用的
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx
request.response = response
response.request = request
return ctx
}
handleRequest(req,res){ // 建立一個處理請求的函數
let ctx = this.createContext(req, res) // 建立ctx
this.fn(ctx) // 調用用戶給的回調,把ctx還給用戶使用。
res.end(ctx.body) // ctx.body用來輸出到頁面,後面會說如何綁定數據到ctx.body
}
listen (...args) {
let server = http.createServer(this.handleRequest.bind(this))// 這裏使用bind調用,以防this丟失
server.listen(...args)
}
}
複製代碼
若是不理解Object.create能夠看這個例子:
let o1 = {a: 'hello'}
let o2 = Object.create(o1)
o2.b = 'world'
console.log('o1:', o1.b) // 建立出的對象不會影響原對象
console.log('o2:', o2.a) // 建立出的對象會繼承原對象的屬性
複製代碼
o1: undefined
o2: hello
通過上面的操做,用戶在ctx上能夠用各類姿式取到想要的值。
例如url,能夠用ctx.req.url、ctx.request.req.url、ctx.response.req.url取到。
app.use((ctx) => {
console.log(ctx.req.url)
console.log(ctx.request.req.url)
console.log(ctx.response.req.url)
console.log(ctx.request.url)
console.log(ctx.request.path)
console.log(ctx.url)
console.log(ctx.path)
})
複製代碼
訪問localhost:3000/abc
/abc
/abc
/abc
/undefined
/undefined
/undefined
/undefined
姿式多,不必定爽,要想爽,咱們但願能實現如下兩點:
1 修改request
let url = require('url')
let request = {
get url() { // 這樣就能夠用ctx.request.url上取值了,不用經過原生的req
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
},
get query() {
return url.parse(this.req.url).query
}
// 。。。。。。
}
module.exports = request
複製代碼
很是簡單,使用對象get訪問器返回一個處理過的數據就能夠將數據綁定到request上了,這裏的問題是如何拿到數據,因爲前面ctx.request這一步,因此this就是ctx,那this.req就是原生的req,再利用一些第三方模塊對req進行處理就能夠了,源碼上拓展了很是多,這裏只舉例幾個,看懂原理便可。
訪問localhost:3000/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc
undefined
undefined
2 接下來要實現ctx直接取值,這裏是經過一個代理來實現的
let proto = {
}
function defineGetter(prop, name){ // 建立一個defineGetter函數,參數分別是要代理的對象和對象上的屬性
proto.__defineGetter__(name, function(){ // 每一個對象都有一個__defineGetter__方法,能夠用這個方法實現代理,下面詳解
return this[prop][name] // 這裏的this是ctx(緣由下面解釋),因此ctx.url獲得的就是this.request.url
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
// .......
module.exports = proto
複製代碼
訪問localhost:3000/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc?id=1
/abc
/abc?id=1
/abc
__defineGetter__方法能夠將一個函數綁定在當前對象的指定屬性上,當那個屬性的值被讀取時,你所綁定的函數就會被調用,第一個參數是屬性,第二個是函數,因爲ctx繼承了proto,因此當ctx.url時,觸發了__defineGetter__方法,因此這裏的this就是ctx。這樣,當調用defineGetter方法,就能夠將參數一的參數二屬性代理到ctx上了。
有個問題,要代理多少個屬性就要調用多少遍defineGetter函數麼?是的,若是想優雅一點,能夠模仿官方源碼,提出一個delegates模塊,批量代理(其實也沒優雅到哪去),這裏爲了方便展現,仍是看懂便可吧。
3 修改response。根據koa的api,輸出數據到頁面不是res.end('xx')也不是res.send('xx'),而是ctx.body = 'xx'。咱們要實現設置ctx.body,還要實現獲取ctx.body。
let response = {
get body(){
return this._body // get時返回出去
},
set body(value){
this.res.statusCode = 200 // 只要設置了body,就應該把狀態碼設置爲200
this._body = value // set時先保存下來
}
}
module.exports = response
複製代碼
這樣獲得的是ctx.response.body,並非ctx.body,一樣,經過context代理一下
修改context
let proto = {
}
function defineGetter (prop, name) {
proto.__defineGetter__(name, function(){
return this[prop][name]
})
}
function defineSetter (prop, name) {
proto.__defineSetter__(name, function(val){ // 用__defineSetter__方法設置值
this[prop][name] = val
})
}
defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('response', 'body') // 一樣代理response的body屬性
defineSetter('response', 'body') // 同理
module.exports = proto
複製代碼
測試一下
app.use((ctx) => {
ctx.body = 'hello world'
console.log(ctx.body)
})
複製代碼
訪問localhost:3000
node控制檯輸出:
hello world
網頁顯示:hello world
接下來解決一下body的問題,上面說了,一旦給body設置值,狀態碼就改爲200,那麼沒設置值就應該是404。還有,用戶不光會輸出字符串,還有多是文件、頁面、json等,這裏都要處理,因此改一下handleRequest函數:
let Stream = require('stream') // 引入stream
handleRequest(req,res){
res.statusCode = 404 // 默認404
let ctx = this.createContext(req, res)
this.fn(ctx)
if(typeof ctx.body == 'object'){ // 若是是個對象,按json形式輸出
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){ // 若是是流
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { // 若是是字符串或buffer
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}
複製代碼
如今只能use一次,咱們要實現use屢次,並能夠在use的回調函數中使用next方法跳到下一個中間件,在此以前,咱們先了解一個概念:「洋蔥模型」。
當咱們屢次使用use時
app.use((crx, next) => {
console.log(1)
next()
console.log(2)
})
app.use((crx, next) => {
console.log(3)
next()
console.log(4)
})
app.use((crx, next) => {
console.log(5)
next()
console.log(6)
})
複製代碼
它的執行順序是這樣的:
1
3
5
6
4
2
next方法會調用下一個use,next下面的代碼會在下一個use執行完再執行,咱們能夠把上面的代碼想象成這樣:
app.use((ctx, next) => {
console.log(1)
// next() 被替換成下一個use裏的代碼
console.log(3)
// next() 又被替換成下一個use裏的代碼
console.log(5)
// next() 沒有下一個use了,因此這個無效
console.log(6)
console.log(4)
console.log(2)
})
複製代碼
這樣的話,理所應當輸出135642
這就是洋蔥模型了,經過next把執行權交給下一個中間件。
這樣,開發者手中的請求數據會像儀仗隊同樣,乖乖的通過每一層中間件的檢閱,最後響應給用戶。
既應付了複雜的操做,又避免了混亂的嵌套。
除此以外,koa的中間件還支持異步,可使用async/await
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
let p = new Promise((resolve, roject) => {
setTimeout(() => {
console.log('3.5')
resolve()
}, 1000)
})
await p.then()
await next()
console.log(4)
ctx.body = 'hello world'
})
複製代碼
1
3
// 一秒後
3.5
4
2
async函數返回的是一個promise,當上一個use的next前加上await關鍵字,會等待下一個use的回調resolve了再繼續執行代碼。
全部如今要作的事有兩步:
這裏用到了數組和遞歸,每次use將當前函數存到一個數組中,最後按順序執行。執行這一步用到一個compose函數,這個函數是重中之重。
constructor () {
super()
// this.fn 改爲:
this.middlewares = [] // 須要一個數組將每一箇中間件按順序存放起來
this.context = context
this.request = request
this.response = response
}
use (fn) {
// this.fn = fn 改爲:
this.middlewares.push(fn) // 每次use,把當前回調函數存進數組
}
compose(middlewares, ctx){ // 簡化版的compose,接收中間件數組、ctx對象做爲參數
function dispatch(index){ // 利用遞歸函數將各中間件串聯起來依次調用
if(index === middlewares.length) return // 最後一次next不能執行,否則會報錯
let middleware = middlewares[index] // 取當前應該被調用的函數
middleware(ctx, () => dispatch(index + 1)) // 調用並傳入ctx和下一個將被調用的函數,用戶next()時執行該函數
}
dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
// this.fn(ctx) 改爲:
this.compose(this.middlewares, ctx) // 調用compose,傳入參數
if(typeof ctx.body == 'object'){
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}
複製代碼
再次測試上面打印123456的例子,能夠正確的獲得135642
最後一步,用Promise.resolve將每一個回調包裝成Promise,並在調用時then,不懂Promise的能夠去看個人另外一篇文章[juejin.cn/post/684490…]
compose(middlewares, ctx){
function dispatch(index){
if(index === middlewares.length) return Promise.resolve() // 若最後一箇中間件,返回一個resolve的promise
let middleware = middlewares[index]
return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) // 用Promise.resolve把中間件包起來
}
return dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
let fn = this.compose(this.middlewares, ctx)
fn.then(() => { // then了以後再進行判斷
if(typeof ctx.body == 'object'){
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream){
ctx.body.pipe(res)
}
else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}).catch(err => { // 監控錯誤發射error,用於app.on('error', (err) =>{})
this.emit('error', err)
res.statusCode = 500
res.end('server error')
})
}
複製代碼
let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')
let Stream = require('stream')
class Koa extends EventEmitter {
constructor () {
super()
this.middlewares = []
this.context = context
this.request = request
this.response = response
}
use (fn) {
this.middlewares.push(fn)
}
createContext(req, res){
const ctx = Object.create(this.context)
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
request.ctx = response.ctx = ctx
request.response = response
response.request = request
return ctx
}
compose(middlewares, ctx){
function dispatch (index) {
if (index === middlewares.length) return Promise.resolve()
let middleware = middlewares[index]
return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
}
return dispatch(0)
}
handleRequest(req,res){
res.statusCode = 404
let ctx = this.createContext(req, res)
let fn = this.compose(this.middlewares, ctx)
fn.then(() => {
if (typeof ctx.body == 'object') {
res.setHeader('Content-Type', 'application/json;charset=utf8')
res.end(JSON.stringify(ctx.body))
} else if (ctx.body instanceof Stream) {
ctx.body.pipe(res)
} else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) {
res.setHeader('Content-Type', 'text/htmlcharset=utf8')
res.end(ctx.body)
} else {
res.end('Not found')
}
}).catch(err => {
this.emit('error', err)
res.statusCode = 500
res.end('server error')
})
}
listen (...args) {
let server = http.createServer(this.handleRequest.bind(this))
server.listen(...args)
}
}
module.exports = Koa
複製代碼
這樣就完成了所有核心功能的編寫,經過本文你就能夠足夠了解koa了,若是對你有幫助,不妨點個贊。