1. 程式人生 > >MVC系列——MVC原始碼學習:打造自己的MVC框架(四:瞭解神奇的檢視引擎)

MVC系列——MVC原始碼學習:打造自己的MVC框架(四:瞭解神奇的檢視引擎)

前言:通過之前的三篇介紹,我們基本上完成了從請求發出到路由匹配、再到控制器的啟用,再到Action的執行這些個過程。今天還是趁熱打鐵,將我們的View也來完善下,也讓整個系列相對完整,博主不希望爛尾。對於這個系列,通過學習原始碼,博主也學到了很多東西,在此還是把博主知道的先發出來,供大家參考。

MVC原始碼學習系列文章目錄:

一、自定義ActionResult

通過前三篇的介紹,我們已經實現了Action方法的執行,但是我們執行Action方法的時候都是在方法裡面直接使用Response輸出結果,這樣寫相當不爽,並且html等文字編輯實在太不方便,於是萌生了自己去實現View的想法,這個過程並不容易,但沒辦法,凡事都要敢於邁出第一步。

在MVC裡面,我們最常見的寫法可能是這樣:

//返回檢視頁面
public ActionResult Index()
{
      return View();  
}

//返回請求的資料
public JsonResult Index()
{
    return Json(new {}, JsonRequestBehavior.AllowGet);
}

將JsonResult轉到定義可以看到,其實JsonResult也是繼承自ActionResult這個抽象類的,這麼神奇的ActioResult,究竟是個什麼東西呢。我們先仿照MVC裡面的也定義一個自己的ActionResult抽象類。

namespace Swift.MVC.MyRazor
{
    public abstract class ActionResult
    {
        public abstract void ExecuteResult(SwiftRouteData routeData);
    }
}

這個類很簡單,就是一個抽象類,下面一個抽象方法,約束實現類必須要實現這個方法,究竟這個類有什麼用?且看博主怎麼一步一步去實現它。

二、ContentResult和JsonResult的實現

檢視MVC原始碼可知,ActionResult的實現類有很多個,博主這裡就挑幾個最常用的來說說。

在MVC裡面,ContentResult常用來向當前請求輸出文字內容資訊,JsonResult常用來返回序列化過的json物件。我們分別來實現它們:

namespace Swift.MVC.MyRazor
{
    public class ContentResult:ActionResult
    {
        //頁面內容
        public string Content { get; set; }

        //編碼方式
        public Encoding ContentEncoding { get; set; }

        //response返回內容的格式
        public string ContentType { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;

            if (!string.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "text/html";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            if (Content != null)
            {
                response.Write(Content);
            }
        }
    }
}
ContentResult.cs
namespace Swift.MVC.MyRazor
{
    public class JsonResult:ActionResult
    {
        public JsonResult()
        {
            JsonRequestBehavior = JsonRequestBehavior.DenyGet;
        }

        public JsonRequestBehavior JsonRequestBehavior { get; set; }

        public Encoding ContentEncoding { get; set; }

        public string ContentType { get; set; }

        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;
            if (!String.IsNullOrEmpty(ContentType))
            {
                response.ContentType = ContentType;
            }
            else
            {
                response.ContentType = "application/json";
            }
            if (ContentEncoding != null)
            {
                response.ContentEncoding = ContentEncoding;
            }
            JavaScriptSerializer jss = new JavaScriptSerializer();
            var json = jss.Serialize(Data);
            response.Write(json);
        }
    }

    public enum JsonRequestBehavior
    {
        AllowGet,
        DenyGet,
    }
}
JsonResult.cs

程式碼不難理解,就是定義了當前Response的返回型別和編碼方式等等。接下來看看如何使用他們,為了更加接近MVC的寫法,我們在Controller基類裡面也定義一系列的“快捷方法”,何為“快捷方法”,就是能夠快速返回某個物件的方法,比如我們在Controller.cs裡面增加如下幾個方法:

        protected virtual ContentResult Content(string content)
        {
            return Content(content, null);
        }

        protected virtual ContentResult Content(string content, string contentType)
        {
            return Content(content, contentType, null);
        }

        protected virtual ContentResult Content(string content, string contentType, Encoding contentEncoding)
        {
            return new ContentResult()
            {
                Content = content,
                ContentType = contentType,
                ContentEncoding = contentEncoding
            };
        }

        protected virtual JsonResult Json(object data, JsonRequestBehavior jsonBehavior)
        {
            return new JsonResult()
            {
                Data = data,
                JsonRequestBehavior = jsonBehavior
            };
        }

 我們也按照MVC裡面的寫法,首先我們新建一個控制器MyViewController.cs,裡面新增兩個方法:

namespace MyTestMVC.Controllers
{
    public class MyViewController:Controller
    {
        public ActionResult ContentIndex()
        {
            return Content("Hello", "text/html", System.Text.Encoding.Default);
        }

        public ActionResult JsonIndex()
        {
            var lstUser = new List<User>();
            lstUser.Add(new User() { Id = 1, UserName = "Admin", Age = 20, Address = "北京", Remark = "超級管理員" });
            lstUser.Add(new User() { Id = 2, UserName = "張三", Age = 37, Address = "湖南", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 3, UserName = "王五", Age = 32, Address = "廣西", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 4, UserName = "韓梅梅", Age = 26, Address = "上海", Remark = "呵呵" });
            lstUser.Add(new User() { Id = 5, UserName = "呵呵", Age = 18, Address = "廣東", Remark = "呵呵" });
            return Json(lstUser, JsonRequestBehavior.AllowGet);
        }
    }
}

看到這種用法,是不是似曾相識?注意,這裡的ActionResult並不是MVC裡面的,而是上文我們自定義的!沒錯,在原生的MVC裡面,這些方法也是這般定義的,因為以上封裝方法本身就是參考MVC的原理來實現的。

看著以上程式碼,貌似大功告成,可以直接測試運行了。是不是這樣呢?總感覺少點東西呢。。。除錯發現,我們的Content()方法僅僅是返回了一個ContentResult物件,並沒有做其他操作啊!按照上述定義思路,貌似應該呼叫ContentResult物件的ExecuteResult()方法才對,因為這個方法裡面才是真正的向當前的響應流裡面寫入返回資訊。那麼這個ExecuteResult()方法究竟在哪裡呼叫呢?這裡,博主這樣呼叫了一下!在Controller.cs這個控制器的基類裡面,我們修改了Execute()方法的邏輯:

  public abstract class Controller:ControllerBase,IDisposable
    {
        public override void Execute(SwiftRouteData routeData)
        {
            //1.得到當前控制器的型別
            Type type = this.GetType();

            //2.從路由表中取到當前請求的action名稱
            string actionName = routeData.RouteValue["action"].ToString();

            //3.從路由表中取到當前請求的Url引數
            object parameter = null;
            if (routeData.RouteValue.ContainsKey("parameters"))
            {
                parameter = routeData.RouteValue["parameters"];
            }
            var paramTypes = new List<Type>();
            List<object> parameters = new List<object>();
            if (parameter != null)
            {
                var dicParam = (Dictionary<string, string>)parameter;
                foreach (var pair in dicParam)
                {
                    parameters.Add(pair.Value);
                    paramTypes.Add(pair.Value.GetType());
                }
            }

            //4.通過action名稱和對應的引數反射對應方法。
            //這裡第二個引數可以不理會action字串的大小寫,第四個引數決定了當前請求的action的過載引數型別
            System.Reflection.MethodInfo mi = type.GetMethod(actionName,
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase, null, paramTypes.ToArray(), null);

            //5.執行該Action方法
            var actionResult = mi.Invoke(this, parameters.ToArray()) as ActionResult;

            //6.得到action方法的返回值,並執行具體ActionResult的ExecuteResult()方法。
            actionResult.ExecuteResult(routeData);
        }
    }

在以上步驟5裡面,執行對應的Action方法之後,取到返回值,這個返回值就是一個ActionResult實現類的例項,比如上文的ContentResult和JsonResult等,然後在步驟6裡面呼叫具體的ExecuteResult()方法。這樣應該就能解決我們上述問題!我們來看測試結果:

這樣,我們這個簡單的ContentResult和JsonResult就大功告成了,縱觀整個過程,ContentResult和JsonResult的實現思路是相對比較簡單的,可以說就是對當前響應流的輸出做了一些封裝而已。

三、解析檢視引擎原理

上面實現了ContentResult和JsonResult,都是針對具體的返回值型別來定義的。除此之外,在MVC裡面我們還有一個使用最多的就是和頁面html打交道的ViewResult。

1、檢視引擎原理解析

1.1、如果沒有檢視引擎,當我們希望通過一個url去請求一個頁面內容的時候,我們的實現思路首先應該就是直接在後臺拼Html,然後將拼好的Html交給響應流輸出。上面說過,這種做法太古老,開發效率低,不易排錯,並且使得前後端不能分離。這一系列的問題也反映出檢視引擎的重要性。

1.2、最簡單檢視引擎的原理:根據博主的理解,使用者通過一個Url去請求一個頁面內容的時候,我們首先定義一個靜態的html頁面,html裡面佈局和邏輯先寫好,然後通過請求的url去找到這個靜態的html,讀取靜態html裡面的文字,最後將讀取到的文字交由Response輸出給客戶端。當然,這只是一個最基礎的原理,沒有涉及模板以及模板語法,我們一步一步來,先將基礎原理搞懂,再說其他的。

1.3、在開始接觸.net裡面檢視引擎之前,博主希望根據自己的理解先自定義一個檢視引擎。說做咱就做,下面就著手來試試。

2、自定義檢視引擎

有了上面的原理做支撐,博主就來動手自己寫一個最基礎的檢視引擎試試了。

2.1、首先定義一個ViewResult去實現ActionResult

namespace Swift.MVC.MyRazor
{
    public class MyViewResult : ActionResult
    {
        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            HttpResponse response = HttpContext.Current.Response;

            response.ContentType = "text/html";

            //取當前view頁面的物理路徑
            var path = AppDomain.CurrentDomain.BaseDirectory + "Views/" + routeData.RouteValue["controller"] + "/" + routeData.RouteValue["action"] + ".html";
            var templateData = string.Empty;
            using (var fsRead = new FileStream(path, FileMode.Open))
            {
                int fsLen = (int)fsRead.Length;
                byte[] heByte = new byte[fsLen];
                int r = fsRead.Read(heByte, 0, heByte.Length);
                templateData = System.Text.Encoding.UTF8.GetString(heByte);
            }
            response.Write(templateData);
        }
    }
}

2.2、在Controller.cs裡面定義“快捷方法”

     protected virtual MyViewResult View()
        {
            return new MyViewResult();
        }

        protected virtual MyViewResult View(object data)
        {
            return new MyViewResult()
            { 
                Data = data 
            };
        }

然後在MyViewController.cs裡面新增Action

        public ActionResult ViewIndex()
        {
            return View();
        }

2.3、新增檢視ViewIndex

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    My First View
</body>
</html>

測試結果:

當然,這只是為了理解檢視引擎的工作原理而寫的一個例子,實際中的檢視引擎肯定要比這個複雜得多,至少得有模板吧,然後模板語法裡面還得定義後端資料對映到html裡面的規則,又得定義一套模板語法規則,其工作量不亞於一個開源專案,博主覺得就沒有必要再深入研究這個自定義模板了,畢竟.net裡面有許多現成並且還比較好用的模板。

關於.net下面的檢視模板引擎,博主接觸過的主要有RazorEngine、NVelocity、VTemplate等,下面博主依次介紹下他們各自的用法。

四、RazorEngine實現檢視引擎

關於.net裡面的模板引擎,RazorEngine算是相對好用的,它是基於微軟的Razor之上包裝而成的一個可以獨立使用的模板引擎。也就是說,保留了Razor的模板功能,但是使得Razor脫離於Asp.net MVC,能夠在其它應用環境下使用,換句話說,你完全可以在你的控制檯程式上面使用模板語法。

關於RazorEngine的使用以及具體的語法,園子裡面也是一搜一大把,這裡博主就不展開細說,只是將一些用到的方法介紹下。

1、基礎用法

要使用RazorEngine,首先必須要安裝元件,我們使用Nuget。

安裝完成之後就可以在我們的.net程式裡面呼叫了。

先來看一個最簡單的。

string template = "姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School";
var result = Razor.Parse(template, new { Name = "小明", Age = 16, School = "育才高中" });

我猜你已經知道結果了吧,你猜的沒錯。

是不是和MVC裡面的cshtml頁面的使用方式非常像~~它使用@Model這種作為佔位符,動態去匹配字串裡面的位置,從而得到以上結果。

除此之外,RazorEngine還提供了引擎的方式,如下。

string template = "姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School";
var result = Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "小明", Age = 16, School = "育才高中" });

結果類似:

博主除錯發現,第一次得到result的時候有點慢,查詢官方文件才知道,第一次需要記錄快取,快取的key是該方法的第二個引數“templateKey”,當第二次載入的時候基本上就飛快了。

博主詳細瞭解了下RunCompile()這個方法,它是IRazorEngineService型別的一個擴充套件方法,這裡的四個引數都有它自己的作用:第一個是匹配的字串;第二個是快取的Key,上文已經說過;第三個是一個Type型別,表示第四個引數的型別,如果為null,那麼第四個引數就為dynamic型別;第四個引數當然就是具體的實體了。

綜合上述Razor.Parse()和Engine.Razor.RunCompile()兩種方式,按照官方的解釋,第一種是原來的用法,當你使用它的時候會發現它會提示方法已經過時,官方主推的是第二種方式。博主好奇心重試了下兩種方式的區別,發現第一次呼叫的時候兩者耗時基本相似,重新整理頁面發現,Razor.Parse()每次呼叫都會耗時那麼久,而RunCompile()方式進行了快取,第一次耗時稍微多點,之後每次呼叫時間基本可以忽略不計。或許這就是官方主推第二種方式的原因吧。

看到這裡,有的小夥伴們就開始想了,既然這個模板這麼方便,那麼我們定義一個html,裡面就按照上述template變數那麼寫,然後讀取html內容,再用模板匹配html內容是不是就可以實現我們的模板要求了呢?沒錯,思路確實是這樣,我們來試一把。

var filepath = AppDomain.CurrentDomain.BaseDirectory + @"Views\" + routeData.RouteValue["controller"] + "\\" + routeData.RouteValue["action"] + ".html";
var fileContent = Engine.Razor.RunCompile(File.ReadAllText(filepath), "templateKey2", null, new { Name = "小明", Age = 16, School = "育才高中" });

然後對應的html內容如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School
</body>
</html>

得到結果

 就是這麼簡單!

2、作為檢視引擎的實現

 有了上面的一些嘗試作為基礎,將RazorEngine作為我們框架的檢視引擎就簡單了。

2.1、先定義一個ActionResult的實現類。由於快取的key必須唯一,這裡使用filepath作為快取的Key,第一次載入快取,之後訪問該頁面就很快了。

namespace Swift.MVC.MyRazor
{
    public class RazorEngineViewResult:ActionResult
    {
        public object Data { get; set; }

        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
            var filepath = AppDomain.CurrentDomain.BaseDirectory + @"Views\" + routeData.RouteValue["controller"] + "\\" + routeData.RouteValue["action"] + ".html";
            var fileContent = Engine.Razor.RunCompile(File.ReadAllText(filepath), filepath, null, Data);
            HttpResponse response = HttpContext.Current.Response;

            response.ContentType = "text/html";
            response.Write(fileContent);
            
        }
    }
}

2.2、在Controller.cs裡面定義“快捷方法”

        protected virtual RazorEngineViewResult RazorEngineView()
        {
            return new RazorEngineViewResult();
        }

        protected virtual RazorEngineViewResult RazorEngineView(object data)
        {
            return new RazorEngineViewResult()
            {
                Data = data
            };
        }

2.3、在具體的控制器裡面呼叫

    public class MyViewController:Controller
    {
       public ActionResult ViewIndex()
        {
            return RazorEngineView(new { Name = "小明", Age = 16, School = "育才高中" });
        }
    }

2.4、對應的View頁面。我們還是用html代替,當然如果你想要用cshtml的檔案,只需要改下上述檔案路徑即可。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    姓名: @Model.Name, 年齡:@Model.Age, 學校:@Model.School
</body>
</html>

得到結果

五、NVelocity實現檢視引擎

關於NVelocity模板引擎,博主簡單從網上down了一個Helper檔案。要使用它,首先還是得安裝元件

首先給出VelocityHelper

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

        /// <summary>
        /// 建構函式
        /// </summary>
        /// <param name="templatDir">模板資料夾路徑</param>
        public VelocityHelper(string templatDir)
        {
            Init(templatDir);
        }

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

        /// <summary>
        /// 初始話NVelocity模組
        /// </summary>
        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();
        }

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

        /// <summary>
        /// 顯示模板
        /// </summary>
        /// <param name="templatFileName">模板檔名</param>
        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();
        }

        /// <summary>
        /// 根據模板生成靜態頁面
        /// </summary>
        /// <param name="templatFileName"></param>
        /// <param name="htmlpath"></param>
        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();
            }

        }

        /// <summary>
        /// 根據模板生成靜態頁面
        /// </summary>
        /// <param name="templatFileName"></param>
        /// <param name="htmlpath"></param>
        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();
            }

        }
    }
VelocityHelper.cs

關於Velocity模板的語法,也沒啥好說的,直接在專案裡面將他們搭起來試試。

1、定義ActionResult的實現類VelocityViewResult

    public class VelocityViewResult:ActionResult
    {
        public object Data { get; set; }
        public override void ExecuteResult(Routing.SwiftRouteData routeData)
        {
        //這裡必須是虛擬路徑
            var velocity = new VelocityHelper(string.Format("~/Views/{0}/", routeData.RouteValue["controller"]));
            // 繫結實體model
            velocity.Put("model", Data);
            // 顯示具體html
            HttpResponse response = HttpContext.Current.Response;
            response.ContentType = "text/html";
            velocity.Display(string.Format("{0}.cshtml", routeData.RouteValue["action"].ToString()));
        }
    }

2、在Controller.cs裡面新增“快捷方法”

        protected virtual VelocityViewResult VelocityView()
        {
            return new VelocityViewResult();
        }

        protected virtual VelocityViewResult VelocityView(object data)
        {
            return new VelocityViewResult()
            {
                Data = data
            };
        }

3、在具體的控制器裡面呼叫

    public class MyViewController:Controller
    {       
        public ActionResult ViewIndex()
        {
            return VelocityView(new { Name = "小明", Age = 16, School = "育才高中" });
        }
    }

4、新建對應的檢視

上面我們在測試RazorEngine引擎的時候,使用的是html代替,這裡我們改用cshtml字尾的模板檔案ViewIndex.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title></title>
</head>
<body>
    <div>
        <h1>姓名: $model.Name</h1>
        <h1>年齡:$model.Age</h1>
        <h1>學校:$model.School</h1>
    </div>
</body>
</html>

這裡就是和RazorEngine不一樣的地方,不過很好理解,原來的@Model這裡用$model代替了而已。

測試結果