摘要:實現的四大模塊上文簡述了源碼的大體框架結構,接下來我們來實現一個的框架,筆者認為理解和實現一個框架需要實現四個大模塊,分別是封裝創建類構造函數構造對象中間件機制和剝洋蔥模型的實現錯誤捕獲和錯誤處理下面我們就逐一分析和實現。
什么是koa框架?
? ? ? ?koa是一個基于node實現的一個新的web框架,它是由express框架的原班人馬打造的。它的特點是優雅、簡潔、表達力強、自由度高。它更express相比,它是一個更輕量的node框架,因為它所有功能都通過插件實現,這種插拔式的架構設計模式,很符合unix哲學。
? ? ? ?koa框架現在更新到了2.x版本,本文從零開始,循序漸進,講解koa2的框架源碼結構和實現原理,展示和詳解koa2框架源碼中的幾個最重要的概念,然后手把手教大家親自實現一個簡易的koa2框架,幫助大家學習和更深層次的理解koa2,看完本文以后,再去對照koa2的源碼進行查看,相信你的思路將會非常的順暢。
? ? ? ?本文所用的框架是koa2,它跟koa1不同,koa1使用的是generator+co.js的執行方式,而koa2中使用了async/await,因此本文的代碼和demo需要運行在node 8版本及其以上,如果讀者的node版本較低,建議升級或者安裝babel-cli,用其中的babel-node來運行本文涉及到的代碼。
? ? ? ?本文實現的輕量版koa的完整代碼gitlab地址為:article_koa2
koa源碼結構? ? ? ?上圖是koa2的源碼目錄結構的lib文件夾,lib文件夾下放著四個koa2的核心文件:application.js、context.js、request.js、response.js。
application.js? ? ? ? application.js是koa的入口文件,它向外導出了創建class實例的構造函數,它繼承了events,這樣就會賦予框架事件監聽和事件觸發的能力。application還暴露了一些常用的api,比如toJSON、listen、use等等。
? ? ? ? listen的實現原理其實就是對http.createServer進行了一個封裝,重點是這個函數中傳入的callback,它里面包含了中間件的合并,上下文的處理,對res的特殊處理。
? ? ? ? use是收集中間件,將多個中間件放入一個緩存隊列中,然后通過koa-compose這個插件進行遞歸組合調用這一些列的中間件。
context.js? ? ? ? 這部分就是koa的應用上下文ctx,其實就一個簡單的對象暴露,里面的重點在delegate,這個就是代理,這個就是為了開發者方便而設計的,比如我們要訪問ctx.repsponse.status但是我們通過delegate,可以直接訪問ctx.status訪問到它。
request.js、response.js? ? ? ? 這兩部分就是對原生的res、req的一些操作了,大量使用es6的get和set的一些語法,去取headers或者設置headers、還有設置body等等,這些就不詳細介紹了,有興趣的讀者可以自行看源碼。
實現koa2的四大模塊? ? ? ? 上文簡述了koa2源碼的大體框架結構,接下來我們來實現一個koa2的框架,筆者認為理解和實現一個koa框架需要實現四個大模塊,分別是:
封裝node http server、創建Koa類構造函數
構造request、response、context對象
中間件機制和剝洋蔥模型的實現
錯誤捕獲和錯誤處理
下面我們就逐一分析和實現。
模塊一:封裝node http server和創建Koa類構造函數? ? ? ? 閱讀koa2的源碼得知,實現koa的服務器應用和端口監聽,其實就是基于node的原生代碼進行了封裝,如下圖的代碼就是通過node原生代碼實現的服務器監聽。
let http = require("http"); let server = http.createServer((req, res) => { res.writeHead(200); res.end("hello world"); }); server.listen(3000, () => { console.log("listenning on 3000"); });
? ? ? ? 我們需要將上面的node原生代碼封裝實現成koa的模式:
const http = require("http"); const Koa = require("koa"); const app = new Koa(); app.listen(3000);
? ? ? ? 實現koa的第一步就是對以上的這個過程進行封裝,為此我們需要創建application.js實現一個Application類的構造函數
let http = require("http"); class Application { constructor() { this.callbackFunc; } listen(port) { let server = http.createServer(this.callback()); server.listen(port); } use(fn) { this.callbackFunc = fn; } callback() { return (req, res) => { this.callbackFunc(req, res); }; } } module.exports = Application;
? ? ? ? 然后創建example.js,引入application.js,運行服務器實例啟動監聽代碼:
let Koa = require("./application"); let app = new Koa(); app.use((req, res) => { res.writeHead(200); res.end("hello world"); }); app.listen(3000, () => { console.log("listening on 3000"); });
? ? ? ? 現在在瀏覽器輸入localhost:3000即可看到瀏覽器里顯示“hello world”。現在第一步我們已經完成了,對http server進行了簡單的封裝和創建了一個可以生成koa實例的類class,這個類里還實現了app.use用來注冊中間件和注冊回調函數,app.listen用來開啟服務器實例并傳入callback回調函數,第一模塊主要是實現典型的koa風格和搭好了一個koa的簡單的架子。接下來我們開始編寫和講解第二模塊。
模塊二:構造request、response、context對象? ? ? ? 閱讀koa2的源碼得知,其中context.js、request.js、response.js三個文件分別是request、response、context三個模塊的代碼文件。context就是我們平時寫koa代碼時的ctx,它相當于一個全局的koa實例上下文this,它連接了request、response兩個功能模塊,并且暴露給koa的實例和中間件等回調函數的參數中,起到承上啟下的作用。
? ? ? ? request、response兩個功能模塊分別對node的原生request、response進行了一個功能的封裝,使用了getter和setter屬性,基于node的對象req/res對象封裝koa的request/response對象。我們基于這個原理簡單實現一下request.js、response.js,首先創建request.js文件,然后寫入以下代碼:
let url = require("url"); module.exports = { get query() { return url.parse(this.req.url, true).query; } };
? ? ? ? 這樣當你在koa實例里使用ctx.query的時候,就會返回url.parse(this.req.url, true).query的值。看源碼可知,基于getter和setter,在request.js里還封裝了header、url、origin、path等方法,都是對原生的request上用getter和setter進行了封裝,筆者不再這里一一實現。
? ? ? ? 接下來我們實現response.js文件代碼模塊,它和request原理一樣,也是基于getter和setter對原生response進行了封裝,那我們接下來通過對常用的ctx.body和ctx.status這個兩個語句當做例子簡述一下如果實現koa的response的模塊,我們首先創建好response.js文件,然后輸入下面的代碼:
module.exports = { get body() { return this._body; }, set body(data) { this._body = data; }, get status() { return this.res.statusCode; }, set status(statusCode) { if (typeof statusCode !== "number") { throw new Error("something wrong!"); } this.res.statusCode = statusCode; } };
? ? ? ? 以上代碼實現了對koa的status的讀取和設置,讀取的時候返回的是基于原生的response對象的statusCode屬性,而body的讀取則是對this._body進行讀寫和操作。這里對body進行操作并沒有使用原生的this.res.end,因為在我們編寫koa代碼的時候,會對body進行多次的讀取和修改,所以真正返回瀏覽器信息的操作是在application.js里進行封裝和操作。
? ? ? ? 現在我們已經實現了request.js、response.js,獲取到了request、response對象和他們的封裝的方法,然后我們開始實現context.js,context的作用就是將request、response對象掛載到ctx的上面,讓koa實例和代碼能方便的使用到request、response對象中的方法。現在我們創建context.js文件,輸入如下代碼:
let proto = {}; function delegateSet(property, name) { proto.__defineSetter__(name, function (val) { this[property][name] = val; }); } function delegateGet(property, name) { proto.__defineGetter__(name, function () { return this[property][name]; }); } let requestSet = []; let requestGet = ["query"]; let responseSet = ["body", "status"]; let responseGet = responseSet; requestSet.forEach(ele => { delegateSet("request", ele); }); requestGet.forEach(ele => { delegateGet("request", ele); }); responseSet.forEach(ele => { delegateSet("response", ele); }); responseGet.forEach(ele => { delegateGet("response", ele); }); module.exports = proto;
? ? ? ? context.js文件主要是對常用的request和response方法進行掛載和代理,通過context.query直接代理了context.request.query,context.body和context.status代理了context.response.body與context.response.status。而context.request,context.response則會在application.js中掛載。
? ? ? ? 本來可以用簡單的setter和getter去設置每一個方法,但是由于context對象定義方法比較簡單和規范,在koa源碼里可以看到,koa源碼用的是__defineSetter__和__defineSetter__來代替setter/getter每一個屬性的讀取設置,這樣做主要是方便拓展和精簡了寫法,當我們需要代理更多的res和req的方法的時候,可以向context.js文件里面的數組對象里面添加對應的方法名和屬性名即可。
? ? ? ? 目前為止,我們已經得到了request、response、context三個模塊對象了,接下來就是將request、response所有方法掛載到context下,讓context實現它的承上啟下的作用,修改application.js文件,添加如下代碼:
let http = require("http"); let context = require("./context"); let request = require("./request"); let response = require("./response"); createContext(req, res) { let ctx = Object.create(this.context); ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; }
? ? ? ? 可以看到,我們添加了createContext這個方法,這個方法是關鍵,它通過Object.create創建了ctx,并將request和response掛載到了ctx上面,將原生的req和res掛載到了ctx的子屬性上,往回看一下context/request/response.js文件,就能知道當時使用的this.res或者this.response之類的是從哪里來的了,原來是在這個createContext方法中掛載到了對應的實例上,構建了運行時上下文ctx之后,我們的app.use回調函數參數就都基于ctx了。
模塊三:中間件機制和剝洋蔥模型的實現? ? ? ? 目前為止我們已經成功實現了上下文context對象、 請求request對象和響應response對象模塊,還差一個最重要的模塊,就是koa的中間件模塊,koa的中間件機制是一個剝洋蔥式的模型,多個中間件通過use放進一個數組隊列然后從外層開始執行,遇到next后進入隊列中的下一個中間件,所有中間件執行完后開始回幀,執行隊列中之前中間件中未執行的代碼部分,這就是剝洋蔥模型,koa的中間件機制。
? ? ? ? koa的剝洋蔥模型在koa1中使用的是generator + co.js去實現的,koa2則使用了async/await + Promise去實現的,接下來我們基于async/await + Promise去實現koa2中的中間件機制。首先,假設當koa的中間件機制已經做好了,那么它是能成功運行下面代碼的:
let Koa = require("../src/application"); let app = new Koa(); app.use(async (ctx, next) => { console.log(1); await next(); console.log(6); }); app.use(async (ctx, next) => { console.log(2); await next(); console.log(5); }); app.use(async (ctx, next) => { console.log(3); ctx.body = "hello world"; console.log(4); }); app.listen(3000, () => { console.log("listenning on 3000"); });
? ? ? ? 運行成功后會在終端輸出123456,那就能驗證我們的koa的剝洋蔥模型是正確的。接下來我們開始實現,修改application.js文件,添加如下代碼: ? ? ?
compose() { return async ctx => { function createNext(middleware, oldNext) { return async () => { await middleware(ctx, oldNext); } } let len = this.middlewares.length; let next = async () => { return Promise.resolve(); }; for (let i = len - 1; i >= 0; i--) { let currentMiddleware = this.middlewares[i]; next = createNext(currentMiddleware, next); } await next(); }; } callback() { return (req, res) => { let ctx = this.createContext(req, res); let respond = () => this.responseBody(ctx); let onerror = (err) => this.onerror(err, ctx); let fn = this.compose(); return fn(ctx); }; }
? ? ? ? koa通過use函數,把所有的中間件push到一個內部數組隊列this.middlewares中,剝洋蔥模型能讓所有的中間件依次執行,每次執行完一個中間件,遇到next()就會將控制權傳遞到下一個中間件,下一個中間件的next參數,剝洋蔥模型的最關鍵代碼是compose這個函數: ? ? ?
compose() { return async ctx => { function createNext(middleware, oldNext) { return async () => { await middleware(ctx, oldNext); } } let len = this.middlewares.length; let next = async () => { return Promise.resolve(); }; for (let i = len - 1; i >= 0; i--) { let currentMiddleware = this.middlewares[i]; next = createNext(currentMiddleware, next); } await next(); }; }
? ? ? ? createNext函數的作用就是將上一個中間件的next當做參數傳給下一個中間件,并且將上下文ctx綁定當前中間件,當中間件執行完,調用next()的時候,其實就是去執行下一個中間件。
for (let i = len - 1; i >= 0; i--) { let currentMiddleware = this.middlewares[i]; next = createNext(currentMiddleware, next); }
? ? ? ? 上面這段代碼其實就是一個鏈式反向遞歸模型的實現,i是從最大數開始循環的,將中間件從最后一個開始封裝,每一次都是將自己的執行函數封裝成next當做上一個中間件的next參數,這樣當循環到第一個中間件的時候,只需要執行一次next(),就能鏈式的遞歸調用所有中間件,這個就是koa剝洋蔥的核心代碼機制。
? ? ? ? 到這里我們總結一下上面所有剝洋蔥模型代碼的流程,通過use傳進來的中間件是一個回調函數,回調函數的參數是ctx上下文和next,next其實就是控制權的交接棒,next的作用是停止運行當前中間件,將控制權交給下一個中間件,執行下一個中間件的next()之前的代碼,當下一個中間件運行的代碼遇到了next(),又會將代碼執行權交給下下個中間件,當執行到最后一個中間件的時候,控制權發生反轉,開始回頭去執行之前所有中間件中剩下未執行的代碼,這整個流程有點像一個偽遞歸,當最終所有中間件全部執行完后,會返回一個Promise對象,因為我們的compose函數返回的是一個async的函數,async函數執行完后會返回一個Promise,這樣我們就能將所有的中間件異步執行同步化,通過then就可以執行響應函數和錯誤處理函數。
? ? ? ? 當中間件機制代碼寫好了以后,運行我們的上面的例子,已經能輸出123456了,至此,我們的koa的基本框架已經基本做好了,不過一個框架不能只實現功能,為了框架和服務器實例的健壯,還需要加上錯誤處理機制。
模塊四:錯誤捕獲和錯誤處理? ? ? ? 要實現一個基礎框架,錯誤處理和捕獲必不可少,一個健壯的框架,必須保證在發生錯誤的時候,能夠捕獲到錯誤和拋出的異常,并反饋出來,將錯誤信息發送到監控系統上進行反饋,目前我們實現的簡易koa框架還沒有能實現這一點,我們接下加上錯誤處理和捕獲的機制。
throw new Error("oooops");
? ? ? ? 基于現在的框架,如果中間件代碼中出現如上錯誤異常拋出,是捕獲不到錯誤的,這時候我們看一下application.js中的callback函數的return返回代碼,如下:
return fn(ctx).then(respond);
? ? ? ? 可以看到,fn是中間件的執行函數,每一個中間件代碼都是由async包裹著的,而且中間件的執行函數compose返回的也是一個async函數,我們根據es7的規范知道,async返回的是一個promise的對象實例,我們如果想要捕獲promise的錯誤,只需要使用promise的catch方法,就可以把所有的中間件的異常全部捕獲到,修改后callback的返回代碼如下:
return fn(ctx).then(respond).catch(onerror);
? ? ? ? 現在我們已經實現了中間件的錯誤異常捕獲,但是我們還缺少框架層發生錯誤的捕獲機制,我們希望我們的服務器實例能有錯誤事件的監聽機制,通過on的監聽函數就能訂閱和監聽框架層面上的錯誤,實現這個機制不難,使用nodejs原生events模塊即可,events模塊給我們提供了事件監聽on函數和事件觸發emit行為函數,一個發射事件,一個負責接收事件,我們只需要將koa的構造函數繼承events模塊即可,構造后的偽代碼如下:
let EventEmitter = require("events"); class Application extends EventEmitter {}
? ? ? ? 繼承了events模塊后,當我們創建koa實例的時候,加上on監聽函數,代碼如下:
let app = new Koa(); app.on("error", err => { console.log("error happends: ", err.stack); });
? ? ? ? 這樣我們就實現了框架層面上的錯誤的捕獲和監聽機制了。總結一下,錯誤處理和捕獲,分中間件的錯誤處理捕獲和框架層的錯誤處理捕獲,中間件的錯誤處理用promise的catch,框架層面的錯誤處理用nodejs的原生模塊events,這樣我們就可以把一個服務器實例上的所有的錯誤異常全部捕獲到了。至此,我們就完整實現了一個輕量版的koa框架了。
結尾目前為止,我們已經實現了一個輕量版的koa框架了,我們實現了封裝node http server、創建Koa類構造函數、構造request、response、context對象、中間件機制和剝洋蔥模型的實現、錯誤捕獲和錯誤處理這四個大模塊,理解了這個輕量版koa的實現原理,再去看koa2的源碼,你就會發現一切都豁然開朗,koa2的源碼無非就是在這個輕量版基礎上加了很多工具函數和細節的處理,限于篇幅筆者就不再一一介紹了。
本文實現的輕量版koa的完整代碼gitlab地址為:article_koa2?
作者:第一名的小蝌蚪 github: 文章會第一時間分享在前端屌絲心路歷程,歡迎star或者watch,感恩?最后,TNFE團隊為前端開發人員整理出了小程序以及web前端技術領域的最新優質內容,每周更新?,歡迎star,github地址:https://github.com/Tnfe/TNFE-...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/53836.html
摘要:實現的四大模塊上文簡述了源碼的大體框架結構,接下來我們來實現一個的框架,筆者認為理解和實現一個框架需要實現四個大模塊,分別是封裝創建類構造函數構造對象中間件機制和剝洋蔥模型的實現錯誤捕獲和錯誤處理下面我們就逐一分析和實現。 什么是koa框架? ? ? ? ?koa是一個基于node實現的一個新的web框架,它是由express框架的原班人馬打造的。它的特點是優雅、簡潔、表達力強、自由度...
摘要:啟動流程主要的啟動流程就是下面的步引入包實例化編寫中間件監聽服務器引入包引入包其實就是引入的一個繼承于原生的類的類其中就包含了等原型方法實例化執行,將等對象封裝在實例中編寫中間件首先判斷的類型,不是方法直接拋錯是生成器函數的話用封裝是函數 啟動流程 koa 主要的啟動流程就是下面的 4 步:引入 koa 包 => 實例化 koa => 編寫中間件 => 監聽服務器 const koa ...
摘要:使用的中間件是一個簡潔的框架,把許多小功能都拆分成了中間件,用一個洋蔥模型保證了中間件豐富的可拓展性,我們要使用來保持登錄狀態,就需要引用中間件。默認是過期時間,以毫秒為單位計算。自動提交到響應頭。默認是是否在快過期時刷新的有效期。 項目要用到登錄注冊,就需要使用到Cookie和Session來保持登錄狀態,于是就簡單研究了一下 Cookie和Session的工作原理 前面已經專門發過...
摘要:云集一線大廠有真正實力的程序員團隊云集一線大廠經驗豐厚的碼農,開源奉獻各教程。融合多種常見的需求場景網絡請求解析模板引擎靜態資源日志記錄錯誤請求處理。結合語句中轉中間件控制權,解決回調地獄問題。注意分支中的目錄為當節課程后的完整代碼。 ?? ?與眾不同的學習方式,為你打開新的編程視角 獨特的『同步學習』方式 文案講解+視頻演示,文字可激發深層的思考、視頻可還原實戰操作過程。 云...
閱讀 1961·2021-09-09 09:33
閱讀 1107·2019-08-30 15:43
閱讀 2646·2019-08-30 13:45
閱讀 3297·2019-08-29 11:00
閱讀 845·2019-08-26 14:01
閱讀 3558·2019-08-26 13:24
閱讀 471·2019-08-26 11:56
閱讀 2683·2019-08-26 10:27