ASP.Net請求處理機制初步探索之旅(5):ASP.Net MVC請求處理流程

發表於2015-03-19

開篇:上一篇我們瞭解了在WebForm模式下一個Page頁面的生命週期,它經歷了初始化Init、載入Load以及呈現Render三個重要階段,其中構造了頁面控制元件樹,並對頁面控制元件樹進行了大量的遞迴操作,最後將與模板結合生成的HTML返回給了瀏覽器。那麼,在ASP.NET MVC模式下,一個頁面的生命週期又經歷了哪些步湊呢?別急,本篇漫漫道來!

一、開放的ASP.NET MVC程式碼

  2009年,Microsoft推出了ASP.NET MVC,也將ASP.NET MVC專案作為開源專案推送到了開源社群中,至今時間也過去快6年了,ASP.NET MVC已經到了5.0的版本階段了。我們看到ASP.NET MVC從一個不完整的小孩長成一個日漸成熟的巨人,我們可以從開源社群找到ASP.NET MVC的原始碼,相比之前我們需要Reflector進行反編譯檢視,這次則輕鬆得多。

這裡我們選擇ASP.NET MVC 4的原始碼作為分析物件,我已經將其上傳到了網盤中,你可以通過下面這個地址進行下載:

傳送門:http://pan.baidu.com/s/1bnF8ZPt

下載完成後,開啟ASP.NET MVC 4的原始碼,你會看到如下解決方案:這裡我們主要關注System.Web.Mvc這個類庫專案

二、從MvcHandler.ProcessRequest開始

  從Part 3中我們知道了在請求處理管道中的第7個事件生成了MvcHandler,在第11和第12個事件之間呼叫了MvcHandler的ProcessRequest方法開始了ASP.NET MVC的處理響應之旅。那麼,我們就從MvcHandler的ProcessRequest方法開始檢視,一個ASP.NET MVC頁面是如何載入出來一個HTML頁的!

(1)Controller的啟用

  ①藉助HttpConetxtWrapper封裝HttpContext

可以看出,這裡通過了一個基於包裝器(又稱裝飾者)模式實現的一個HttpContextWrapper類對HttpContext進行了一個封裝,並呼叫過載的另一個ProcessRequest方法進行繼續處理。

PS:有關ASP.NET MVC中HttpContext, HttpContextBase, HttpContextWrapper三者之間的聯絡請參考:http://blog.csdn.net/sundacheng1989/article/details/10551091

  ②控制器工廠根據URL建立控制器

可以看出,這裡通過呼叫ProcessRequestInit方法將上下文物件傳入進行處理,然後返回生成的控制器例項以及控制器工廠。因此,我們轉入ProcessRequestInit方法看看:

在這個方法中,首先根據RouteData路由資料取得要請求的Controller名稱,然後取得ControllerFactory(控制器工廠)物件,通過ControllerFactory來建立指定名稱的控制器,最後將控制器作為out引數傳遞出去。

  ③呼叫控制器的Execute方法進入Action

具體實現了IController介面的Controller物件通過呼叫Excute方法開始執行具體的Action,那麼Action究竟又是怎樣被觸發的呢?

(2)Action的觸發

  ①從ControllerBase的Excute方法開始

首先,Controller並沒有實現IController介面,而是Controller的基類ControllerBase實現了IController介面;然後,ControllerBase中定義了一個抽象方法ExcuteCore,讓其子類去具體執行,這裡主要是讓Controller類物件執行這個方法。

  ②根據URL獲取Action名稱並準備觸發Action

首先,通過路由資料獲取Action名稱,例如請求URL為:http://xxx.com/Home/Index,這裡獲取的Action名稱即為Index。然後,通過ActionInvoker.InvokeAction去執行具體的Action。那麼問題來了,這個ActionInvoker又是啥東東?我們先看看這個介面的定義:

通過查閱資料,我們發現原來是一個叫做ControllerActionInvoker的類實現了IActionInvoker介面,那麼我們就去看看這個ControllerActionInvoker類吧。

  ③獲取Controller與Action的描述資訊和過濾器資訊

看到這裡,也許會有人問什麼是描述資訊?那麼看到我們在開發中經常給Controller或者Action新增的Attribute資訊也許就不會感到陌生了:例如我們給某個名為Index的Action新增了[HttpPost]或者[HttpGet]特性,在請求時需要通過HTTP報文請求方式來區分這兩個Action。

那麼,什麼又是過濾器資訊?首先,過濾器涉及到一個叫做AOP(面向切面程式設計)的概念,我們可以通過前面的請求處理管道進行理解,雖然我們的ASP.NET頁面請求處理部分只是其中一小部分,但是在這部分執行之前還經歷了許多事件,在這之後又經歷了許多事件,而這些事件都是可以自定義邏輯的,它們都可以叫做過濾器。ASP.NET MVC預設為我們提供了四種型別的過濾器(Filter),如下圖所示:

Filters

PS:對過濾器不熟悉的朋友可以看看我的另一篇對ASP.NET MVC基礎知識中的過濾器(Filter)的介紹:http://www.cnblogs.com/edisonchou/p/3932640.html

  ④獲取引數資訊並開始真正執行Action:Filter->Action->Filter

通過上面所獲取的各種描述資訊與過濾器資訊找到Action並獲取所需的引數,然後呼叫InvokeActionMethodWithFilters方法執行Action。因此,再轉到InvokeActionMethodWithFilters方法看看:

在這個方法中,首先將上下文物件、描述資訊、引數資訊傳入InvokeActionMethod方法中,得到了一個Result物件。這個Result物件又是什麼?轉到定義一看,原來不就是我們在開發中經常返回的ActionResult型別嗎?

那麼,在InvokeActionMethod方法中又是如何返回Result的呢?再次轉到定義看看:

在這個方法中,首先執行了指定的Action,然後獲得了一個returnValue返回值,通過傳入返回值建立具體型別的ActionResult作為方法的返回值。這裡需要注意的是,ActionResult是一個抽象類,像什麼JsonResult、EmptyResult、ViewResult等都是其子類,而這裡的CreateActionResult就是要建立其具體子類的例項並返回。

現在將目光返回到InvokeActionMethodWithFilters方法中,看到程式碼最後宣告瞭一個委託thunk,它是過濾器結合經過反轉之後再合併之前宣告的委託continuation之後的一個新委託(它所持有的委託鏈順序會協調一致),目的是為了完成AOP的效果,比如首先要執行Action執行之前的過濾器,才能執行Action方法。

  ⑤ActionResult閃亮登場:Filter->Result

現在回到InvokeAction這個主方法中,剛剛執行完Action之後將結果都儲存在了postActionContext中的Result中,現在繼續執行過濾器(比如:可以對剛剛的Action結果進行一些處理),目的也是為了完成AOP的效果,比如執行完Action之後,必須要執行Action結束後的過濾器業務邏輯方法。那麼,這裡又是進行了什麼操作呢?轉到InvokeActionResultWithFilters方法中去看看:

首先,判斷過濾器執行的序號是否已經到了最後,如果不是,則繼續遞迴執行本方法呼叫過濾器(這裡對應的過濾器是OnResultExecuting事件,即在Result被生成時之前進行觸發)。如果到了最後,則開始生成最終的ActionResult。看看這個InvokeActionResult方法,它是一個虛方法。

(3)View的呈現

我們知道ActionResult是一個抽象類,那麼這個InvokeActionResult應該是由其之類來實現。於是,我們找到ViewResult,但是其並未直接繼承於ActionResult,再找到其父類ViewResultBase,它則繼承了ActionResult。於是,我們來檢視它的ExecuteResult方法:

  ①約定大於配置的緣故

我們在日常開發中,總是被告知約定大於配置,View中的名字必須與Controller中Action的名字一致。在這了,我們知道了原因,可以看出,這裡就是國通URL來取得ViewName然後去查詢View的。

  ②找到ViewEngine檢視引擎並獲取ViewEngineResult

首先,我們瞭解一下什麼是ViewEngine檢視引擎:我們在ASP.NET MVC開發中一般會有兩個選擇,一個是aspx檢視引擎,另一個是ASP.NET MVC 3.0推出的Razor檢視引擎。Razor檢視引擎在減少程式碼冗餘、增強程式碼可讀性和Visual Studio智慧感知方面,都有著突出的優勢。因此,Razor一經推出就深受廣大ASP.Net開發者的喜愛。

這裡通過FindView方法獲取到具體的View物件,而FindView又是ViewResultBase的一個抽象方法。這時,我們需要到ViewResult中去看看這個FindView方法。

這裡通過在ViewEngineCollection檢視引擎集合中呼叫FindView方法返回一個ViewEngineResult物件,而View則作為屬性存在於這個ViewEngineResult物件之中。

  ③載入ViewData/TempData等資料生成ViewContext

這裡開始載入ViewData、TempData等資料生成ViewContext,可以在ViewContext的建構函式中看到如下程式碼:

現在知道我們在Action方法中定義的那些ViewData或者TempData是在哪裡被存入上下文了吧?

  ④開始Render:HTML頁面的呈現

ViewContext上下文物件已生成好,TextWriter已經拿到,現在就開始對View進行正式的呈現了,也就是返回給瀏覽器端請求的HTML。由於這裡View物件是一個實現了IView介面的類物件,於是我們找到RazorView,但是它並未直接實現IView介面,於是我們找到它的父類BuildManagerCompiledView 

首先,通過ViewPath獲取View的型別(Type),這裡也是通過BuildManger來完成的,每個cshtml都會被asp.net編譯成一個類。然後,通過反射生成了View的具體例項。最後,通過RendView方法進行下一步的呈現工作。RenderView是一個抽象方法,具體實現是在RazorView類或WebFormView類中。

在此方法中,首先將傳遞過來的例項轉換成了一個WebViewPage類的例項,然後將ViewContext、ViewData等資料賦給WebViewPage例項作為屬性,以便在View中獲取。然後,如果有開始頁則先執行開始頁。最後,將HttpContext、Page與Model物件封裝為一個WebPageContext物件傳入ExecutePageHierarchy方法中進行執行頁面的渲染。

首先,我們從字面上來看,Hierarchy代表層次,那麼方法名的意思大概是:根據層次執行頁面。那麼,什麼是頁面的層次?

在執行ExecutePageHierachy這個方法來渲染View時,這個方法裡面要完成相當多的工作,主要是ViewStart的執行,和Layout的執行。這裡的困難之處在於對於有Layout的頁面來說,Layout的內容是先輸出的,然後是RenderBody內的內容,最後還是Layout的內容。如果僅僅是這樣的話,只要初始化一個TextWriter,按部就班的往裡面寫東西就可以了,但是實際上,Layout並不能首先執行,而應該是View的程式碼先執行,這樣的話View就有可能進行必要的初始化,供Layout使用。例如我們有如下的一個View:

這個Layout的內容如下:

這樣可以在頁面顯示Code in View字樣。 但是反過來,如果試圖在View中顯示在Layout裡面的”Data from Layout” 則是行不通的,什麼也不會被顯示。所以RenderBody是先於Layout中其他程式碼執行的,這種Layout的結構稱為 Page Hierachy

在這樣的程式碼執行順序下,還要實現文字輸出的順序,因此asp.net mvc這裡的實現中就使用了棧,這個棧是OutputStack,裡面壓入了TextWriter。注意到這只是一個頁面的處理過程,一個頁面之中還會有Partial View 和 Action等,這些的處理方式都是一樣的,因此還需要一個棧來記錄處理到了哪個(子)頁面,因此還有一個棧,稱之為TemplateStack,裡面壓入的是PageContext,PageContext維護了view的必要資訊,比如Model之類的,當然也包括上面提到的OutputStack。有了上面的基本資訊,下面看程式碼,先看入口點:

這個方法中,第一步首先將pageContext入棧:PushContext

第二步判斷是否存在ViewStart檔案,如果有,就執行startPage.ExecutePageHierachy()。如果不存在,則直接執行ExecutePageHierachy()

這個方法就是將context壓棧,然後執行相應的view的程式碼,然後出棧。有了這些出入棧的操作,可以保證View的程式碼,也就是Execute的時候的writer是正確的。Execute中的方法除去PartialView,Action之類的,最終呼叫的是WebPageBase中的WriteLiteral方法:

這裡的Output屬性是:

在呼叫了Excute方法後,頁面上的HTML內容基本輸出完畢,至此View就渲染完畢了

第三步,pageContext出棧,主要是棧中的元素的清理工作。

三、一圖勝千言,總體上概覽

參考資料

致謝:本文參閱了大量園友的相關文章,向以下文章作者表示感謝!

(1)Darren Ji,《ASP.NET MVC請求處理管道宣告週期的19個關鍵環節》:http://www.cnblogs.com/darrenji/p/3795661.html

(2)初心不可忘,《綜述:ASP.NET MVC請求處理管道》:http://www.cnblogs.com/luguobin/archive/2013/03/15/2962458.html

(3)學而不思則罔,《ASP.NET Routing與MVC之二:請求如何啟用Controller與Action》:http://www.cnblogs.com/acejason/p/3886968.html

(4)王承偉,《ASP.NET MVC請求原理與原始碼分析》:http://bbs.itheima.com/thread-134340-1-1.html

(5)Ivony,《通過原始碼研究ASP.NET MVC中的Conroller和View》:http://www.cnblogs.com/Ivony/archive/2010/11/13/aspnet-mvc-by-source-1.html

(6)痞子一毛,《ASP.NET MVC請求處理圖解》:http://www.cnblogs.com/piziyimao/archive/2013/02/27/2935969.html

(7)蔣金楠,《ASP.NET MVC中的View是如何被呈現出來的》:http://www.cnblogs.com/artech/archive/2012/08/22/view-engine-01.html

(8)yinzixin,《深入ASP.NET MVC之七:ActionResult的執行》:http://www.cnblogs.com/yinzixin/archive/2012/12/05/2799459.html (一篇好文,值得閱讀)

相關文章