A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.html
1.0 版本是經過組合不一樣的 generator,能夠免除重複繁瑣的回調函數嵌套,並極大地提高錯誤處理的效率。前端
2.0版本Koa放棄了generator,採用Async 函數實現組件數組瀑布流式(Cascading)的開發模式。node
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
複製代碼
核心代碼就是lib目錄下的四個文件git
koa的流程分爲三個部分:初始化 -> 啓動Server -> 請求響應github
初始化json
啓動serverapi
請求響應數組
初始化
定義了三個對象,context
, response
, request
bash
request
定義了一些set/get訪問器,用於設置和獲取請求報文和url信息,例如獲取query數據,獲取請求的url(詳細API參見Koa-request文檔)服務器
response
定義了一些set/get操做和獲取響應報文的方法(詳細API參見Koa-response 文檔)
context
經過第三方模塊 delegate 將 koa 在 Response 模塊和 Request 模塊中定義的方法委託到了 context 對象上,因此如下的一些寫法是等價的:
//在每次請求中,this 用於指代這次請求建立的上下文 context(ctx)
this.body ==> this.response.body
this.status ==> this.response.status
this.href ==> this.request.href
this.host ==> this.request.host
......
複製代碼
爲了方便使用,許多上下文屬性和方法都被委託代理到他們的 ctx.request
或 ctx.response
,好比訪問 ctx.type
和 ctx.length
將被代理到 response
對象,ctx.path
和 ctx.method
將被代理到 request
對象。
每個請求都會建立一段上下文,在控制業務邏輯的中間件中,ctx
被寄存在this
中(詳細API參見 Koa-context 文檔)
啓動Server
- 初始化一個koa對象實例
- 監聽端口
var koa = require('koa');
var app = koa()
app.listen(9000)
複製代碼
解析啓動流程,分析源碼
application.js
是koa的入口文件// 暴露出來class,`class Application extends Emitter`,用new新建一個koa應用。
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false; // 是否信任proxy header,默認false // TODO
this.middleware = []; // 保存經過app.use(middleware)註冊的中間件
this.subdomainOffset = 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建立
}
...
複製代碼
Application.js
除了上面的的構造函數外,還暴露了一些公用的api,好比經常使用的 listen
和use
(use放在後面講)。
做用: 啓動koa server
語法糖
// 用koa啓動server
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
// 等價於
// node原生啓動server
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001); // on mutilple address
複製代碼
// listen
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
封裝了nodejs的建立http server,在監聽端口以前會先執行this.callback()
// callback
callback() {
// 使用koa-compose(後面會講) 串聯中間件堆棧中的middleware,返回一個函數
// fn接受兩個參數 (context, next)
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
// this.callback()返回一個函數handleReqwuest,請求過來的時候,回調這個函數
// handleReqwuest接受參數 (req, res)
const handleRequest = (req, res) => {
// 爲每個請求建立ctx,掛載請求相關信息
const ctx = this.createContext(req, res);
// handleRequest的解析在【請求響應】部分
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製代碼
const ctx = this.createContext(req, res);
建立一個最終可用版的context
ctx上包含5個屬性,分別是request,response,req,res,app
request和response也分別有5個箭頭指向它們,因此也是一樣的邏輯
補充瞭解 各對象之間的關係
最左邊一列表示每一個文件的導出對象
中間一列表示每一個Koa應用及其維護的屬性
右邊兩列表示對應每一個請求所維護的一些列對象
黑色的線表示實例化
紅色的線表示原型鏈
藍色的線表示屬性
請求響應
回顧一下,koa啓動server的代碼
app.listen = function() {
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
複製代碼
// callback
callback() {
const fn = compose(this.middleware);
...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製代碼
callback()
返回了一個請求處理函數this.handleRequest(ctx, fn)
// handleRequest
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
// 請求走到這裏標明成功了,http respond code設爲默認的404 TODO 爲何?
res.statusCode = 404;
// koa默認的錯誤處理函數,它處理的是錯誤致使的異常結束
const onerror = err => ctx.onerror(err);
// respond函數裏面主要是一些收尾工做,例如判斷http code爲空如何輸出,http method是head如何輸出,body返回是流或json時如何輸出
const handleResponse = () => respond(ctx);
// 第三方函數,用於監聽 http response 的結束事件,執行回調
// 若是response有錯誤,會執行ctx.onerror中的邏輯,設置response類型,狀態碼和錯誤信息等
onFinished(res, onerror);
// 執行中間件,監聽中間件執行結果
// 成功:執行response
// 失敗,捕捉錯誤信息,執行對應處理
// 返回Promise對象
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
做用: 將函數推入middleware數組
use(fn) {
// 首先判斷傳進來的參數,傳進來的不是一個函數,報錯
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 判斷這個函數是否是 generator
// koa 後續的版本推薦使用 await/async 的方式處理異步
// 因此會慢慢不支持 koa1 中的 generator,再也不推薦你們使用 generator
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
// 若是是 generator,控制檯警告,而後將函數進行包裝
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
// 將函數推入 middleware 這個數組,後面要依次調用裏面的每個中間件
this.middleware.push(fn);
// 保證鏈式調用
return this;
}
複製代碼
const fn = compose(this.middleware)
app.use([MW])僅僅是將函數推入middleware數組,真正讓這一系列函數組合成爲中間件的,是koa-compose,koa-compose是Koa框架中間件執行的發動機
'use strict'
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) {
// last called middleware #
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
// 若是中間件中沒有 await next ,那麼函數直接就退出了,不會繼續遞歸調用
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
Koa2.x的compose方法雖然從純generator函數執行修改爲了基於Promise.all,可是中間件加載的中心思想沒有發生改變,依舊是從第一個中間件開始,遇到await/yield next,就中斷本中間件的代碼執行,跳轉到對應的下一個中間件執行期內的代碼…一直到最後一箇中間件,而後逆序回退到倒數第二個中間件await/yield next下部分的代碼執行,完成後繼續會退…一直會退到第一個中間件await/yield next下部分的代碼執行完成,中間件所有執行結束
級聯的流程,V型加載機制
koa-router 路由
對其實現機制有興趣的能夠戳看看 -> Koa-router路由中間件API詳解
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()
const Router = require('koa-router')
// 子路由1
let home = new Router()
home.get('/', async ( ctx )=>{
let html = `
<ul>
<li><a href="/page/helloworld">/page/helloworld</a></li>
<li><a href="/page/404">/page/404</a></li>
</ul>
`
ctx.body = html
})
// 子路由2
let page = new Router()
page.get('hello', async (ctx) => {
ctx.body = 'Hello World Page!'
})
// 裝載全部子路由的中間件router
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())
// 加載router
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000, () => {
console.log('[demo] route-use-middleware is starting at port 3000')
})
複製代碼
koa-bodyparser 請求數據獲取
獲取GET請求數據有兩個途徑
是從上下文中直接獲取
是從上下文的request對象中獲取
對於POST請求的處理,koa2沒有封裝獲取參數的方法須要經過解析上下文context中的原生node.js請求對象req,將POST表單數據解析成query string(例如:a=1&b=2&c=3),再將query string 解析成JSON格式(例如:{"a":"1", "b":"2", "c":"3"})
對於POST請求的處理,koa-bodyparser中間件能夠把koa2上下文的formData數據解析到ctx.request.body中
...
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
app.use( async ( ctx ) => {
if ( ctx.url === '/' && ctx.method === 'POST' ) {
// 當POST請求的時候,中間件koa-bodyparser解析POST表單裏的數據,並顯示出來
let postData = ctx.request.body
ctx.body = postData
} else {
...
}
})
app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
複製代碼
koa-static 靜態資源加載
爲靜態資源訪問建立一個服務器,根據url訪問對應的文件夾、文件
...
const static = require('koa-static')
const app = new Koa()
// 靜態資源目錄對於相對入口文件index.js的路徑
const staticPath = './static'
app.use(static(
path.join( __dirname, staticPath)
))
app.use( async ( ctx ) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000')
})
複製代碼
PS:廣告一波,網易考拉前端招人啦~有興趣的戳我投遞簡歷