從源碼入手探索koa2應用的實現

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.html

  • 只提供封裝好http上下文、請求、響應,以及基於async/await的中間件容器
  • 基於koa的app是由一系列中間件組成,原來是generator中間件,如今被async/await代替(generator中間件,須要經過中間件koa-convert封裝一下才能使用)
  • 按照app.use(middleware)順序依次執行中間件數組中的方法

1.0 版本是經過組合不一樣的 generator,能夠免除重複繁瑣的回調函數嵌套,並極大地提高錯誤處理的效率。前端

2.0版本Koa放棄了generator,採用Async 函數實現組件數組瀑布流式(Cascading)的開發模式。node

源碼文件

├── lib
│   ├── application.js
│   ├── context.js
│   ├── request.js
│   └── response.js
└── package.json
複製代碼

核心代碼就是lib目錄下的四個文件git

  • application.js 是整個koa2 的入口文件,封裝了context,request,response,以及最核心的中間件處理流程。
  • context.js 處理應用上下文,裏面直接封裝部分request.js和response.js的方法
  • request.js 處理http請求
  • response.js 處理http響應

koa流程

koa整體流程圖

koa的流程分爲三個部分:初始化 -> 啓動Server -> 請求響應github

  • 初始化json

    • 初始化koa對象以前咱們稱爲初始化
  • 啓動serverapi

    • 初始化中間件(中間件創建聯繫)
    • 啓動服務,監聽特定端口,並生成一個新的上下文對象
  • 請求響應數組

    • 接受請求,初始化上下文對象
    • 執行中間件
    • 將body返回給客戶端

初始化

定義了三個對象,context, response, requestbash

  • 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.requestctx.response,好比訪問 ctx.typectx.length 將被代理到 response 對象,ctx.pathctx.method 將被代理到 request 對象。

    每個請求都會建立一段上下文,在控制業務邏輯的中間件中,ctx被寄存在this中(詳細API參見 Koa-context 文檔

啓動Server

  1. 初始化一個koa對象實例
  2. 監聽端口
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,好比經常使用的 listenuse(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
複製代碼
// 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);
}
複製代碼
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)
            }
        }
    }
}
複製代碼

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')
})
複製代碼

koa-bodyparser 請求數據獲取

GET請求數據獲取

獲取GET請求數據有兩個途徑

  1. 是從上下文中直接獲取

    • 請求對象ctx.query,返回如 { a:1, b:2 }
    • 請求字符串 ctx.querystring,返回如 a=1&b=2
  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')
})

複製代碼

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:廣告一波,網易考拉前端招人啦~有興趣的戳我投遞簡歷

參考

相關文章
相關標籤/搜索