1. 程式人生 > >自己動手寫一個簡單的MVC框架(第一版)

自己動手寫一個簡單的MVC框架(第一版)

一、MVC概念回顧

asp.net mvc

  路由(Route)、控制器(Controller)、行為(Action)、模型(Model)、檢視(View)

用一句簡單地話來描述以上關鍵點:

  路由(Route)就相當於一個公司的前臺小姐,她負責帶你(請求)找到跟你面試的面試官(控制器Controller),面試官可能會面試不同的職位(Action),你(請求)也會拿到不同的結果(ActionResult);

二、開始DEMO:單一處理程式入口

2.1 建立一個空白Web程式,移除所有預設引用

  無論是ASP.NET WebForms還是ASP.NET MVC,他們都只是一個框架,是建立在System.Web之上的框架。為了保證程式的純淨,我們可以將所有預設的引用都移除。當然,我們還是得保留幾個必要的dll引用:

注意:這裡我們並沒有引入System.Web.Mvc.dll,因為我們要實現的就是一個簡單的MVC機制。

2.2 模擬ASP.NET MVC,建立幾個MVC資料夾

  按照ASP.NET MVC的慣例新增Controllers、Models和Views資料夾(不是必須的):

2.3 新建一個Controller

  我們首先在Controllers資料夾下新建一個介面,取名為IController,它約定了所有Controller都必須要實現的方法:Execute

    public interface IController
    {
        
void Execute(HttpContext context); }

  IController介面只定義了一個方法宣告,它接收一個HttpContext的上下文物件。

  有了介面,我們就可以實現具體的Controller了,這裡我們實現了兩個Controller:HomeController和ProductController。

  (1)HomeController

    public class HomeController : IController
    {
        private HttpContext currentContext;

        
// action 1 : Index public void Index() { currentContext.Response.Write("Home Index Success!"); } // action 2 : Add public void Add() { currentContext.Response.Write("Home Add Success!"); } public void Execute(HttpContext context) { currentContext = context; // 預設Action名稱 string actionName = "index"; // 獲取Action名稱 if (!string.IsNullOrEmpty(context.Request["action"])) { actionName = context.Request["action"]; } switch (actionName.ToLower()) { case "index": this.Index(); break; case "add": this.Add(); break; default: this.Index(); break; } } }
View Code

  (2)ProductController

    public class ProductController : IController
    {
        private HttpContext currentContext;

        // action 1 : Index
        public void Index()
        {
            currentContext.Response.Write("Product Index Success!");
        }

        // action 2 : Add
        public void Add()
        {
            currentContext.Response.Write("Product Add Success!");
        }

        public void Execute(HttpContext context)
        {
            currentContext = context;
            // 預設Action名稱
            string actionName = "index";
            // 獲取Action名稱
            if (!string.IsNullOrEmpty(context.Request["action"]))
            {
                actionName = context.Request["action"];
            }

            switch (actionName.ToLower())
            {
                case "index":
                    this.Index();
                    break;
                case "add":
                    this.Add();
                    break;
                default:
                    this.Index();
                    break;
            }
        }
    }
View Code

2.4 新建一個ashx(一般處理程式),作為處理程式的入口

  有了Controller之後,需要藉助一個入口來指引請求到達指定Controller,所以這裡我們實現一個最簡單的一般處理程式,它將url中的引數進行解析並例項化指定的Controller進行後續請求處理:

    /// <summary>
    /// 模擬MVC程式的單一入口
    /// </summary>
    public class Index : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            // 獲取Controller名稱
            var controllerName = context.Request.QueryString["c"];
            // 宣告IControoler介面-根據Controller Name找到對應的Controller
            IController controller = null;

            if (string.IsNullOrEmpty(controllerName))
            {
                controllerName = "home";
            }

            switch (controllerName.ToLower())
            {
                case "home":
                    controller = new HomeController();
                    break;
                case "product":
                    controller = new ProductController();
                    break;
                default:
                    controller = new HomeController();
                    break;
            }

            controller.Execute(context);
        }

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

  該一般處理程式接收http請求的兩個引數controller和action,並通過controller的引數名稱生成對應的Controller例項物件,將HttpContext物件作為引數傳遞給對應的Controller物件進行後續處理。

2.5 新建一個Global(全域性處理程式),作為路由對映的入口

  在Global.asax中有一個Application_BeginRequest的事件,它發生在每個Request開始處理之前,因此在這裡我們可以進行一些類似於URL重寫的工作。解析URL當然也在這裡進行,我們要做的就是將使用者輸入的類似於MVC形式的URL:http://www.xxx.com/home/index 進行正確的解析,將該請求交由HomeController進行處理。

    public class Global : System.Web.HttpApplication
    {
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            #region 方式一:偽靜態方式實現路由對映服務
            // 獲得當前請求的URL地址
            var executePath = Request.AppRelativeCurrentExecutionFilePath;
            // 獲得當前請求的引數陣列
            var paraArray = executePath.Substring(2).Split('/');
            // 如果沒有引數則執行預設配置
            if (string.IsNullOrEmpty(executePath) || executePath.Equals("~/") ||
                paraArray.Length == 0)
            {
                return;
            }

            string controllerName = "home";
            if (paraArray.Length > 0)
            {
                controllerName = paraArray[0];
            }
            string actionName = "index";
            if (paraArray.Length > 1)
            {
                actionName = paraArray[1];
            }

            // 入口一:單一入口 Index.ashx
            Context.RewritePath(string.Format("~/Index.ashx?controller={0}&action={1}", controllerName, actionName));
            // 入口二:指定MvcHandler進行後續處理
            //Context.RemapHandler(new MvcHandler());
            #endregion
        }
    }
View Code

  這裡我們直接在程式碼中hardcode了一個預設的controller和action名稱,分別是home和index。

  可以看出,最後我們實際上做的就是解析URL,並通過重定向到Index.ashx進行所謂的Route路由工作。

2.6 執行吧偽MVC

  (1)預設路由

  (2)/home/add

  (3)/product/index

三、改造DEMO:藉助反射讓多型發光

3.1 在Global檔案中模擬路由規則表

  想想我們在ASP.NET MVC專案中是不是首先向程式註冊一些指定的路由規則,因此這裡我們也在Global.asax中模擬一個路由規則表:

  (1)增加一個靜態的路由規則集合

    // 定義路由規則
    private static IList<string> Routes;

  (2)在Application_Start事件中註冊路由規則

    protected void Application_Start(object sender, EventArgs e)
    {
        Routes = new List<string>();
        // http://www.edisonchou.cn/controller/action
        Routes.Add("{controller}/{action}");
        // http://www.edisonchou.cn/controller
        Routes.Add("{controller}");
    }

  (3)改寫Application_BeginRequest事件,使URL與路由規則進行匹配

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        #region 方式二:模擬路由表實現對映服務
        // 模擬路由字典
        IDictionary<string, string> routeData = new Dictionary<string, string>();
        // 將URL與路由表中每一條記錄進行匹配
        foreach (var item in Routes)
        {
            var executePath = Request.AppRelativeCurrentExecutionFilePath;//// 獲得當前請求的引數陣列
            // 如果沒有引數則執行預設配置
            if (string.IsNullOrEmpty(executePath) || executePath.Equals("~/"))
            {
                executePath += "/home/index";
            }
            var executePathArray = executePath.Substring(2).Split(new[] { '/' },
                StringSplitOptions.RemoveEmptyEntries);
            var routeKeys = item.Split(new[] { '/' },
                StringSplitOptions.RemoveEmptyEntries);
            if (executePathArray.Length == routeKeys.Length)
            {
                for (int i = 0; i < routeKeys.Length; i++)
                {
                    routeData.Add(routeKeys[i], executePathArray[i]);
                }

                // 入口一:單一入口 Index.ashx
                //Context.RewritePath(string.Format("~/Index.ashx?c={0}&a={1}", routeData["{controller}"], routeData["{action}"]));
                // 入口二:指定MvcHandler進行後續處理
                Context.RemapHandler(new MvcHandler(routeData));
                // 只要滿足一條規則就跳出迴圈匹配
                break;
            }
        }
        #endregion
    }

3.2 模擬ASP.NET管道工作,實現MvcHandler

  在ASP.NET請求處理管道中,具體的處理工作都是轉交給了實現IHttpHandler介面的Handler物件進行處理。因此,這裡我們也遵照這個規則,實現一個MvcHandler來代替剛才的Index.ashx來進行路由工作:

    public class MvcHandler : IHttpHandler
    {
        // 路由表
        private IDictionary<string, string> routeData;
        // 所有控制器的型別集合
        private static IList<Type> alloctionControllerTypes;

        // 當前類第一次載入時呼叫靜態建構函式
        static MvcHandler()
        {
            alloctionControllerTypes = new List<Type>();
            // 獲得當前所有引用的程式集
            var assemblies = BuildManager.GetReferencedAssemblies();
            // 遍歷所有的程式集
            foreach (Assembly assembly in assemblies)
            {
                // 獲取當前程式集中所有的型別
                var allTypes = assembly.GetTypes();
                // 遍歷所有的型別
                foreach (Type type in allTypes)
                {
                    // 如果當前型別滿足條件
                    if (type.IsClass && !type.IsAbstract && type.IsPublic
                        && typeof(IController).IsAssignableFrom(type))
                    {
                        // 將所有Controller加入集合
                        alloctionControllerTypes.Add(type);
                    }
                }
            }
        }

        public MvcHandler(IDictionary<string, string> routeData)
        {
            this.routeData = routeData;
        }

        public void ProcessRequest(HttpContext context)
        {
            var controllerName = routeData["{controller}"];

            if (string.IsNullOrEmpty(controllerName))
            {
                // 指定預設控制器
                controllerName = "home";
            }

            IController controller = null;
            // 通過反射的方式載入具體例項
            foreach (var controllerItem in alloctionControllerTypes)
            {
                if (controllerItem.Name.Equals(string.Format("{0}Controller", controllerName), StringComparison.InvariantCultureIgnoreCase))
                {
                    controller = Activator.CreateInstance(controllerItem) as IController;
                    break;
                }
            } 

            var requestContext = new HttpContextWrapper()
            {
                Context = context,
                RouteData = routeData
            };
            controller.Execute(requestContext);
        }

        public bool IsReusable
        {
            get
            {
                throw new NotImplementedException();
            }
        }
    }
View Code

  上述程式碼中需要注意以下幾點:

  (1)在靜態建構函式中初始化所有Controller

    // 路由表
    private IDictionary<string, string> routeData;
    // 所有控制器的型別集合
    private static IList<Type> alloctionControllerTypes;

    // 當前類第一次載入時呼叫靜態建構函式
    static MvcHandler()
    {
        alloctionControllerTypes = new List<Type>();
        // 獲得當前所有引用的程式集
        var assemblies = BuildManager.GetReferencedAssemblies();
        // 遍歷所有的程式集
        foreach (Assembly assembly in assemblies)
        {
            // 獲取當前程式集中所有的型別
            var allTypes = assembly.GetTypes();
            // 遍歷所有的型別
            foreach (Type type in allTypes)
            {
                // 如果當前型別滿足條件
                if (type.IsClass && !type.IsAbstract && type.IsPublic
                    && typeof(IController).IsAssignableFrom(type))
                {
                    // 將所有Controller加入集合
                    alloctionControllerTypes.Add(type);
                }
            }
        }
    }

  此段程式碼利用反射載入了所有實現了IController介面的Controller類,並存入了一個靜態集合alloctionControllerTypes裡面,便於後面所有請求進行匹配。

  (2)在ProcessRequest方法中再次利用反射動態建立Controller例項

    public void ProcessRequest(HttpContext context)
    {
        var controllerName = routeData["{controller}"];

        if (string.IsNullOrEmpty(controllerName))
        {
            // 指定預設控制器
            controllerName = "home";
        }

        IController controller = null;
        // 通過反射的方式載入具體例項
        foreach (var controllerItem in alloctionControllerTypes)
        {
            if (controllerItem.Name.Equals(string.Format("{0}Controller", controllerName), StringComparison.InvariantCultureIgnoreCase))
            {
                controller = Activator.CreateInstance(controllerItem) as IController;
                break;
            }
        } 

        var requestContext = new HttpContextWrapper()
        {
            Context = context,
            RouteData = routeData
        };
        controller.Execute(requestContext);
    }

  這裡由於要使用到RouteData這個路由表的Dictionary物件,所以我們需要改寫一下傳遞的物件由原來的HttpContext型別轉換為自定義的包裝類HttpContextWrapper:

    public class HttpContextWrapper
    {
        public HttpContext Context { get; set; }
        public IDictionary<string, string> RouteData { get; set; }
    }

  可以看出,其實就是簡單地包裹了一下,添加了一個RouteData的路由表屬性。

  當然,IController介面的方法定義也得隨之改一下:

    public interface IController
    {
        void Execute(HttpContextWrapper context);
    }

  至此,MvcHandler的程式碼就寫完,我們可以總結一下它的主要流程:

3.3 改寫Controller匹配新介面

  (1)HomeController

    public class HomeController : IController
    {
        private HttpContext currentContext;
        public void Execute(HttpContextWrapper context)
        {
            currentContext = context.Context;
            // 獲取Action名稱
            string actionName = "index";
            if (context.RouteData.ContainsKey("{action}"))
            {
                actionName = context.RouteData["{action}"];
            }

            switch (actionName.ToLower())
            {
                case "index":
                    this.Index();
                    break;
                case "add":
                    this.Add();
                    break;
                default:
                    this.Index();
                    break;
            }
        }

        // action 1 : Index
        public void Index()
        {
            currentContext.Response.Write("Home Index Success!");
        }

        // action 2 : Add
        public void Add()
        {
            currentContext.Response.Write("Home Add Success!");
        }
    }
View Code

  (2)ProductController

    public class ProductController : IController
    {
        private HttpContext currentContext;
        public void Execute(HttpContextWrapper context)
        {
            currentContext = context.Context;
            // 獲取Action名稱
            string actionName = "index";
            if (context.RouteData.ContainsKey("{action}"))
            {
                actionName = context.RouteData["{action}"];
            }

            switch (actionName.ToLower())
            {
                case "index":
                    this.Index();
                    break;
                case "add":
                    this.Add();
                    break;
                default:
                    this.Index();
                    break;
            }
        }

        // action 1 : Index
        public void Index()
        {
            currentContext.Response.Write("Product Index Success!");
        }

        // action 2 : Add
        public void Add()
        {
            currentContext.Response.Write("Product Add Success!");
        }
    }
View Code

3.4 執行吧偽MVC

  (1)預設路由

  (2)/product/add

  (3)/product

四、小結

  本文首先回顧了一下MVC的關鍵概念,並從一個“純淨”的ASP.NET Web空專案開始一步一步構建一個類似於MVC的應用程式,通過單一處理入口的偽靜態方式與模擬路由表的方式進行了簡單地實現,並進行了測試。此次實驗,核心就在於獲取路由資料,指定處理程式,也就是理解並模擬路由機制。路由模組就是一個很簡單的HttpModule(如果您對HttpModule不熟悉,請瀏覽我翻譯的一篇文章:ASP.NET應用程式和頁面生命週期),而ASP.NET MVC幫我們實現了UrlRoutingModule從而使我們輕鬆實現了路由機制,該機制獲取了路由資料,並制定處理程式(如MvcHandler),執行MvcHandler的ProcessRequest方法找到對應的Controller型別,最後將控制權交給對應的Controller物件,就相當於前臺小妹妹幫你找到了面試官,你可以跟著面試官去進行相應的面試了(Actioin),希望你能得到好的結果(ActionResult)。

  當然,這個DEMO還有很多需要改進的地方,仍然需要不斷的改進才能稱之為一個“框架”。第一個版本就到此,後續我會寫第二個版本,希望到時再寫一篇筆記來分享。

附件下載

  MySimpleMvc : 點我下載

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

死磕 java執行緒系列之自己動手一個執行緒池

(手機橫屏看原始碼更方便) 問題 (1)自己動手寫的執行緒池如何支援帶返回值的任務呢? (2)如果任務執行的過程中丟擲異常了該

自己動手一個簡單MVC框架第一版

一、MVC概念回顧   路由(Route)、控制器(Controller)、行為(Action)、模型(Model)、檢視(View) 用一句簡單地話來描述以上關鍵點:   路由(Route)就相當於一個公司的前臺小姐,她負責帶你(請求)找到跟你面試的面試官(控制器Controller),面試官

自己動手一個簡單MVC框架第二版

一、ASP.NET MVC核心機制回顧   在ASP.NET MVC中,最核心的當屬“路由系統”,而路由系統的核心則源於一個強大的System.Web.Routing.dll元件。   在這個System.Web.Routing.dll中,有一個最重要的類叫做UrlRoutingModule,它是一個

自己動手實現一個簡單的Mybatis初級版本1.0

手寫Mybatis-v1.0 原始碼連結(包括v1.0與v2.0): https://github.com/staticLin/customMyBatis.git 從上一個文章 ---Mybatis概述中瞭解到了Mybatis的主要架構與底層原理流程,結尾給出了一個巨集觀流程圖,可

ROS的初步學習--自己一個簡單的釋出Publisher、訂閱(Subscriber)程式

1 寫一個釋出(Publisher)節點 節點(node)是連線到ROS網路中可執行的基本單元。我們在這建立一個釋出者—“talker”節點,這個節點持續對外發布訊息。 首先我們要把目錄切換到我們的beginner_tutorials工程包中 $ cd ~

自己動手一個持久層框架

[TOC] ## 0. 前言 and Flag 不得不說我又買課了,之前買課都花了大幾百了,但是這次又沒躲過去。買了拉鉤教育的【**java高薪訓練營**】。主要看到那個課程目錄有點牛逼,基本上把我聽說過的技術都包括了,不過真假不太確定,之後就是知乎百度谷歌一頓搜尋,沒查到什麼負面資訊,B站也有一部分視訊,我

從零一個Java WEB框架Controller層轉換器

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

從零一個Java WEB框架Controller層優化

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

從零一個Java WEB框架IOC建立

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

從零一個Java WEB框架框架的演進

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

從零一個Java WEB框架Dao層優化

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

從零一個Java WEB框架Server層 優化

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github 上一篇地

從零一個Java WEB框架

該系列,其實是對《架構探險》這本書的實踐。本人想記錄自己的學習心得所寫下的。 從一個簡單的Servlet專案開始起步。對每一層進行優化,然後形成一個輕量級的框架。 每一篇,都是針對專案的不足點進行優化的。 專案已放上github

自己動手處理器之第二階段3——Verilog HDL行為語句

將陸續上傳本人寫的新書《自己動手寫處理器》(尚未出版),今天是第七篇,我儘量每週四篇 2.6 Verilog HDL行為語句 2.6.1 過程語句       Verilog定義的模組一般包括有過程語句,過程語句有兩種:initial、always。其中initial常用於

自己動手處理器之第二階段1——可編程邏輯器件與PLD電路設計流程

Language 設有 tex 主體 algorithm 元器件 shee 數字 ltr 將陸續上傳本人寫的新書《自己動手寫處理器》(尚未出版),今

PHP MVC框架基礎小白自己動手一個PHP框架示例

一個示例專案,具體展示了PHP,MVC模式框架開發的全過程本人小白一個,希望各位大神多多指教,由於教程文字過多而且不易解釋我直接將示例專案打包,下面附上鍊接各位對PHP框架學習可以借鑑專案內帶有其他方法和註釋,希望能對大家有所幫助

一個屬於自己的PHP的MVC框架

第一篇文章已經把所需的目錄搭建好了,接下來的工作就是寫一些程式碼了 用編輯器開啟public/index.php檔案,寫上下面的程式碼 <?php define(DS, DIRECTORY_SEPARATOR); define(ROOT, dirna

一個屬於自己的PHP的MVC框架

最近想做個PHP的個人部落格作為學習用,但是發現儘管把PHP函式用得很熟悉了,按照常規的辦法,寫一個頁面處理一個請求,僅僅一個部落格就可能有很多個頁面,而且php程式碼和html程式碼都結合的非常緊密,如果想要實現更換面板的功能,就顯得非常無力。在網上找了好多framework框架,但似乎又要開始學

自己動手一個Vue外掛MD.7

造不完的輪子,封不完的外掛。網上什麼都有,但是有那找的功夫,自己都寫完了。漫島仍然在向前推進,只是你們看不到最新的更新內容了,剩餘的不會展示,等以後上線了再去看把。 講一下如何寫一個的Vue外掛,(以一個極其簡單的loading效果為例),會了這個其他不愁。 第一步,在compon

自己動手一個輕量級的Android網路請求框架

最近有空在看《App研發錄》一書,良心之作。書中第一部分第二章節講了不少關於網路底層封裝的知識,看後覺得學到了不少乾貨。 索性自己也動手完成了一個非常輕量級的網路請求框架,從該書中獲得了不少幫助。特此記錄,回顧一下思路,整理收穫。OK,一起來看。 就如書中所