日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Koa源码解析

發布時間:2025/3/20 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Koa源码解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Koa是一款設計優雅的輕量級Node.js框架,它主要提供了一套巧妙的中間件機制與簡練的API封裝,因此源碼閱讀起來也十分輕松,不論你從事前端或是后端研發,相信都會有所收獲。

目錄結構

首先將源碼下載到本地,可以看到Koa的源碼只包含下述四個文件:

lib ├── application.js ├── context.js ├── request.js └── response.js

application.js

application.js為Koa的主程序入口文件,在package.json的main字段有定義。它主要負責HTTP服務的注冊、封裝請求相應對象,并初始化中間件數組并通過compose方法進行執行。

context.js

context.js的核心工作為將請求與響應方法集成到一個上下文(Context)中,上下文中的大多數方法都是直接委托到了請求與響應對象上,本身并沒做什么改變,它能為編寫Web應用程序提供便捷。

request.js

request.js將http庫的request方法進行抽象與封裝,通過它可以訪問到各種請求信息。

response.js

response.js與request功能類似,它是對response對象的抽象與封裝。

中間件

示例

對于Koa的中間件機制相信大家都耳熟能詳了,現在讓我們來看看源碼實現。在這里還是先舉一個最簡單的例子:

const Koa = require('koa'); const app = new Koa();app.use((ctx, next) => {console.log('enter 1');next();console.log('out 1'); });app.use((ctx, next) => {console.log('enter 2');next();console.log('out 2'); });app.use((ctx, next) => {console.log('enter 3');next();console.log('out 3'); });app.listen(3000);

現在讓我們來訪問應用:curl 127.0.0.1:3000,可以看到以下輸出結果:

enter 1 enter 2 enter 3 out 3 out 2 out 1

next是什么?

通過以上的結果進行分析,當我們執行next()的時候,可能程序的執行權交給了下一個中間件,next函數會等待下一個中間件執行完畢,然后接著執行,這樣的執行機制被稱為“洋蔥模型”,因為它就像請求穿過一層洋蔥一樣,先從外向內一層一層執行,再從內向外一層一層返回,而next就是進行下一層的一把鑰匙:

原理

聊完了理想,現在我們來聊現實。首先來看看app.use函數:

use(fn) {if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');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');fn = convert(fn);}debug('use %s', fn._name || fn.name || '-');this.middleware.push(fn);return this;}

整個函數只做了一件事情,將中間件函數添加到了實例中的middleware數組,其他的即是對類型進行校驗,若不為函數則直接報TypeError,若為生成器則發出deprecated警告并使用koa-convert[注1]對其轉化。

中間件在什么時候執行的呢?首先我們找到listen的回調函數:

const server = http.createServer(this.callback());

然后來看看這個神奇的callback函數:

callback() {const fn = compose(this.middleware);if (!this.listenerCount('error')) this.on('error', this.onerror);const handleRequest = (req, res) => {const ctx = this.createContext(req, res);return this.handleRequest(ctx, fn);};return handleRequest;}

函數首先將中間件使用koa-compose進行處理,那個compose到底是個什么呢?不如直接來看源碼吧(省略掉了注釋與類型檢測):

function compose (middleware) {return function (context, next) {// last called middleware #let index = -1return dispatch(0)function dispatch (i) {if (i <= index) return Promise.reject(new Error('next() called multiple times'))index = ilet fn = middleware[i]if (i === middleware.length) fn = nextif (!fn) return Promise.resolve()try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));} catch (err) {return Promise.reject(err)}}} }

首先我們把目光放到index與i兩個變量上,當執行執行compose(middleware)函數時,會返回一個閉包函數distpach(0),閉包函數執行時,dispatch函數內部的判斷邏輯如下:

  • 若i小于等于index 則報出錯誤:'next() called multiple times'。
  • 若i大于index時,將i賦予index,此時i與index相等。
  • 邏輯很簡單,但這樣做的目的是什么呢?假若程序按著預期執行,每個中間件內部都執行next(),假若有3個中間件,那么當每次執行dispatch(i)時,到Line8之前index與i的值分別為:-1/0, 0/1, 1/2,可以看出i始終要大于index,index的閉包變量每次在執行完函數后都會加1,因此可以知道的是若同一個中間件執行了兩次,index就會等于i,再執行一次index就會大于i,由此可知,index的存在意義在于限制next能執行不超過1次。

    Line9到Line11用于取出middleware中的當前中間件,若數組為最大索引標識,則會將fn等于next函數,意味著將再執行一次越級的索引i + 1,由于取不到值,于是就執行到Line11返回Promise.resolve()。

    當函數執行到Line13,則會運行當前中間件,并將是否執行下一個中間件dispatch(i + 1)的決定權傳遞到next參數,將運行結果返回,返回函數的運行結果的意義在于每次執行next的返回結果都是下一個中間件的執行結果的Promise對象。

    回到callback

    讓我們繼續看callback函數等剩余邏輯:

    callback() {const fn = compose(this.middleware);if (!this.listenerCount('error')) this.on('error', this.onerror);const handleRequest = (req, res) => {const ctx = this.createContext(req, res);return this.handleRequest(ctx, fn);};return handleRequest;}

    首先來看看Line3,因為Application繼承與Emitter,故此方法是用于監聽實例中的error事件的,當listenerCount的數值為0時,表示沒有監聽過,則注冊監聽函數。

    接著生成一個handleRequest回調,當每個請求過來時,都會創建ctx上下文對象,并將中間件函數傳入實例方法handleRequest,讓我們來看看此時的處理函數:

    handleRequest(ctx, fnMiddleware) {const res = ctx.res;res.statusCode = 404;const onerror = err => ctx.onerror(err);const handleResponse = () => respond(ctx);onFinished(res, onerror);return fnMiddleware(ctx).then(handleResponse).catch(onerror);}

    在這里多出了幾個函數:

    • on-finished,監聽請求是否正常結束。
    • respond, 當中間件執行完畢后,處理response對象的status與body的字段。

    回到示例

    回到示例,是否恍如隔日?現在的代碼還困擾你嗎?讓我們稍作修改:

    app.use((ctx, next) => {console.log('enter 1');next();console.log('out 1'); });app.use((ctx, next) => {console.log('enter 2'); });app.use((ctx, next) => {console.log('enter 3');next();console.log('out 3'); });

    此時你能準確的知道執行結果嗎?此時打印順序為:enter 1 -> enter 2 -> out 1。因為只有next才是進入到下一中間件的鑰匙。若再將程序改一改:

    app.use(async (ctx, next) => {console.log('enter 1');next();console.log('out 1'); });app.use(async (ctx, next) => {console.log('enter 2');await next();console.log('out 2'); });

    此時執行結果為:enter1 -> enter2 -> out 1 -> out2,這你能答對嗎?你不需要記住范式與結果,回想一下核心的compose函數:return Promise.resolve(fn(context, dispatch.bind(null, i + 1))),首先中間件全為async函數,若使用await next(),則會等待下一個中間件返回resolve狀態才會執行此代碼,如果某一個Promise中間件不使用await關鍵字呢?它會在主進程上進行排隊等待,等到函數執行棧返回到當前函數后立即執行。對于此示例來講,當進入到第二個中間件,遇到await關鍵字時,console.log('out 2')則不會再執行,而是進入到微任務隊列中,此時主進程已無其他任務,則函數退出當前棧,返回到了第一個函數中,此時輸出out 1,當第一個中間件執行結束后,事件循環才會將中間件2的微任務取出來執行,因此你見到了上述的輸出順序。

    上下文

    通過上述分析,我們了解到http.createServer中有一個callback函數,它不僅負責執行compose函數,也會調用createContext方法創建函數上下文,源碼如下:

    createContext(req, res) {const context = Object.create(this.context);const request = context.request = Object.create(this.request);const response = context.response = Object.create(this.response);context.app = request.app = response.app = this;context.req = request.req = response.req = req;context.res = request.res = response.res = res;request.ctx = response.ctx = context;request.response = response;response.request = request;context.originalUrl = request.originalUrl = req.url;context.state = {};return context;}

    可以由這個函數得知,ctx對象包含了`context.js
    request.js、response.js`的代碼。通過訪問req與res的源代碼,大家可以發現在request與response對象中封裝了許許多多http庫的請求方法與各類工具函數,若對http底層實現感興趣的小伙伴可以仔細讀一下request與response文件,否則多查閱幾遍官網文檔,大概了解其中的api即可。

    而對于context.js,其實十分簡單,它也封裝了部分工具方法,并使用node-delegates進行委托方法與屬性,對于此類方法的時間,估計koa3會將這一部分進行重構為Proxy吧。

    注解

    1. koa-convert轉化

    在Koa版本號為1.x時,中間件都是使用Generator實現的,因此可以通過官方提供的koa-convert臨時對其進行轉化與兼容,基本用法為:

    function * legacyMiddleware (next) {// beforeyield next// after } app.use(convert(legacyMiddleware))

    然后打開源碼發現,核心代碼大概如下:

    function convert (mw) {return (ctx, next) => co.call(ctx, mw.call(ctx, createGenerator(next))) }

    convert函數將生成器通過co進行包裝為Promise函數,在ctx上下文進行執行,并傳入next函數。

    總結

    凡是涉及到原理性的東西,感覺自己很難避免自顧自說,用圖片進行可視化的方式會更加直觀,易于理解,希望之后自己多多使用圖片來闡述原理。

    通過源碼分析,我們知道了Koa的核心思想建立于中間件機制,它是一個設計十分簡潔、巧妙的Web框架,擴展性極強,egg.js就是建立于Koa之上的上層框架。

    總結

    以上是生活随笔為你收集整理的Koa源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。