目前我主要使用.Net MVC框架进行网页创建,数据库是MSSQL Server。所以,我就用.NET MVC框架的web页面周期来说明页面的生命周期,但是我觉着其他MVC框架也是大同小异的。
本文主要分两个部分
一、.NET MVC的网页生命周期
二、普通ASP.NET的网页生命周期
一、.NET MVC的网页生命周期
ASP.NET MVC请求从开始到结束的每一个过程,在浏览器输入URL并敲击回车来请求一个ASP.Net MVC网站的页面之后发生的任何事情,都是页面的生命周期的一部分。
为什么需要关心这些?有两个原因。首先是因为ASP.NET MVC是一个扩展性非常强的框架。例如,我们可以插入不同的ViewEngine来控制网站内容呈现的方式。我们还可以定义控制器生成和分配到某个请求的方式。因为我想发掘任何ASP.NET MVC页面请求的扩展点,所以我要来探究请求过程中的一些步骤。
其次,如果你对测试驱动开发佷感兴趣,当为控制器写单元测试时,我们就必须理解控制器的依赖项。在写测试的时候,我们需要使用诸如Typemock Isolator或Rhino Mocks的Mock框架来模拟某些对象。如果不了解页面请求生命周期就不能进行有效的模拟。
生命周期步骤概览
当我们对ASP.NET MVC网站发出一个请求的时候,会发生5个主要步骤:
步骤1:创建RouteTable
当ASP.NET应用程序第一次启动的时候才会发生第一步。RouteTable把URL映射到Handler。
步骤2:UrlRoutingModule拦截请求
第二步在我们发起请求的时候发生。UrlRoutingModule拦截了每一个请求并且创建和执行合适的Handler。
步骤3:执行MvcHandler
MvcHandler创建了控制器,并且把控制器传入ControllerContext,然后执行控制器。
步骤4:执行控制器
控制器检测要执行的控制器方法,构建参数列表并且执行方法。
步骤5:调用RenderView方法
大多数情况下,控制器方法调用RenderView()来把内容呈现回浏览器。Controller.RenderView()方法把这个工作委托给某个ViewEngine来做。
现在让我们来详细研究每一个步骤:
步骤1:创建RouteTable
当我们请求普通ASP.NET应用程序页面的时候,对于每一个页面请求都会在磁盘上有这样一个页面。例如,如果我们请求一个叫做SomePage.aspx的页面,在WEB服务器上就会有一个叫做SomePage.aspx的页面。如果没有的话,会得到一个错误。
从技术角度说,ASP.NET页面代表一个类,并且不是普通类。ASP.NET页面是一个Handler。换句话说,ASP.NET页面实现了IhttpHandler接口并且有一个ProcessRequest()方法用于在请求页面的时候接受请求。ProcessRequest()方法负责生成内容并把它发回浏览器。
因此,普通ASP.NET应用程序的工作方式佷简单明了。我们请求页面,页面请求对应磁盘上的某个页面,这个页面执行ProcessRequest()方法并把内容发回浏览器。
ASP.NET MVC应用程序不是以这种方式工作的。当我们请求一个ASP.NET MVC应用程序的页面时,在磁盘上不存在对应请求的页面。而是,请求被路由转到一个叫做控制器的类上。控制器负责生成内容并把它发回浏览器。
当我们写普通ASP.NET应用程序的时候,会创建很多页面。在URL和页面之间总是一一对应进行映射。每一个页面请求对应相应的页面。
相反,当我们创建ASP.NET MVC应用程序的时候,创建的是一批控制器。使用控制器的优势是可以在URL和页面之间可以有多对一的映射。例如,所有如下的URL都可以映射到相同的控制器上。
http://MySite/Products/1 http://MySite/Products/2 http://MySite/Products/3
这些URL映射到一个控制器上,通过从URL中提取产品ID来显示正确的产品。这种控制器方式比传统的ASP.NET方式更灵活。控制器方式可以产品更显而易见的URL。
那么,某个页面请求是怎么路由到某个控制器上的呢?ASP.NET MVC应用程序有一个叫做路由表(Route Table)的东西。路由表映射某个URL到某个控制器上。一个应用程序有一个并且只会有一个路由表。路由表在Global.asax文件中创建。
应用程序的路由表由RouteTable.Routes的静态属性表示。这个属性表示了路由对象的集合。我们在应用程序首次启动时为路由表增加两个路由对象(Application_Start()方法在第一次请求网站页面的时候被调用一次)。
路由对象负责把URL映射到Handler。我们可以创建了两个路由对象。这2个对象都把URL映射到MvcRouteHandler。第一个路由映射任何符合{controller}/{action}/{id}模式的URL到MvcRouteHandler。第二个路由映射某个URL Default.aspx到MvcRouteHandler。
顺便说一下,这种新的路由构架可以脱离ASP.NET MVC独立使用。Global.asax文件映射URL到MvcRouteHandler。然而,我们可以选择把URL路由到不同类型的Handler上。这里说的路由构架包含在一个叫做System.Web.Routing.dll的独立程序集中。我们可以脱离MVC使用路由。
步骤2:UrlRoutingModule拦截请求
当我们对ASP.NET MVC应用程序发起请求的时候,请求会被UrlRoutingModule HTTP Module拦截。HTTP Module是特殊类型的类,它参与每一次页面请求。例如,传统ASP.NET包含了FormsAuthenticationModule HTTP Module用来使用表单验证实现页面访问安全性。
UrlRoutingModule拦截请求后做的第一件事情就是包装当前的HttpContext为HttpContextWrapper2对象。HttpContextWrapper2类和派生自HttpContextBase的普通HttpContext类不同。创建的HttpContext的包装可以使使用诸如Typemock Isolator或Rhino Mocks的Mock对象框进行模拟变得更简单。
接着,Module把包装后的HttpContext传给在之前步骤中创建的RouteTable。HttpContext包含了URL、表单参数、查询字符串参数以及和当前请求关联的cookie。如果在当前请求和路由表中的路由对象之间能找到匹配,就会返回路由对象。
如果UrlRoutingModule成功获取了RouteData对象,Module然后就会创建表示当前HttpContext和RouteData的RouteContext对象。Module然后实例化基于RouteTable的新HttpHandler,并且把RouteContext传给Handler的构造函数。
对于ASP.NET MVC应用程序,从RouteTable返回的Handler总是MvcHandler(MvcRouteHandler返回MvcHandler)。只要UrlRoutingModule匹配当前请求到路由表中的路由,就会实例化带有当前RouteContext的MvcHandler。
Module进行的最后一步就是把MvcHandler设置为当前的HTPP Handler。ASP.NET应用程序自动调用当前HTTP Handler的ProcessRequest()方法然后转入下一步。
步骤3:执行MvcHandler
在之前的步骤中,表示某个RouteContext的MvcHandler被设置作为当前的HTTP Handler。ASP.NET应用程总是会发起一系列的事件,包括Star、BeginRequest、PostResolveRequestCache、 PostMapRequestHandler、PreRequestHandlerExecute和EndRequest事件(非常多的应用程序事件——对于完整列表,请查阅Visual Studio 文档中的HttpApplication类)。
之前内容中描述的所有东西都在PostResolveRequestCache和PostMapRequestHandler中发生。当前HTTP Handler的ProcessRequest()方法在PreRequestHandlerExecute事件之后被调用。
当之前内容中创建的MvcHandler对象的ProcessRequest()被调用的时候,会创建一个新的控制器。控制器由ControllerFactory创建。由于我们可以创建自己的ControllerFactory,所以这又是一个可扩展点。默认的ControllerFactory名字相当合适,叫做DefaultControllerFactory。
RequestContext以及控制器的名字被传入ControllerFactory.CreateController()方法来获得一个控制器。然后,从RequestContext和控制器构造ControllerContext对象。最后,调用控制器类的Execute()方法。在调用Execute()方法的时候会给方法传入ControllerContext。
步骤4:执行控制器
Execute()方法首先创建TempData对象(在Ruby On Rails中叫做Flash对象)。TempData可以用于保存下次请求必须的临时数据(TempData和会话状态差不多,不长期占用内存)。
接着,Execute()方法构建请求的参数列表。这些参数从请求参数中提取,将会被作为方法的参数。参数会被传入执行的控制器方法。
Execute()通过对控制器类进行反射来找到控制器的方法。控制器类是我们写的。Execute()方法找到了我们控制器类中的方法后就执行它。Execute()方法不会执行被装饰NonAction特性的方法。
至此,就进入了自己应用程序的代码。
步骤5:调用RenderView方法
通常,我们的控制器方法最后会调用RenderView()或RedirectToAction()方法。RenderView()方法负责把视图(页面)呈现给浏览器。
当我们调用控制器RenderView()方法的时候,调用会委托给当前ViewEngine的RenderView()方法。ViewEngine是另外一个扩展点。默认的ViewEngine是WebFormViewEngine。然而,我们可以使用诸如Nhaml的其它ViewEngine。
WebForm的ViewEngine.RenderView()方法创建了一个叫做ViewLocator的类来寻找视图。然后,它使用BuildManager来创建ViewPage类的实例。然后,如果页面有ViewData就会设置ViewData。最后,ViewPage 的RenderView()方法被调用。
ViewPage类从System.Web.UI.Page基类(和用于传统ASP.NET的页面一样)派生。RenderView()方法做的最后一个工作就是调用页面类的ProcessRequest()。调用视图的ProcessRequest()生成内容的方式和普通ASP.NET页面生成内容的方式一致。
可扩展点
ASP.NET MVC生命周期在设计的时候包含了很多可扩展点。我们可以自定义通过插入自定义类或覆盖既有类来自定义框架的行为。下面是这些扩展点的概要:
路由对象:当我们创建路由表的时候,调用RouteCollection.Add()方法来增加新的路由对象。Add()方法接受了RouteBase对象。我们可以通过派生RouteBase基类来实现自己的路由对象。
MvcRouteHandler :当创建MVC应用程序的时候,我们把URL映射到MvcRouteHandler对象上。然而,我们可以把URL映射到实现IRouteHandler接口的任何类上。路由类的构造函数接受任何实现IRouteHandler接口的对象。
MvcRouteHandler.GetHttpHandler():MvcRouteHandler 类的GetHttpHandler()方法是virtual方法。默认情况下,MvcRouteHandler返回MvcHandler。如果愿意的话,我们可以覆盖GetHttpHandler()方法来返回不同的Handler。
ControllerFactory:我们可以通过System.Web.MVC.ControllerBuilder.Current.SetControllerFactory()方法指定一个自定义类来创建自定义的控制器工厂。控制器工厂负责为某个控制器名和RequestContext返回控制器。
控制器:我们可以通过实现Icontroller接口来实现自定义控制器。这个接口只有一个Execute(ControllerContext controllerContext)方法。
ViewEngine:我们可以为控制器指定自定义的ViewEngine。通过为公共的Controller.ViewEngine属性指定ViewEngine来把ViewEngine指定给控制器。ViewEngine必须实现IviewEngine接口,接口只有一个方法:RenderView(ViewContext viewContext)。
ViewLocator :ViewLocator把视图名映射到实际视图文件上。我们可以通过WebFormViewEngine.ViewLocator的属性来执行自定义的ViewLocator。
二、普通ASP.NET的网页生命周期
大体上,ASP.NET请求的处理流程分为如下图所示的两步。用户发送一个请求到IIS服务器时:
(1)ASP.NET会创建一个能够处理请求的环境。换句话说,它会创建一个包含请求、响应以及上下文对象的应用程序对象来处理这个请求。
(2)一旦ASP.NET环境被创建,用户请求就会通过由modules(管道)、handlers(处理程序)和page objects(页面对象)触发的一系列事件进行处理。简而言之,我们暂且将此步凑称为MHPM(Module、Handler、Page和Module Event)。
ASP.NET环境的创建
第一步:用户请求到达IIS后,IIS首先会检查哪一个ISAPI扩展能够处理这个请求,这会取决于文件的后缀名。例如:如果请求的是一个‘.aspx‘的页面,那么就会被传递到‘aspnet_isapi.dll‘来进行处理。
第二步:如果这是该网站的首次请求,那么一个称为‘ApplicationManager‘的类会首先创建一个该网站可以运行的应用程序域(App Domain)。正如我们所知,应用程序域隔离部署在同一台IIS服务器上的两个不同的Web应用程序。因此,即使其中一个应用程序域出现了错误,也不会影响其他应用程序域的正常运作。
(.NET平台下,程序集并没有直接加载进进程中。.NET可执行程序承载在进程的一个逻辑分区中,术语称应用程序域(简称AppDomain)。应用程序域是.NET引入的一个新概念,它比进程所占用的资源要少,可以被看作是一个轻量级的进程。在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll)。这样可以使应用程序域之间实现深度隔离,所以:即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。)
第三步:在新创建的应用程序域中,会创建ASP.NET的宿主环境,也就是HttpRuntime对象。一旦宿主环境被创建完成,ASP.NET最核心的对象如HttpContext、HttpRequest和HttpResponse对象都会被创建好。
第四步:一旦所有核心的ASP.NET对象被创建好,HttpApplication对象就会随之被创建来服务这个请求。如果你的系统中存在一个global.asax文件,那么这个global.asax文件的对象也会被创建。但是,需要注意的是你的global.asax需要继承自HttpApplication类。
注意:在一个ASP.NET页面第一次附加到网站,一个HttpApplication实例便随之产生。为了最大化得提高处理性能,HttpApplication的实例将会被复用以处理多个请求。
Global.asax 文件(也称作 ASP.NET 应用程序文件)是可选文件,包含用于响应 ASP.NET 或 HttpModule 引发的应用程序级别事件的代码。(换句话说,我们可以自定义后面我们所要介绍的一些事件,因为请求处理流程会经历后面的10多个事件,我们可以写代码来自定义其中的一些事件,加一些我们想做的业务逻辑操作,比如:URL重写、身份验证、图片水印等等。)如果不定义该文件,ASP.NET 页框架假设您未定义任何应用程序或会话事件处理程序。
第五步:此时HttpApplication对象将会被分配给一系列的ASP.NET核心对象来处理请求的页面。
第六步:这时,HttpApplication开始通过HTTP管道事件、处理程序(Handlers)和页面事件来处理请求了。也就是说:它会触发 MHPM 中的事件来处理请求。