摘要:三中間件實現原理首先需要明確是中間件并不是中的概念,它只是和框架衍生的概念。中間件的執行流程主要由與函數決定依次取出中間件終止條件路由匹配規則函數中使用閉包函數來檢測是否與當前路由相匹配,匹配則執行該上的中間件函數,否則繼續檢查下一個。
Koa作為下一代Web開發框架,不僅讓我們體驗到了async/await語法帶來同步方式書寫異步代碼的酸爽,而且本身簡潔的特點,更加利于開發者結合業務本身進行擴展。
??本文從以下幾個方面解讀Koa源碼:
封裝創建應用程序函數
擴展res和req
中間件實現原理
異常處理
一、封裝創建應用程序函數??利用NodeJS可以很容易編寫一個簡單的應用程序:
const http = require("http") const server = http.createServer((req, res) => { // 每一次請求處理的方法 console.log(req.url) res.writeHead(200, { "Content-Type": "text/plain" }) res.end("Hello NodeJS") }) server.listen(8080)
注意:當瀏覽器發送請求時,會附帶請求/favicon.ico。
??而Koa在封裝創建應用程序的方法中主要執行了以下流程:
組織中間件(監聽請求之前)
生成context上下文對象
執行中間件
執行默認響應方法或者異常處理方法
// application.js listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } callback() { // 組織中間件 const fn = compose(this.middleware); // 未監聽異常處理,則采用默認的異常處理方法 if (!this.listenerCount("error")) this.on("error", this.onerror); const handleRequest = (req, res) => { // 生成context上下文對象 const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; // 默認狀態碼為404 res.statusCode = 404; // 中間件執行完畢之后 采用默認的 錯誤 與 成功 的處理方式 const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }二、擴展res和req
??首先我們要知道NodeJS中的res和req是http.IncomingMessage和http.ServerResponse的實例,那么就可以在NodeJS中這樣擴展req和res:
Object.defineProperties(http.IncomingMessage.prototype, { query: { get () { return querystring.parse(url.parse(this.url).query) } } }) Object.defineProperties(http.ServerResponse.prototype, { json: { value: function (obj) { if (typeof obj === "object") { obj = JSON.stringify(obj) } this.end(obj) } } })
??而Koa中則是自定義request和response對象,然后保持對res和req的引用,最后通過getter和setter方法實現擴展。
// application.js 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; // 保存原生req對象 context.res = request.res = response.res = res; // 保存原生res對象 request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.state = {}; // 最終返回完整的context上下文對象 return context; }
??所以在Koa中要區別這兩組對象:
request、response: Koa擴展的對象
res、req: NodeJS原生對象
// request.js get header() { return this.req.headers; }, set header(val) { this.req.headers = val; },
??此時已經可以采用這樣的方式訪問header屬性:
ctx.request.header
??但是為了方便開發者調用這些屬性和方法,Koa將response和request中的屬性和方法代理到context上。
??通過Object.defineProperty可以輕松的實現屬性的代理:
function access (proto, target, name) { Object.defineProperty(proto, name, { get () { return target[name] }, set (value) { target[name] = value } }) } access(context, request, "header")
??而對于方法的代理,則需要注意this的指向:
function method (proto, target, name) { proto[name] = function () { return target[name].apply(target, arguments) } }
??上述就是屬性代理和方法代理的核心代碼,這基本算是一個常用的套路。
??代理這部分詳細的源碼,可以查看node-delegates, 不過這個包時間久遠,有一些老方法已經廢除。
?? 在上述過程的源碼中涉及到很多JavaScript的基礎知識,例如:原型繼承、this的指向。對于基礎薄弱的同學,還需要先弄懂這些基礎知識。
三、中間件實現原理?? 首先需要明確是:中間件并不是NodeJS中的概念,它只是connect、express和koa框架衍生的概念。
??在connect中,開發者可以通過use方法注冊中間件:
function use(route, fn) { var handle = fn; var path = route; // 不傳入route則默認為"/",這種基本是框架處理參數的一種套路 if (typeof route !== "string") { handle = route; path = "/"; } ... // 存儲中間件 this.stack.push({ route: path, handle: handle }); // 以便鏈式調用 return this; }
??use方法內部獲取到中間件的路由信息(默認為"/")和中間件的處理函數之后,構建成layer對象,然后將其存儲在一個隊列當中,也就是上述代碼中的stack。
??connect中間件的執行流程主要由handle與call函數決定:
function handle(req, res, out) { var index = 0; var stack = this.stack; ... function next(err) { ... // 依次取出中間件 var layer = stack[index++] // 終止條件 if (!layer) { defer(done, err); return; } var path = parseUrl(req).pathname || "/"; var route = layer.route; // 路由匹配規則 if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) { return next(err); } ... call(layer.handle, route, err, req, res, next); } next(); }
??handle函數中使用閉包函數next來檢測layer是否與當前路由相匹配,匹配則執行該layer上的中間件函數,否則繼續檢查下一個layer。
??這里需要注意next中檢查路由的方式可能與想象中的不太一樣,所以默認路由為"/"的中間件會在每一次請求處理中都執行。
function call(handle, route, err, req, res, next) { var arity = handle.length; var error = err; var hasError = Boolean(err); try { if (hasError && arity === 4) { // 錯誤處理中間件 handle(err, req, res, next); return; } else if (!hasError && arity < 4) { // 請求處理中間件 handle(req, res, next); return; } } catch (e) { // 記錄錯誤 error = e; } // 將錯誤傳遞下去 next(error); }
??在通過call方法執行中間件方法的時候,采用try/catch捕獲錯誤,這里有一個特別需要注意的地方是,call內部會根據是否存在錯誤以及中間件函數的參數決定是否執行錯誤處理中間件。并且一旦捕獲到錯誤,next方法會將錯誤傳遞下去,所以接下來普通的請求處理中間件即使通過了next中的路由匹配,仍然會被call方法給過濾掉。
??下面是layer的處理流程圖:
??上述就是connect中間件設計的核心要點,總結起來有如下幾點:
通過use方法注冊中間件;
中間件的順序執行是通過next方法銜接的并且需要手動調用,在next中會進行路由匹配,從而過濾掉部分中間件;
當中間件的執行過程中發生異常,則next會攜帶異常過濾掉非錯誤處理中間件,也是為什么錯誤中間件會比其他中間件多一個error參數;
在請求處理的周期中,需要手動調用res.end()來結束響應;
??Koa中間件與connect中間件的設計有很大的差異:
Koa中間件的執行并不需要匹配路由,所以注冊的中間件每一次請求都會執行。(當然還是需要手動調用next);
Koa中通過繼承event,暴露error事件讓開發者自定義異常處理;
Koa中res.end由中間件執行完成之后自動調用,這樣避免在connect忘記調用res.end導致用戶得不到任何反饋。
Koa中采用了async/await語法讓開發者利用同步的方式編寫異步代碼。
??當然,Koa中也是采用use方法注冊中間件,相比較connect省去路由匹配的處理,就顯得很簡潔:
use(fn) { this.middleware.push(fn); return this; }
??并且use支持鏈式調用。
??Koa中間件的執行流程主要通過koa-compose中的compose函數完成:
function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!") for (const fn of middleware) { if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!") } /** * @param {Object} context * @return {Promise} * @api public */ return function (context, next) { 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 if (!fn) return Promise.resolve() try { // 遞歸調用下一個中間件 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
??看到這里本質上connect與koa實現中間件的思想都是遞歸,不難看出koa相比較connect實現得更加簡潔,主要原因在于:
connect中提供路由匹配的功能,而Koa中則是相當于connect中默認的"/"路徑。
connect在捕獲中間件的異常時,通過next攜帶error一個個中間件驗證,直到錯誤處理中間件,而Koa中則是用Promise包裝中間件,一旦中間件發生異常,那么會直接觸發reject狀態,直接在Promise的catch中處理就行。
??上述就是connect中間件與Koa中間件的實現原理,現在在再看Koa中間件的這張執行流程圖,應該沒有什么疑問了吧?!
四、異常處理??對于同步代碼,通過try/catch可以輕松的捕獲異常,在connect中間件的異常捕獲則是通過try/catch完成。
??對于異步代碼,try/catch則無法捕獲,這時候一般可以構造Promise鏈,在最后的catch方法中捕獲錯誤,Koa就是這樣處理,并且在catch方法中發送error事件,以便開發者自定義異常處理邏輯。
this.app.emit("error", err, this);
??前面也談到Koa利用async/await語法帶來同步方式書寫異步代碼的酸爽,另外也讓錯誤處理更加自然:
// 也可以這樣自定義錯誤處理 app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.status || 500 ctx.body = err } })五、總結
??相信看到這里,再回憶一下之前遇到的那些問題,你應該會有新的理解,并且再次使用Koa時會更加得心應手,這也是分析Koa源碼的目的之一。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100520.html
摘要:主要通過處理二進制數據流,但是它并不支持字符編碼方式,需要通過模塊進行處理。最后留圖一張往期精彩回顧玩轉原理解析玩轉核心原理分析 一、前置知識 ??在理解koa-bodyparser原理之前,首先需要了解部分HTTP相關的知識。 1、報文主體 ??HTTP報文主要分為請求報文和響應報文,koa-bodyparser主要針對請求報文的處理。 ??請求報文主要由以下三個部分組成: 報文頭...
摘要:搭建一個的腳手架并不困難,但就如其他體力活一樣,我們并不想重復勞動其他腳手架生成出來的項目目錄結構可能并不是我們想要的一些基于和類型的框架型項目集成了等重量級組件,當然,這本身無可厚非,大家的定位不一樣我希望只通過一個最小化的容器去 Github Repo: https://github.com/qddegtya/let-us-koa showImg(https://segmentfa...
摘要:四路由注冊構造函數首先看了解一下構造函數限制必須采用關鍵字服務器支持的請求方法,后續方法會用到保存前置處理函數存儲在構造函數中初始化的和屬性最為重要,前者用來保存前置處理函數,后者用來保存實例化的對象。 一、前言 ??Koa為了保持自身的簡潔,并沒有捆綁中間件。但是在實際的開發中,我們需要和形形色色的中間件打交道,本文將要分析的是經常用到的路由中間件 -- koa-router。 ??...
摘要:感謝大神的免費的計算機編程類中文書籍收錄并推薦地址,以后在倉庫里更新地址,聲音版全文狼叔如何正確的學習簡介現在,越來越多的科技公司和開發者開始使用開發各種應用。 說明 2017-12-14 我發了一篇文章《沒用過Node.js,就別瞎逼逼》是因為有人在知乎上黑Node.js。那篇文章的反響還是相當不錯的,甚至連著名的hax賀老都很認同,下班時讀那篇文章,竟然坐車的還坐過站了。大家可以很...
摘要:感謝大神的免費的計算機編程類中文書籍收錄并推薦地址,以后在倉庫里更新地址,聲音版全文狼叔如何正確的學習簡介現在,越來越多的科技公司和開發者開始使用開發各種應用。 說明 2017-12-14 我發了一篇文章《沒用過Node.js,就別瞎逼逼》是因為有人在知乎上黑Node.js。那篇文章的反響還是相當不錯的,甚至連著名的hax賀老都很認同,下班時讀那篇文章,竟然坐車的還坐過站了。大家可以很...
閱讀 3054·2021-11-11 16:55
閱讀 3170·2021-10-18 13:34
閱讀 592·2021-10-14 09:42
閱讀 1642·2021-09-03 10:30
閱讀 848·2021-08-05 10:02
閱讀 970·2019-08-30 11:27
閱讀 3484·2019-08-29 15:14
閱讀 1254·2019-08-29 13:02