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代替了而已。
測試結果