MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)
前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配、再到控制器的激活,再到Action的执行这些个过程。今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾。对于这个系列,通过学习源码,博主也学到了很多东西,在此还是把博主知道的先发出来,供大家参考。
本文原创地址:http://www.cnblogs.com/landeanfen/p/6019719.html
MVC源码学习系列文章目录:
- MVC系列——MVC源码学习:打造自己的MVC框架(一)
- MVC系列——MVC源码学习:打造自己的MVC框架(二:附源码)
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
- MVC系列——MVC源码学习:打造自己的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); } } } }
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, } }
代码不难理解,就是定义了当前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是一个独立的开源项目,项目的地址是https://github.com/Antaris/RazorEngine
关于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(); } } }
关于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代替了而已。
测试结果
六、VTemplate实现视图引擎
VTemplate模板引擎简称VT,好几年前就听说过这么一个东西,但一直没研究它的语法,感觉写起来太麻烦,详情可以看看 这里。这里也不想展开说明了,因为觉得原理通了,实现起来就是模板语法的不同了,大家如果有兴趣可以自己去实现一套VT版本的视图引擎。
七、总结
总结,又到了写总结的时间了,好紧张~~这一篇主要解析了下MVC里面View的原理以及使用模板引擎的实现,至此,我们自己的MVC基本功能都已经有了,此系列暂时告一段落吧。还是给出源码地址:源码下载。
此系列文章,花了很多时间整理,但是也收到很多园友的打赏,在此感谢大家对博主的支持和厚爱。后续博主一定继续努力,将更好的干货带给大家!
如果你觉得本文能够帮助你,可以右边随意 打赏 博主,也可以 推荐 进行精神鼓励。你的支持是博主继续坚持的不懈动力。
本文原创出处:http://www.cnblogs.com/landeanfen/
欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利