深入解析koa之中間件流程控制
前言
koa被認(rèn)為是第二代web后端開發(fā)框架,相比于前代express而言,其最大的特色無疑就是解決了回調(diào)金字塔的問題,讓異步的寫法更加的簡潔。在使用koa的過程中,其實(shí)一直比較好奇koa內(nèi)部的實(shí)現(xiàn)機(jī)理。最近終于有空,比較深入的研究了一下koa一些原理,在這里會(huì)寫一系列文章來記錄一下我的學(xué)習(xí)心得和理解。
在我看來,koa最核心的函數(shù)是大名鼎鼎的co,koa正是基于這個(gè)函數(shù)實(shí)現(xiàn)了異步回調(diào)同步化,以及中間件流程控制。當(dāng)然在這篇文章中我并不會(huì)去分析co源碼,我打算在整個(gè)系列文章中,一步一步講解如何實(shí)現(xiàn)koa中間件的流程控制原理,koa的異步回調(diào)同步寫法實(shí)現(xiàn)原理,最后在理解這些的基礎(chǔ)上,實(shí)現(xiàn)一個(gè)簡單的類似co的函數(shù)。
本篇首先只談一談koa的中間件流程控制原理。
1. koa中間件執(zhí)行流程
關(guān)于koa中間件如何執(zhí)行,官網(wǎng)上有一個(gè)非常經(jīng)典的例子,有興趣的可以去看看,不過這里,我想把它修改的更簡單一點(diǎn):
var koa = require('koa'); var app = koa(); app.use(function*(next) { console.log('begin middleware 1'); yield next; console.log('end middleware 1'); }); app.use(function*(next) { console.log('begin middleware 2'); yield next; console.log('end middleware 2'); }); app.use(function*() { console.log('middleware 3'); }); app.listen(3000);
運(yùn)行這個(gè)例子,然后使用curl工具,運(yùn)行:
curl http://localhost:3000
可以看到,運(yùn)行之后,會(huì)輸出:
begin middleware 1 begin middleware 2 middleware 3 end middleware 2 end middleware 1
這個(gè)例子非常形象的代表了koa的中間件執(zhí)行機(jī)制,可以用下圖的洋蔥模型來形容:
通過這種執(zhí)行流程,開發(fā)者可以非常方便的開發(fā)一些中間件,并且非常容易的整合到實(shí)際業(yè)務(wù)流程中。那么,這樣的流程又是如何實(shí)現(xiàn)和控制的呢?
2. koa中的generator和compose
簡單來說,洋蔥模型的執(zhí)行流程是通過es6中的generator來實(shí)現(xiàn)的。不熟悉generator的同學(xué)可以去看看其特性,其中一個(gè)就是generator函數(shù)可以像打斷點(diǎn)一樣從函數(shù)某個(gè)地方跳出,之后還可以再回來繼續(xù)執(zhí)行。下面一個(gè)例子可以說明這種特性:
var gen=function*(){ console.log('begin!'); //yield語句,在這里跳出,將控制權(quán)交給anotherfunc函數(shù)。 yield anotherfunc; //下次回來時(shí)候從這里開始執(zhí)行 console.log('end!'); } var anotherfunc(){ console.log('this is another function!'); } var g=gen(); var another=g.next(); //'begin!' //another是一個(gè)對(duì)象,其中value成員就是返回的anotherfunc函數(shù) another.value(); //'this is another function!' g.next(); //'end!';
從這個(gè)簡單例子中,可以看出洋蔥模型最基本的一個(gè)雛形,即yield前后的語句最先和最后執(zhí)行,yield中間的代碼在中心執(zhí)行。
現(xiàn)在設(shè)想一下,如果yield后面跟的函數(shù)本身就又是一個(gè)generator,會(huì)怎么樣呢?其實(shí)就是從上面例子里面做一個(gè)引申:
var gen1=function*(){ console.log('begin!'); yield g2; console.log('end!'); } var gen2=function*(){ console.log('begin 2'); yield anotherfunc; console.log('end 2'); } var anotherfunc(){ console.log('this is another function!'); } var g=gen(); var g2=gen2(); var another1=g.next(); //'begin!'; var another2=another1.value.next(); //'begin 2'; another2.value(); //'this is another function!'; another1.value.next(); //'end 2'; g.next(); //'end!';
可以看出,基本上是用上面的例子,再加一個(gè)嵌套而已,原理是一樣的。
而在koa中,每個(gè)中間件generator都有一個(gè)next參數(shù)。在我們這個(gè)例子中,g2就可以看成是g函數(shù)的next參數(shù)。事實(shí)上,koa也確實(shí)是這樣做的,當(dāng)使用app.use()掛載了所有中間件之后,koa有一個(gè)koa-compose模塊,用于將所有g(shù)enerator中間件串聯(lián)起來,基本上就是將后一個(gè)generator賦給前一個(gè)generator的next參數(shù)。koa-compose的源碼非常簡單短小,下面是我自己實(shí)現(xiàn)的一個(gè):
function compose(middlewares) { return function(next) { var i = middlewares.length; var next = function*() {}(); while (i--) { next = middlewares[i].call(this, next); } return next; } }
使用我們自己寫的compose對(duì)上面一個(gè)例子改造,是的其更接近koa的形式:
function compose(middlewares) { return function(next) { var i = middlewares.length; var next = function*() {}(); while (i--) { next = middlewares[i].call(this, next); } return next; } } var gen1=function*(next){ console.log('begin!'); yield next; console.log('end!'); } var gen2=function*(next){ console.log('begin 2'); yield next; console.log('end 2'); } var gen3=function*(next){ console.log('this is another function!'); } var bundle=compose([gen1,gen2,gen3]); var g=bundle(); var another1=g.next(); //'begin!'; var another2=another1.value.next(); //'begin 2'; another2.value.next(); //'this is another function!'; another1.value.next(); //'end 2'; g.next(); //'end!';
怎么樣?是不是有一點(diǎn)koa中間件寫法的感覺了呢?但是目前,我們還是一步一步手動(dòng)的在執(zhí)行我們這個(gè)洋蔥模型,能否寫一個(gè)函數(shù),自動(dòng)的來執(zhí)行我們這個(gè)模型呢?
3. 讓洋蔥模型自動(dòng)跑起來:一個(gè)run函數(shù)的編寫
上面例子中,最后的代碼我們可以看出一個(gè)規(guī)律,基本就是外層的generator調(diào)用next方法把控制權(quán)交給內(nèi)層,內(nèi)層再繼續(xù)調(diào)用next把方法交給更里面的一層。整個(gè)流程可以用一個(gè)函數(shù)嵌套的寫法寫出來。話不多說,直接上代碼:
function run(gen) { var g; if (typeof gen.next === 'function') { g = gen; } else { g = gen(); } function next() { var tmp = g.next(); //如果tmp.done為true,那么證明generator執(zhí)行結(jié)束,返回。 if (tmp.done) { return; } else if (typeof g.next === 'function') { run(tmp.value); next(); } } next(); } function compose(middlewares) { return function(next) { var i = middlewares.length; var next = function*() {}(); while (i--) { next = middlewares[i].call(this, next); } return next; } } var gen1 = function*(next) { console.log('begin!'); yield next; console.log('end!'); } var gen2 = function*(next) { console.log('begin 2'); yield next; console.log('end 2'); } var gen3 = function*(next) { console.log('this is another function!'); } var bundle = compose([gen1, gen2, gen3]); run(bundle);
run函數(shù)接受一個(gè)generator,其內(nèi)部執(zhí)行其實(shí)就是我們上一個(gè)例子的精簡,使用遞歸的方法執(zhí)行。運(yùn)行這個(gè)例子,可以看到結(jié)果和我們上一個(gè)例子相同。
到此為止,我們就基本講清楚了koa中的中間件洋蔥模型是如何自動(dòng)執(zhí)行的。事實(shí)上,koa中使用的co函數(shù),一部分功能就是實(shí)現(xiàn)我們這里編寫的run函數(shù)的功能。
值得注意的是,這篇文章只注重分析中間件執(zhí)行流程的實(shí)現(xiàn),暫時(shí)并沒有考慮異步回調(diào)同步化原理。下一篇文章中,我將帶大家繼續(xù)探析koa中異步回調(diào)同步化寫法的機(jī)理。
這篇文章的代碼可以在github上面找到:https://github.com/mly-zju/async-js-demo,其中process_control.js文件就是本篇的事例源碼。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Nodejs 獲取時(shí)間加手機(jī)標(biāo)識(shí)的32位標(biāo)識(shí)實(shí)現(xiàn)代碼
本文給大家分享nodejs獲取時(shí)間加手機(jī)標(biāo)識(shí)的32位標(biāo)識(shí)實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2017-03-03Nodejs讀取文件時(shí)相對(duì)路徑的正確寫法(使用fs模塊)
本篇文章主要介紹了linux 環(huán)境 mysql寫入中文報(bào)錯(cuò)的相關(guān)知識(shí)。具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-04-04Node.js API詳解之 querystring用法實(shí)例分析
這篇文章主要介紹了Node.js API詳解之 querystring用法,結(jié)合實(shí)例形式分析了Node.js API中querystring的基本功能、用法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2020-04-04nodejs 整合kindEditor實(shí)現(xiàn)圖片上傳
這篇文章主要介紹了nodejs 整合kindEditor實(shí)現(xiàn)圖片上傳,需要的朋友可以參考下2015-02-02nodejs的HTML分析利器node-jquery用法淺析
這篇文章主要介紹了nodejs的HTML分析利器node-jquery用法,簡單分析了node-jquery的功能并結(jié)合實(shí)例說明了node-jquery控制臺(tái)輸出信息的操作技巧,需要的朋友可以參考下2016-11-11Node在Controller層進(jìn)行數(shù)據(jù)校驗(yàn)的過程詳解
這篇文章主要給大家介紹了關(guān)于Node在Controller層進(jìn)行數(shù)據(jù)校驗(yàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Nodejs學(xué)習(xí)筆記之測(cè)試驅(qū)動(dòng)
本文是本系列文章的第二篇,主要是測(cè)試針對(duì)于web后端的驅(qū)動(dòng),在開發(fā)過程中,在開發(fā)完成一段代碼后如果負(fù)責(zé)任而不是說完全把問題交給測(cè)試人員去發(fā)現(xiàn)的話,這個(gè)時(shí)候通常都會(huì)去做一些手動(dòng)的測(cè)試。2015-04-04