自己動手寫一個簡單的MVC框架

周旭龍發表於2016-03-18

一、ASP.NET MVC核心機制回顧

在ASP.NET MVC中,最核心的當屬“路由系統”,而路由系統的核心則源於一個強大的System.Web.Routing.dll元件。

在這個System.Web.Routing.dll中,有一個最重要的類叫做UrlRoutingModule,它是一個實現了IHttpModule介面的類,在請求處理管道中專門針對ASP.NET MVC請求進行處理。首先,我們要了解一下UrlRoutingModule是如何起作用的。

(1)IIS網站的配置可以分為兩個塊:全域性 Web.config 和本站 Web.config。Asp.Net Routing屬於全域性性的,所以它配置在全域性Web.Config 中,我們可以在如下路徑中找到:“$WindowsMicrosoft.NETFramework版本號ConfigWeb.config“

<?xml version="1.0" encoding="utf-8"?>
 <!-- the root web configuration file -->
 <configuration>
     <system.web>
         <httpModules>
             <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" />
         </httpModules>
    </system.web>
 </configuration>

(2)通過在全域性Web.Config中註冊 System.Web.Routing.UrlRoutingModule,IIS請求處理管道接到請求後,就會載入 UrlRoutingModule型別的Init()方法。

PS : 在UrlRoutingModule中為請求處理管道中的第七個事件PostResolveRequestCache註冊了一個事件處理方法:OnApplicationPostResolveRequestCache。從這裡可以看出:ASP.NET MVC的入口在UrlRoutingModule,即訂閱了HttpApplication的第7個管道事件PostResolveRequestCahce。換句話說,是在HtttpApplication的第7個管道事件處對請求進行了攔截。

現在我們將ASP.NET MVC的請求處理分為兩個重要階段來看看:

①在第七個事件中建立實現了IHttpHandler介面的MvcHandler

當請求到達UrlRoutingModule的時候,UrlRoutingModule取出請求中的Controller、Action等RouteData資訊,與路由表中的所有規則進行匹配,若匹配,把請求交給IRouteHandler,即MVCRouteHandler。我們可以看下UrlRoutingModule的原始碼來看看,以下是幾句核心的程式碼:

public virtual void PostResolveRequestCache(HttpContextBase context)
{
    // 通過RouteCollection的靜態方法GetRouteData獲取到封裝路由資訊的RouteData例項
    RouteData routeData = this.RouteCollection.GetRouteData(context);
    if (routeData != null)
    {
        // 再從RouteData中獲取MVCRouteHandler
        IRouteHandler routeHandler = routeData.RouteHandler;
        ......
        if (!(routeHandler is StopRoutingHandler))
        {
            ......
            // 呼叫 IRouteHandler.GetHttpHandler(),獲取的IHttpHandler 型別例項,它是由 IRouteHandler.GetHttpHandler獲取的,這個得去MVC的原始碼裡看
            IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
            ......
            // 合適條件下,把之前將獲取的IHttpHandler 型別例項 對映到IIS HTTP處理管道中
            context.RemapHandler(httpHandler);
        }
    }
}

從原始碼片段中可以看出,最後將請求轉移給了實現了IHttpHandler介面的處理程式進行後續的處理。在ASP.NET MVC的實現中,是將請求交給了MvcHandler這個類,通過執行其ProcessRequest方法來進行後續的處理。

②在第十一個事件與第十二個事件之間呼叫MvcHandler的ProcessRequest()方法

(1)在WebForm中,此階段會呼叫Page類物件的ProcessRequest()方法。在ASP.NET MVC中,會呼叫MvcHandler的ProcessRequest()方法,此方法會啟用具體請求的Controller類物件,觸發Action方法,返回ActionResult例項

(2)如果ActionResult是非ViewResult,比如JsonResult, ContentResult,這些內容將直接被輸送到Response響應流中,顯示給客戶端;如果是ViewResult,就會進入下一個渲染檢視環節。

(3)在渲染檢視環節,ViewEngine找到需要被渲染的檢視,View被載入成WebViewPage型別,並渲染生成Html,最終返回Html。

二、我的MVC框架核心部分介紹

2.1 解決方案概覽

在該解決方案中,一共有兩個專案:

一個是App,它是一個由最小化的引用環境(只引用了System和System.Web,以及Mvc.Lib)搭建起來的一個Web應用專案,藉助MVC核心類庫(Mvc.Lib)實現了MVC模式。

一個是Lib,它是一個模擬ASP.NET MVC框架的最小化、輕量級的迷你MVC框架,其中Mvc資料夾模擬System.Web.Mvc,Routing資料夾模擬System.Web.Routing,而View則簡單地藉助NVelocity模板引擎提供View檢視服務。

2.2 MVC核心類庫

(1)Routing

從第一部分我們可以知道,ASP.NET MVC的入口在於UrlRoutingModule,因此這裡我們便模擬實現了一個UrlRoutingModule.

 /// 
    /// 解析請求中的路由資料,並分發請求到Handler
    /// 
    public class UrlRoutingModule : IHttpModule
    {
        public void Init(HttpApplication application)
        {
            // 註冊ASP.NET請求處理管道的第七個事件
            application.PostResolveRequestCache += Application_PostResolveRequestCache;
        }

        // 假設請求 http://www.edisonchou.cn/home/index
        private void Application_PostResolveRequestCache(object sender, EventArgs e)
        {
            var application = sender as HttpApplication;
            var context = application.Context;
            // 根據全域性路由表解析當前請求的路徑
            var requestUrl = context.Request.AppRelativeCurrentExecutionFilePath.Substring(2);
            // 遍歷全域性路由表中的路由規則解析資料
            IDictionarystring, object> routeData;
            var route = RouteTable.MatchRoutes(requestUrl, out routeData);
            if (route == null)
            {
                // 404 Not Found
                throw new HttpException(404, "Not Found!");
            }
            // 獲取處理請求的Handler處理程式
            if (!routeData.ContainsKey("controller"))
            {
                // 404 Not Found
                throw new HttpException(404, "Not Found!");
            }
            var handler = route.GetRouteHandler(routeData);
            // 為當前請求指定Handler處理程式
            context.RemapHandler(handler);
        }

        public void Dispose()
        {
            this.Dispose();
        }
    }

該UrlRoutingModule通過註冊ASP.NET請求處理管道的第七個事件,來實現對URL地址進行路由規則的處理,並將最後生成的路由資料交給MvcHandler進行後續處理。這裡我省略了ASP.NET MVC原始碼中MvcRouteHandler生成MvcHandler的步驟,直接丟給MvcHandler處理。

核心部分有兩點,一是路由規則的匹配,二是為請求指定handler。

在路由規則的匹配中,通過設定路由資料鍵值對(Dictionary),並將設定好的路有資料傳遞給MvcHandler。具體的流程如下圖所示,這裡就不再展示原始碼,請自行下載DEMO檢視:

(2)Mvc

在此資料夾中,實現了三個核心的部分:

① 最核心的處理者 : MvcHandler

 public class MvcHandler : IHttpHandler
    {
        private IDictionarystring, object> routeData;

        public MvcHandler(IDictionarystring, object> routeData)
        {
            this.routeData = routeData;
        }

        public void ProcessRequest(HttpContext context)
        {
            var controllerName = routeData["controller"].ToString();
            // 藉助控制器工廠建立具體控制器例項
            IController controller = DefaultControllerFactory.CreateController(controllerName);
            // 確保有找到一個Controller處理請求
            if (controller == null)
            {
                // 404 Not Found!
                throw new HttpException(404, "Not Found");
            }
            // 封裝請求
            var requestContext = new RequestContext { HttpContext = context, RouteData = routeData };
            // 開始執行
            var result = controller.Execute(requestContext);
            result.Execute(requestContext);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }

② 花樣的返回型別 : ActionResult 以及它的子類們

在以往的ASP.NET MVC開發中,我們在Action方法的編寫中,總會看到它們的返回型別都是以ActionResult為基類的各種Result型別。

     /// <summary>
    /// Action統一的返回型別
    /// </summary>
    public abstract class ActionResult
    {
        public abstract void Execute(RequestContext context);
    }

因此,這裡也實現了ActionResult這個抽象類,並以此為基礎實現了ContentResult、JsonResult以及ViewResult。它們的區別就在於是不同的返回型別,因此有不同的處理。

這裡以ContentResult 和 JsonResult 為例,來看看具體做了什麼處理。

[ContentResult]

  public class ContentResult : ActionResult
    {
        private string content;
        private string contentType;

        public ContentResult(string content, string contentType)
        {
            this.content = content;
            this.contentType = contentType;
        }

        public override void Execute(RequestContext context)
        {
            context.HttpContext.Response.Write(content);
            context.HttpContext.Response.ContentType = contentType;
        }
    }
[JsonResult]

    public class JsonResult : ActionResult
    {
        private object paraObj;

        public JsonResult(object paraObj)
        {
            this.paraObj = paraObj;
        }

        public override void Execute(RequestContext context)
        {
            JavaScriptSerializer jss = new JavaScriptSerializer();
            var json = jss.Serialize(paraObj);
            context.HttpContext.Response.Write(json);
            context.HttpContext.Response.ContentType = "application/json";
        }
    }

相信有經驗的讀者一眼就看穿了,因此這裡也就不再多說了。

③ 路由的擴充套件者 : RouteExtend

在以往的ASP.NET MVC開發中,我們會在Global全域性應用處理檔案中為專案註冊路由規則,但卻不知道其實我們常用的MapRoute方法其實是一個擴充套件方法,它並不位於System.Web.Routing這個類庫之中,而是位於System.Web.Mvc這個類庫之中。

因此,我們也在Mvc資料夾中實現了一個RouteExtend類,它為RouteTable類的Route集合實現了一個擴充套件方法:

/// 
    /// Route 的擴充套件方法所在類
    /// 
    public static class RouteExtend
    {
        /// 
        /// 指定MvcHandler來處理
        /// 
        public static void MapRoute(this IList source, string urlTemplate, object defaults)
        {
            MapRoute(source, urlTemplate, defaults, routeData => new MvcHandler(routeData));
        }

        /// 
        /// 通過指定實現了IHttpHandler的處理程式來處理
        /// 
        public static void MapRoute(this IList source, string urlTemplate, object defaults, Funcstring, object>, IHttpHandler> handler)
        {
            source.Add(new Route(urlTemplate, defaults, handler));
        }
    }

可以看出,MvcHandler是在這裡傳入的(Mvc與Routing是單向依賴)。那麼,為什麼還要提供一個可傳入自定義Handler的介面呢?因為,不同的路由規則有可能需要不同的實現IHttpHandler的處理程式來處理,也不一定就非得是MvcHandler。

(3)View

在ASP.NET MVC中提供了aspx與Razor等模板引擎,這裡我偷了懶,直接藉助了NVelocity模板引擎來實現。因此,這個資料夾中只有一個VelocityHelper類(我直接從網上搜尋的),該類可以幫助我們找到指定的HTML並繫結Model實體。

    /// 
    /// NVelocity模板工具類 VelocityHelper
    /// 
    public class VelocityHelper
    {
        private VelocityEngine velocity = null;
        private IContext context = null;

        public object YZControl { get; private set; }

        /// 
        /// 建構函式
        /// 
        /// 模板資料夾路徑
        public VelocityHelper(string templatDir)
        {
            Init(templatDir);
        }

        /// 
        /// 無引數建構函式
        /// 
        public VelocityHelper() { }

        /// 
        /// 初始話NVelocity模組
        /// 
        public void Init(string templatDir)
        {
            // 建立VelocityEngine例項物件
            velocity = new VelocityEngine();

            // 使用設定初始化VelocityEngine
            ExtendedProperties props = new ExtendedProperties();
            props.AddProperty(RuntimeConstants.RESOURCE_LOADER, "file");
            props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, HttpContext.Current.Server.MapPath(templatDir));
            //props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, Path.GetDirectoryName(HttpContext.Current.Request.PhysicalPath));
            props.AddProperty(RuntimeConstants.INPUT_ENCODING, "utf-8");
            props.AddProperty(RuntimeConstants.OUTPUT_ENCODING, "utf-8");

            // 模板的快取設定
            props.AddProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, true);              //是否快取
            props.AddProperty("file.resource.loader.modificationCheckInterval", (Int64)30);    //快取時間(秒)

            velocity.Init(props);

            // 為模板變數賦值
            context = new VelocityContext();
        }

        /// 
        /// 給模板變數賦值
        /// 
        /// 模板變數
        /// 模板變數值
        public void Put(string key, object value)
        {
            if (context == null)
            {
                context = new VelocityContext();
            }
            context.Put(key, value);
        }

        /// 
        /// 顯示模板
        /// 
        /// 模板檔名
        public void Display(string templatFileName)
        {
            // 從檔案中讀取模板
            Template template = velocity.GetTemplate(templatFileName);
            // 合併模板
            StringWriter writer = new StringWriter();
            template.Merge(context, writer);
            // 輸出
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.Write(writer.ToString());
            HttpContext.Current.Response.Flush();
            HttpContext.Current.Response.End();
        }

        /// 
        /// 根據模板生成靜態頁面
        /// 
        /// 
        /// 
        public void CreateHtml(string templatFileName, string htmlpath)
        {
            // 從檔案中讀取模板
            Template template = velocity.GetTemplate(templatFileName);
            // 合併模板
            StringWriter writer = new StringWriter();
            template.Merge(context, writer);
            using (StreamWriter write2 = new StreamWriter(HttpContext.Current.Server.MapPath(htmlpath), false, Encoding.UTF8, 200))
            {
                write2.Write(writer);
                write2.Flush();
                write2.Close();
            }
        }

        /// 
        /// 根據模板生成靜態頁面
        /// 
        /// 
        /// 
        //public void CreateJS(string templatFileName, string htmlpath)
        //{
        //    //從檔案中讀取模板
        //    Template template = velocity.GetTemplate(templatFileName);
        //    //合併模板
        //    StringWriter writer = new StringWriter();
        //    template.Merge(context, writer);
        //    using (StreamWriter write2 = new StreamWriter(HttpContext.Current.Server.MapPath(htmlpath), false, Encoding.UTF8, 200))
        //    {
        //        write2.Write(YZControl.Strings.Html2Js(YZControl.Strings.ZipHtml(writer.ToString())));
        //        write2.Flush();
        //        write2.Close();
        //    }
        //}
    }

三、我的MVC框架應用例項

3.1 MVC 應用DEMO介紹

這是一個ASP.NET 空Web應用專案搭建起來的MVC Web應用專案,它移除了自帶的所有引用專案,僅僅保留了System和System.Web,做到了儘可能地“純淨”。通過引入Mvc.Lib核心類庫,建立Controller、Model和View資料夾以及對應的類和HTML來實現MVC模式。

(1)引入Mvc.Lib核心類庫之後,需要配置一下Web.config,使UrlRoutingModule能夠正常工作:

<system.web>
    <compilation debug="true" targetFramework="4.5"/>
    <httpRuntime targetFramework="4.5"/>
    <!-- HttpModule配置(IIS6版本) -->
    <httpModules>
      <add name="UrlRoutingModule" type="Manulife.Web.Mvc.Lib.Routing.UrlRoutingModule"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <!-- 配置不去校驗是否是整合模式 -->
    <validation validateIntegratedModeConfiguration="false"/>
    <!-- HttpModule配置(IIS7及以上版本) -->
    <modules>
      <add name="UrlRoutingModule" type="Manulife.Web.Mvc.Lib.Routing.UrlRoutingModule"/>
    </modules>
  </system.webServer>

(2)新建Global全域性處理配置,在Application_Start事件中為專案新增路由規則:

 public class Global : System.Web.HttpApplication
    {

        protected void Application_Start(object sender, EventArgs e)
        {
            // 註冊路由規則1
            RouteTable.Routes.MapRoute(
                urlTemplate: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index" }
                );
            // 註冊路由規則2
            RouteTable.Routes.MapRoute(
                urlTemplate: "{controller}/{action}",
                defaults: new { controller = "Home", action = "Index" }
                );
            // 註冊路由規則3
            RouteTable.Routes.MapRoute(
                urlTemplate: "{controller}",
                defaults: new { controller = "Home", action = "Index" }
                );
        }

    }

(3)看看Controller是怎麼寫的?是不是很熟悉?

public class HomeController : ControllerBase
    {
        public ActionResult Index(int id, string controller, string action)
        {
            return new ContentResult(string.Format("<h1>Controller : {0}, Action : {1}, Id : {2}</h1>", controller, action, id), "text/html");
        }

        public ActionResult View()
        {
            return new ViewResult(new { Id = 1, Name = "Edison Chou", Age = 27, Gender = true });
        }
    }

(4)看看View中的HTML呢?這裡使用NVelocity模板引擎提供的語法,操作Model實體物件。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Index - View</title>
    <meta charset="utf-8" />
</head>
<body>
    <h1>User Name : $model.Name</h1>
    <h1>User Age : $model.Age</h1>
</body>
</html>

3.2 MVC 應用DEMO演示

(1)預設路由 : home/index -> ContentResult

(2)請求JsonResult

(3)請求ViewResult

附件下載

Manulife.Web.Mvc

相關文章