中間件在路由與控制器中的應(yīng)用中間件是什么?在傳統(tǒng)框架的年代,很少會有中間件這個概念。我最早接觸這個概念其實(shí)是在學(xué)習(xí) MySQL 的時(shí)候,了解過 MyCat 這類的組件也被稱為中間件。既然是中間,那么它就是一個夾在應(yīng)用和調(diào)用中間的東西。我們還是以請求為例,一個請求要經(jīng)過接收、處理、返回這三個過程,而中間件,就可以看作是夾在這三個操作中間的一些操作。比如說,我們的請求發(fā)過來,在沒有到達(dá)路由或者控制器的時(shí)候,就可以通過中間件做一些預(yù)判,像參數(shù)合法不合法、登錄狀態(tài)的判斷之類的。就像我們用 Laravel 做業(yè)務(wù)開發(fā)的時(shí)候,經(jīng)常需要自己寫的的中間件就是處理登錄信息和解決跨域問題的中間件(Laravel8有自己的跨域組件了)。 在之前學(xué)習(xí) Node.js 的時(shí)候,express 框架中也是有中間件這個東西的,而且概念和 Laravel 的中間件是完全相同的?,F(xiàn)在,這種中間件技術(shù)也已經(jīng)是各種現(xiàn)代化框架的必備功能之一了。在 TP3 的時(shí)候,其實(shí)那幾個勾子方法也可以視為是中間件的一種,只不過它們是請求已經(jīng)到達(dá)控制器了,但在調(diào)用具體的控制器方法之前,預(yù)埋了一些勾子函數(shù)而已,關(guān)于勾子函數(shù)的相關(guān)知識可以參考 【PHP設(shè)計(jì)模式-模板方法模式】https://mp.weixin.qq.com/s/2sX1ASQpnMybJ2xFqRR3Ig 。 好了,不扯遠(yuǎn)了,我們直接來看看中間件在 Laravel 中,是如何使用的。 定義中間件創(chuàng)建一個中間件也是可以通過命令行的。
通過這個命令,我們會發(fā)現(xiàn)在 app/Http/Middleware 這個目錄下就創(chuàng)建了一個名為 MiddlewareTest.php 的文件。這就是一個中間件文件,當(dāng)然,你也可以自己創(chuàng)建,只需要將創(chuàng)建的文件放到這個目錄下就可以了。同時(shí),在這個目錄里面,我們還能看到許多系統(tǒng)已經(jīng)為我們準(zhǔn)備好的中間件。一會兒我們將拿其中的一兩個來學(xué)習(xí),不過在此之前,我們還是先看看這個自動生成的 MiddlewareTest.php 文件里有什么內(nèi)容吧。
貌似有點(diǎn)簡單啊,就一個 handle() 方法,然后有兩個參數(shù),一個是 Request ,另一個是閉包類型的 next 參數(shù)。Request 就不多說了,之前的文章中已經(jīng)講過,這個 Request 是貫穿整個 Laravel 應(yīng)用的,所以在中間件中有也不稀奇。更主要的是,其實(shí)我們的中間件主要就是對于 請求 和 響應(yīng) 的中間操作,所以這個 Request 是非常重要的。 另外這個 next() 是什么鬼?怎么是一個閉包類型的參數(shù)?這里如果學(xué)習(xí)過之前我寫過的設(shè)計(jì)模式系列文章的同學(xué)一定不會陌生,想一想 責(zé)任鏈 這個模式,記不起來或者沒看過的朋友可以移步 【PHP設(shè)計(jì)模式之責(zé)任鏈模式】https://mp.weixin.qq.com/s/ZA9vyCEkEg9_KTll-Jkcqw 先學(xué)習(xí)一下再回來,相信你對中間件的基礎(chǔ)原理馬上就明白了。 好了,不賣關(guān)子,這個 next 其實(shí)就是在框架中形成的一個責(zé)任鏈,或者說是 管道 也可以,它們略有區(qū)別但大體本質(zhì)上還是相似的,就是讓請求像水一樣在一個管道中向下流,然后到達(dá)一個終點(diǎn)(比如控制器)之后,再換另一條管子流回來(也就是響應(yīng))。而這個 next 就是下一個要處理這個請求的節(jié)點(diǎn)。具體的內(nèi)容還是參考 責(zé)任鏈模式 的講解,因?yàn)樗鼈兊脑泶_實(shí)是相通的。 先不自己寫代碼,我們先看下框架為我們提供的中間件里面都寫了什么。首先我們看到的是上篇文章中提到過的預(yù)防 CSRF 攻擊的功能,它就是通過中間件來進(jìn)行判斷 _token 標(biāo)簽是否存在的。
app/Http/Middleware/VerifyCsrfToken.php 繼承的是 laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php ,也就是說源代碼是在框架底層的,所以我們直接進(jìn)入 laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php 來查看。它的里面也是有一個 handle() 也就是上面的這段代碼。 handle() 里面會讀取請求中是否有 _token 參數(shù)或者頭信息里是否有 X-CSRF-TOKEN 信息,取出來之后與 session 中的 _token 信息進(jìn)行比對。成功之后會在 if 條件內(nèi)部調(diào)用 next ,也就是通知后面的中間件或者其它管道節(jié)點(diǎn)繼續(xù)請求的處理。如果失敗的話,則會返回 CSRF token mismatch 的錯誤,請求也就中止了。相關(guān)的源碼都在 VerifyCsrfToken.php 中,這里就不一一展示了,大家可以自行查閱。 其它的默認(rèn)提供的中間件大家可以自己嘗試分析一下是干什么用的,怎么實(shí)現(xiàn)的,接下來我們就自己定義一下我們剛剛創(chuàng)建的這個中間件。就做一些簡單的功能。
咦,貌似和我們默認(rèn)提供的中間件有些不同,為什么我們不是直接返回 next() ,而是用一個變量接住了 next() 然后又做了一些操作之后再 return 呢?這個其實(shí)就是 后置中間件 的作用。 其實(shí)就像我們前面說的,前置中間件,就是在 next() 之前對請求進(jìn)行處理操作,比如我們這里給請求中新增加了一個字段。而后置中間件,則是在 next() 結(jié)束之后,管道回流的時(shí)候,可以對響應(yīng)進(jìn)行一些操作,比如我們?yōu)轫憫?yīng)增加了一個時(shí)間的輸出。當(dāng)然,一般情況下,響應(yīng)數(shù)據(jù)我們還是盡量在控制器那邊搞定,而后置中間件最大的好處是可以針對一次請求進(jìn)行完整的請求和響應(yīng)的日志記錄。不過這些還是以業(yè)務(wù)功能的需求為基礎(chǔ),大家只要知道有這個功能就可以了。 而前置中間件在業(yè)務(wù)開發(fā)中,我們使用得最多的其實(shí)是對于登錄鑒權(quán)的驗(yàn)證,比如用戶是否登錄,是否有權(quán)限,都可以在未到達(dá)控制器之前通過中間件進(jìn)行判斷,如果未登錄或者權(quán)限不夠就直接返回錯誤信息。就像 CSRF 的中間件一樣,如果沒有 _token 的話,根本到不了控制器,直接就會返回錯誤信息。 接下來,我們還要準(zhǔn)備一個控制器。
這個控制器非常簡單,我們只是將接收到的請求中的參數(shù)獲取并相加了一下。前面在中間件中我們看到如果有 a 參數(shù)的話,我們會復(fù)制一個 aa 參數(shù) 中間件和控制器我們準(zhǔn)備好了,接下來就是如何使用中間件了,分幾種情況,我們一個一個來說。 路由上使用中間件在路由上使用中間件非常簡單,我們只需要一個 middleware 方法就可以了。
是不是感覺有點(diǎn)簡單的過分了,現(xiàn)在我們就為這個路由指定了一個我們自己定義的中間件。注意,其它沒有寫的路由是不是走這個中間件的。也就是說,在路由中定義中間件,只有我們指定的路由才會執(zhí)行相應(yīng)的中間件代碼。 控制器里使用中間件在路由中配置中間件是最簡單也是最方便的做法,但如果我們說不想在路由中配置,比如說這個控制器里面的方法可能會定義多種路由,我們想讓所有定義的路由都可以走這個中間件的話,那么除了后面要講的全局配置中間件以外,我們還可以在某個控制器中定義要使用的中間件。
在上面的測試代碼中,我們使用的依然是和上面那個路由相同的控制器方法,只不過在這個路由上,我們沒有指定中間件,而是在控制器的代碼中,在 構(gòu)造函數(shù) 里面通過 middleware() 方法指定了中間件,這樣就可以讓這個控制器中的所有方法都去執(zhí)行指定的中間件內(nèi)容。我們再定義一個新的控制器方法并且指定一個沒有中間件的路由來測試。
可以看到對這個新的路由和控制器方法來說,中間件也是正常發(fā)揮作用的。 全局使用中間件上面說過的內(nèi)容,都是在某一個特定的情況下使用中間件,比如說指定的路由,或者是指定的控制器。Laravel 也為我們準(zhǔn)備了全局中間件定義的地方,全局的意思就很明顯了,所有的請求都會加上這個中間件。
我們只需要找到 App\Http\Kernel.php 文件,在其中的 middleware 變量中添加最后一行,也就是我們自定義的那個中間件就可以了。這樣,所有的請求都會走這個中間件。Kernel.php 是一個核心文件,我們繼續(xù)看它,會發(fā)現(xiàn)下面還有兩個變量,一個是 middlewareGroups ,一個是 routeMiddleware 。其實(shí)從名字就可以看出,middlewareGroups 是為中間件分組的,里面默認(rèn)定義了兩個中間件組,分別是 web 和 api 。其實(shí)他們對應(yīng)的就是路由文件夾下的 api.php 和 web.php 所要加載的中間件。在源代碼中,我們可以找到 app/Providers/RouteServiceProvider.php 這個文件,查看里面的 boot() 方法。
在這個 boot() 方法中,就可以看到,它定義了兩個路由,加載的分別是 routes 目錄下對應(yīng)的兩個文件,然后使用 middleware() 指定的中間件其實(shí)就是我們在中間件組中定義的那兩個中間件組。既然是組的概念,那么在組中的所有中間件都會在這兩個路由文件中被執(zhí)行。大家可以嘗試注釋掉 web 分組下面的 \App\Http\Middleware\VerifyCsrfToken::class 這個中間件,就會發(fā)現(xiàn) web.php 下的所有請求都不需要進(jìn)行 CSRF 驗(yàn)證了。 另外一個 routeMiddleware 的意思其實(shí)是給中間件起個別名,比如我們在這個變量中增加一個:
然后在路由中,直接在 middleware() 方法中使用這個定義的名稱就可以了。
中間件調(diào)用源碼分析中間件的核心用法就是上面的內(nèi)容了,其它的一些功能大家可以參考官方文檔來進(jìn)行學(xué)習(xí)。接下來,我們就進(jìn)入到中間件源碼的調(diào)用分析。其實(shí)在之前的文章和這篇文章的開頭就已經(jīng)說過了,中間件就是 責(zé)任鏈模式 的一個典型應(yīng)用。而在 Laravel 中,這個責(zé)任鏈又是以管道的形式實(shí)現(xiàn)的。 在執(zhí)行入口文件 public/index.php 時(shí),第一步就會來到 laravel/framework/src/Illuminate/Foundation/Http/Kernel.php 中,注意這個 Kernel.php 是源碼中的文件,也是整個 Laravel 框架的核心文件。它的構(gòu)造函數(shù)里面,就會調(diào)用一個 syncMiddlewareToRouter() 方法。
從方法名可以看出,這個方法的作用是給路由同步中間件,它就是把我們在 app/Http/Kernel.php 中定義的中間件數(shù)組放到路由對象 laravel/framework/src/Illuminate/Routing/Router.php 中。這個時(shí)候,中間件就已經(jīng)全部被讀取到了。接下來,在 index.php 中調(diào)用的 handle() 方法里面,會通過 sendRequestThroughRouter() 方法構(gòu)造路由管道。
Pipeline 就是一個管道,在 through() 中,我們會將默認(rèn)的全局中間件保存在 Pipeline 的 pipes 變量中,然后讓請求像水一樣在這個中間件管道中一路流下去。 上面是處理全局中間件,還記得在 Kernel.php 中我們會將中間件傳遞給路由對象嗎?接下來,就是在路由構(gòu)造完成之后,通過路由 Router.php 中的 runRouteWithinStack() 方法,構(gòu)造路由中間件相關(guān)的管道。
關(guān)于管道的分析,我們將在核心架構(gòu)相關(guān)的文章再次學(xué)習(xí),現(xiàn)在,你只需要知道這個水管已經(jīng)鋪好了,接下來就是把請求,也就是讓我們的水在管道中流動就可以了。中間件就是這個管道中的一個個的閥門,我們可以對水進(jìn)行過濾處理,也可以關(guān)掉閥門不讓水流過,也可以讓水再從另一個管道流回,發(fā)揮你的想象力吧。 總結(jié)關(guān)于中間件的內(nèi)容就是這些,使用的方法其實(shí)有這些就已經(jīng)足夠我們?nèi)粘5拈_發(fā)應(yīng)用了。對于源碼的分析并沒有太深入,因?yàn)樵偻伦叩脑捑褪枪艿老嚓P(guān)的實(shí)現(xiàn)了。因此,在這里我們只是簡單的指出了中間件在何時(shí)加載,在何時(shí)放到管道中而已,后續(xù)的內(nèi)容我們后面再說,不要心急,一口吃下熱豆腐可是會燙傷嘴的。意猶未盡的小伙伴不如自己調(diào)試一下,看看管道又是如何實(shí)現(xiàn)的吧,我們將在比較后期的內(nèi)容中才會再講到管道這一塊。 參考文檔: https:///docs/laravel/8.x/middleware/9366#b53cb2 |
|