從源碼入手探索koa2應用的實現
A Koa application is an object containing an array of middleware functions which are composed and executed in a stack-like manner upon request.
- 只提供封裝好http上下文、請求、響應,以及基于async/await的中間件容器
- 基于koa的app是由一系列中間件組成,原來是generator中間件,現在被async/await代替(generator中間件,需要通過中間件koa-convert封裝一下才能使用)
- 按照app.use(middleware)順序依次執行中間件數組中的方法
1.0 版本是通過組合不同的 generator,可以免除重復繁瑣的回調函數嵌套,并極大地提升錯誤處理的效率。 2.0版本Koa放棄了generator,采用Async 函數實現組件數組瀑布流式(Cascading)的開發模式。
源碼文件
├── lib │ ├── application.js │ ├── context.js │ ├── request.js │ └── response.js └── package.json
核心代碼就是lib目錄下的四個文件
- application.js 是整個koa2 的入口文件,封裝了context,request,response,以及最核心的中間件處理流程。
- context.js 處理應用上下文,里面直接封裝部分request.js和response.js的方法
- request.js 處理http請求
- response.js 處理http響應
koa流程
koa的流程分為三個部分: 初始化 -> 啟動Server -> 請求響應
-
初始化
- 初始化koa對象之前我們稱為初始化
-
啟動server
- 初始化中間件(中間件建立聯系)
- 啟動服務,監聽特定端口,并生成一個新的上下文對象 </ul> </li>
-
請求響應
- 接受請求,初始化上下文對象
- 執行中間件
- 將body返回給客戶端 </ul> </li> </ul>
- 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 文檔 )
- 初始化一個koa對象實例
- 監聽端口
初始化
定義了三個對象, context , response , request
啟動Server
var koa = require('koa'); var app = koa()
app.listen(9000) </pre>
解析啟動流程,分析源碼
application.js 是koa的入口文件
// 暴露出來class,
class Application extends Emitter
,用new新建一個koa應用。 module.exports = class Applicationextends 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創建 } ...
</pre>
Application.js 除了上面的的構造函數外,還暴露了一些公用的api,比如常用的 listen 和 use (use放在后面講)。
listen
作用: 啟動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 </pre>
// 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;
} </pre>
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);
} </pre>
Koa處理請求的過程:當請求到來的時候,會通過 req 和 res 來創建一個 context (ctx) ,然后執行中間件
koa中另一個常用API - use
作用: 將函數推入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; }
koa-compose
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) } } }
} </pre>
Koa2.x的compose方法雖然從純generator函數執行修改成了基于Promise.all,但是中間件加載的中心思想沒有發生改變,依舊是從第一個中間件開始,遇到await/yield next,就中斷本中間件的代碼執行,跳轉到對應的下一個中間件執行期內的代碼…一直到最后一個中間件,然后逆序回退到倒數第二個中間件await/yield next下部分的代碼執行,完成后繼續會退…一直會退到第一個中間件await/yield next下部分的代碼執行完成,中間件全部執行結束
級聯的流程,V型加載機制
koa2常用中間件
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') }) </pre>
koa-bodyparser 請求數據獲取
GET請求數據獲取
獲取GET請求數據有兩個途徑
-
是從上下文中直接獲取
- 請求對象ctx.query,返回如 { a:1, b:2 }
- 請求字符串 ctx.querystring,返回如 a=1&b=2
-
是從上下文的request對象中獲取
- 請求對象ctx.request.query,返回如 { a:1, b:2 }
- 請求字符串 ctx.request.querystring,返回如 a=1&b=2
POST請求數據獲取
對于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') }) </pre>
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') }) </pre>
參考
- koa文檔
- 深入淺出koa #2
- 深入淺出koa2 #11
- Node.js Koa 之Async中間件
- koa中文文檔
- koa2 源碼分析 (一)
- Koa2源碼閱讀筆記
- Koa2進階學習筆記
- Koa-router路由中間件API詳解
- 跨入Koa2.0,從Compose開始
來自:https://blog.kaolafed.com/2017/12/29/從源碼入手探索koa2應用的實現/