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

xdtt1946 6年前發布 | 30K 次閱讀 Koa.js 中間件 前端技術

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對象之前我們稱為初始化
    </li>
  • 啟動server

    • 初始化中間件(中間件建立聯系)
    • 啟動服務,監聽特定端口,并生成一個新的上下文對象
    • </ul> </li>
    • 請求響應

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

        初始化

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

        • 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

        1. 初始化一個koa對象實例
        2. 監聽端口
        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 = &lt;ul&gt; &lt;li&gt;&lt;a href="/page/helloworld"&gt;/page/helloworld&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="/page/404"&gt;/page/404&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; 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請求數據有兩個途徑

        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') }) </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>

        參考

         

        來自:https://blog.kaolafed.com/2017/12/29/從源碼入手探索koa2應用的實現/

         

 本文由用戶 xdtt1946 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!