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