开始想这个标题的时候,很是忧郁到底叫什么标题会比较好哪,最后还是确定为讲解,虽然略显初级,但是很多概念,想通了还是比较有价值的。废话略过,开始!
1、MVC的基本开发流程
2、webform和MVC的选择
3、MVC的内部过程
1、MVC的开发流程
MVC的出现时微软在2009年左右开始提出的网站开发的新的发展方向,这个方面的开发官方解释是可以比较好的实现三层分离,而且分离之后,可以实现复用等相关好处,通常人们列举的例子是ui方面可以同时支持HTML网络或者WAP网络。但是其实个人的理解是,动态网站的开发经过不断地证实和发展,java的struts模型,可以提高开发速度,也可以降低差多,是比较好的框架,而微软也需要提供自己的开发框架。不能够只是一个界面一个界面的设计方式,设计模式逐步进入到了web开发的领域。
MVC使用vs2010进行开发时(这里介绍的是MVC2),首先需要选在一个模板,然后vs2010会帮忙创建好对应的目录结构。
每个目录的基本功能:
Content:主要用于描述css之类的资源
Controllers:主要就是controller的存放位置,创建controller时,都是需要在该目录创建的。
Models:主要就是entity的具体位置,以及跟entity相关的操作类
Scripts:javascript脚本存放的位置
Views:该部分主要是放置view显示部分的
Global.asax:目前来看,该部分主要就是路由设置
Web.config:该配置文件而已
从开发的流程方面来看,MVC的开发方式,或者说思考的方式出现了变化,在MVC当中,需要理解一个重要的点是:
Controller才是系统的中心,一切围绕Controller展开。
Model:所谓模型,可以理解为数据,这个数据可以是数据库中对应的表中的数据,这种数据是只有属性,而没有动作的,这种数据通常也被称之为Entity,即实体,除了这种数据之外,MODLE起始还要包括Interface,这些接口的目标是提供可以控制Entity的接口标准,然后在提供实现的载体,通常我们称之为Mock类,为了方便,可能我们还会在Model当中创建各种factory,从而简化对象的创建。
Controller:这个部分就是核心了,其实所谓核心,是说所有的处理,全部围绕着Controller展开,它的主要工作是访问model,获取数据后,将参数转发给view,然后让view表现出来。在这里主要完成的工作有两点:
1、在客户端访问一个页面后,需要跳转到Controller对应的action中去,然后在action中处理对应的view显示出来。
2、 完成客户的表单提交相应处理,也就是Form表单处理。(还记得之前讲过,对于HTML而言,只有Form表单实现了客户端的信息发送给服务端,然后由服务端处理相关的相应,因为MVC的设计目标就是放弃了微软原有的服务器控件,因此一切回归原始,采用HTML的form表单方式实现提交和相关的控制处理。)
View:顾名思义,该部分就是现实的部分,这个部分需要时刻记住的是,这个view虽然也是aspx的页面,但是已经发生了根本性的变化,不再有所谓的codebeind代码了,这个view的所有变成将采用混合式的变成,你会注意到这个部分的变成变为HTML与C#的混合,会出现很多的<%%><%=%>类似的代码。很多人起始对这个部分有不同的开发,混合代码对于分层不利,但是在MVC中,因为不涉及逻辑,所以view的表现变得简单,混合编程会变为可以接受的处理方式。另外,这种方式带来的好处是,美工可以介入了,他们的修改对于程序员来说,没有什么特别,也是非常容易直接引入的。带来的坏处是Gridview这种强大的服务器控件被丢弃了。虽然是这样,但是我个人觉得,这是回到了web开发的本质。他的思想,与JSP,PHP等等变为一致。
Golabal.asax:路由表,这个部分就是所谓的全局路由表。在MVC框架中,之所以实现了MVC功能,一个重要的概念是路由表,该部分实现了地址访问的动态,不再提供具体页面的访问模式。
注意:给我的感觉是,记住在view目录和model目录中,添加子目录,每个controller对应的view,都是一个目录下的view。这个是MVC框架查找时自动搜索的。
2、webform和MVC的选择
这个部分的争论,我想从微软开始推出MVC框架后,大家就在不间断的讨论着,不同的人,给出的看法也是不同,就我个人而言,我觉得MVC才是未来趋势,是世界最后大同的根本。尽管web form的模式,是微软开创性的创造,但是毕竟web开发不是微软首创,很多时候,大势所趋而已。我这里只是想谈谈两者的思想出发点的差别:
- webform模式,这个模式的思维基础,是微软在桌面开发中取得了前所未有的成功,这些成功,微软希望复制到网络开发中,何为form,就是窗口开发,这种框架的逻辑是所见即所得+事件处理,微软希望可以将web实现为桌面开发的模式,但是网络开发的基础是HTML和HTTP协议,这两个部分带来的问题是HTML表现元素有限,并且只能够通过form与后台服务器通信。另外,HTTP协议无状态,无法实现消息机制,为了解决这些问题,微软创造了新的开发模式,引入ASP.NET服务器控件,实现了丰富的控件,通过postback回传机制,实现了事件模型,通过codebehind技术实现web页面与C#代码的分离。上述技术,的确非常成功,也确实很大程度上简化了web的开发,但是随着发展,带来了问题,就是webform的开发基础是页面化的,这种思维模式是说你开一个页面,然后在这个页面写响应事件,但是这种模式对于频繁变化的web程序缺乏良好的复用性,并且,前端人员开发的界面,往往在合成时,需要重做,这是微软自己创造的困难,这是一种以Page为中心的开发思想。
- MVC模式,这个模式的思维基础,是分工清晰,以Controller为核心,在开发时可以先做model再做Controller,最后做view,通过使用demo view实现,最后再替换美工的view。这种模式变成了以数据为中心的开发思想。最大的好处是,这种模式中每个部分都可以灵活复用,最大限度的实现现在的各种网络需要,比如互联网和移动互联网。而且,其他的变成语言,在思想方面也基本采用这种模式,这种模式最终被时间证明,成为了标准思考方式和开发方式。微软提倡的桌面化开发,渐渐退却往日之光芒。
3、MVC的内部过程
这个部分是个非常核心的问题,本文除了自己理解,还大量引用了其他相关的文章。尝试讲解清楚MVC的基本运转流程。
MVC的主体过程:
问题:
1、 浏览器请求的地址,并不是具体的某个页面,如1234.aspx页面,而是controller/action方法,这是如何做到的?
2、 Controller被访问到以后,如何找到具体的view进行返回的?
我个人的理解就是回答了上述的问题,也就解释清楚了MVC的基本框架。
第一个问题,浏览器请求地址的问题,MVC之所以能够找到具体的Controller是因为有一个route组件,实现了路由处理的功能,将请求转化为Controller的具体方法。需要注意该组件居然是个独立组件。
Routing的作用:
1、 解析URL,识别当中的参数
2、 解析之后,调用具体的controller和action
比如首页地址是: localhost/home/index
我们发现访问上面的地址, 最后会传递给 HomeController中名为index的action(即HomeController类中的index方法).
当然服务器端不会自己去实现这个功能, 关键点就是在Global.asax.cs文件中的下列代码:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
回来看我们的Url: localhost/home/index
localhost是域名, 所以首先要去掉域名部分: home/index
对应了上面代码中的这种URL结构:{controller}/{action}/{id}
因为我们建立了这种Url结构的识别规则, 所以能够识别出 Controller是home,action是index,id没有则为默认值"".
上述功能之所以能够实现,关键在MapRoute方法,虽然MapRoute方法是RouteCollection对象的方法,但是却被放置在System.Web.Mvc程序集中, 如果你的程序只引用了System.Web.Routing, 那么RouteCollection对象是不会有MapRoute方法的. 但是如果你同又引用了System.Web.Mvc, 则在mvc的dll中为RouteCollection对象添加了扩展方法:
public static void IgnoreRoute(this RouteCollection routes, string url);
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults);
public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
RouteCollection是一个集合,他的每一项应该是一个Route对象. 但是我们使用MapRoute时并没有创建这个对象, 这是因为当我们将MapRoute方法需要的参数传入时, 在方法内部会根据参数创建一个Route对象:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces) {
if (routes == null) {
throw new ArgumentNullException("routes");
}
if (url == null) {
throw new ArgumentNullException("url");
}
Route route = new Route(url, new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
if ((namespaces != null) && (namespaces.Length > 0)) {
route.DataTokens = new RouteValueDictionary();
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
上面就是MapRoute方法的实现, 至于在创建Route对象时第二个参数是一个MvcRouteHandler, 它是一个实现了IRouteHandler接口的类. IRouteHandler十分简单只有一个方法:
IHttpHandler GetHttpHandler(RequestContext requestContext);
参数是一个RequestContext 类实例, 这个类的结构也很简单:
public class RequestContext
{
public RequestContext(HttpContextBase httpContext, RouteData routeData);
public HttpContextBase HttpContext { get; }
public RouteData RouteData { get; }
}
其中的一个属性RouteData就包含了Routing根据Url识别出来各种参数的值, 其中就有Controller和Action的值.
归根结底, ASP.NET MVC最后还是使用HttpHandler处理请求. ASP.NET MVC定义了自己的实现了IHttpHandler接口的Handler:MvcHandler, 因为MvcRouteHandler的GetHttpHandler方法最后返回的就是MvcHandler.
MvcHandler的构造函数需要传入RequestContext 对象, 也就是传入了所有的所有需要的数据, 所以最后可以找到对应的Controller和Action, 已经各种参数.
(引用参考:http://www.cnblogs.com/zhangziqiu/archive/2009/02/28/ASPNET-MVC-2.html)
(引用参考:http://www.cnblogs.com/zhangziqiu/archive/2009/03/11/Aspnet-MVC-3.html)
第二个问题:Controller找到了,Action也找到了,此时如何哪?
下面分层次的总结Controller处理流程:
1. 页面处理流程
发送请求 –> UrlRoutingModule捕获请求 –>MvcRouteHandler.GetHttpHandler() –> MvcHandler.ProcessRequest()
2.MvcHandler.ProcessRequest() 处理流程:
使用工厂方法获取具体的Controller –> Controller.Execute() –> 释放Controller对象
3.Controller.Execute() 处理流程
获取Action –> 调用Action方法获取返回的ActionResult –> 调用ActionResult.ExecuteResult() 方法
4.ActionResult.ExecuteResult() 处理流程
获取IView对象-> 根据IView对象中的页面路径获取Page类-> 调用IView.RenderView() 方法(内部调用Page.RenderView方法)
通过对MVC源代码的分析,我们了解到Controller对象的职责是传递数据,获取View对象(实现了IView接口的类),通知View对象显示.View对象的作用是显示.虽然显示的方法RenderView()是由Controller调用的,但是Controller仅仅是一个"指挥官"的作用, 具体的显示逻辑仍然在View对象中.需要注意IView接口与具体的ViewPage之间的联系.在Controller和View之间还存在着IView对象.对于ASP.NET程序提供了WebFormView对象实现了IView接口.WebFormView负责根据虚拟目录获取具体的Page类,然后调用Page.RenderView().
引用参考:(Http://www.cnblogs.com/zhangziqiu/archive/2009/03/11/Aspnet-MVC-3.html)
讲到这里,相信很多人开始似乎明白了,又似乎不明白了,下面我做进一步的讲解,先看一下,通常Controller的实现如下:
public class HomeController:Controller
{
public ActionResult Index()
{
Return View(“Index”);
}
}
先看看关键类ActionResult,这个返回值,体现了微软精心设计,为什么做这么个类,其实本质而言,微软希望这个action可以返回更多内容,而不仅仅是view。
类名 |
抽象类 |
父类 |
功能 |
ContentResult |
|
|
根据内容的类型和编码,数据内容. |
EmptyResult |
|
|
空方法. |
FileResult |
abstract |
|
写入文件内容,具体的写入方式在派生类中. |
FileContentResult |
|
FileResult |
通过文件byte[] 写入文件. |
FilePathResult |
|
FileResult |
通过文件路径写入文件. |
FileStreamResult |
|
FileResult |
通过文件Stream 写入文件. |
HttpUnauthorizedResult |
|
|
抛出401错误 |
JavaScriptResult |
|
|
返回javascript文件 |
JsonResult |
|
|
返回Json格式的数据 |
RedirectResult |
|
|
使用Response.Redirect重定向页面 |
RedirectToRouteResult |
|
|
根据Route规则重定向页面 |
ViewResultBase |
abstract |
|
调用IView.Render() |
PartialViewResult |
|
ViewResultBase |
调用父类ViewResultBase 的ExecuteResult方法. |
ViewResult |
|
ViewResultBase |
调用父类ViewResultBase 的ExecuteResult方法. |
这里我们主要讲解viewResult的作用。
在ASP.NETMVC中,ViewResult用的最多,Controller有一个View方法,它来实例化一个ViewResult对象,并返回。
下面是View方法:
protected internal virtual ViewResult View(string viewName, string masterName, object model) { if (model != null) { ViewData.Model = model; } return new ViewResult { ViewName = viewName, MasterName = masterName, ViewData = ViewData, TempData = TempData }; }
ViewResult类的ExecuteResult方法的具体参考如下:
public override void ExecuteResult(ControllerContext context) { if (context == null) { throw new ArgumentNullException("context"); } if (String.IsNullOrEmpty(ViewName)) { ViewName = context.RouteData.GetRequiredString("action"); } ViewEngineResult result = null; if (View == null) { result = FindView(context); // 很关键,找到具体的view View = result.View; } ViewContext viewContext = new ViewContext(context, View, ViewData, TempData);
// 很关键,渲染自己 View.Render(viewContext, context.HttpContext.Response.Output);
if (result != null) { result.ViewEngine.ReleaseView(context, View); } }
那么如何FindView哪?具体如下:
rotectedoverrideViewEngineResult FindView(ControllerContext context) {
ViewEngineResult result =ViewEngineCollection.FindView(context, ViewName, MasterName);
if (result.View != null) {
return result;
}
//we need to generate an exception containing all the locations we searched
StringBuilder locationsText = new StringBuilder();
foreach (string location in result.SearchedLocations) {
locationsText.AppendLine();
locationsText.Append(location);
}
throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture,
MvcResources.Common_ViewNotFound,ViewName, locationsText));
}
从ViewResult类的FindView方法中,得知ViewEngineResult是通过ViewEngineCollection的FindView得到的,而ViewEngineCollection正是ViewEngines的静态属性Engines,Engines返回一个只有一个WebFormViewEngine类型实例的一个集合。所以,ViewEngineResult会是调用WebFormViewEngine类的FindView方法返回的结果。如果ViewEngins的静态属性Engines有多个ViewEngine提供,那么就依次遍历它们直到找到第一个不为空的ViewEngineResult为止。这样我们就可以在同一个MVC网站中使用多种视图引擎了。
静态类ViewEngines的描述如下:
public static class ViewEngines
{
private static readonly ViewEngineCollection _engines = new ViewEngineCollection { new WebFormViewEngine(), new RazorViewEngine() };
public static ViewEngineCollection Engines
{
get { return _engines;}
}
}
public class ViewEngineCollection : Collection<IViewEngine>
{
//其他成员
public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName);
public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName);
}
从上述例子可以看出,起始微软为我们提供了两个ViewEngine, WebFormViewEngine和RazorViewEngine,WebFormViewEngine对应的是ASPX界面,RazorViewEngine对应的是.cshtml/.vbhtml引擎
此外,这里有一个隐藏很深的概念,似乎很多书都没讲清楚,每一个引擎都会对应一个view,作为页面渲染使用,对于viewengine做进一步的解释,就是说,为什么一个view当中的<%%>之类的界面元素最后可以变成html,就是这些引擎在起作用,他们的内部实现,可以提供类似正则表达式或者是页面parsing的方法,完成页面字符串解析,从而转换为对应的html,再response给客户端浏览器。
微软提供的两种viewengine和view如下:
RazorViewEngine和Razorview
(参考引用:http://www.cnblogs.com/artech/archive/2012/09/05/razor-view-engine-02.html)
WebformViewengine和Webformview
通过上边的讲述,基本的概念已经讲清楚了,如果希望实现自己的viewengine,可以查看一下微软参考实现是如何做到的,然后我们就可以防治自己的viewengine了。这里便需要进一步说明的是,为什么MVC需要viewengine,而WEBFORM不需要,是因为,微软的webform是直接通过IHttphandler处理了,也就是说ASP.NETWEBFORM模式中的HTTPHANDLER完成了事件处理和页面显示的双重功能。而ASP.NETMVC没有事件处理,因此页面显示被划到了viewengine当中实现了。事件处理,被controller替换处理了。
微软的Razorview和Webformview本质上是实现于IView接口,而WebformViewengine和Webformview本质上是实现于IViewEngine
下面介绍他们的实现逻辑:
public interface IView
2: {
3: void Render(ViewContext viewContext, TextWriter writer);
4: }
5:
6: public class ViewContext : ControllerContext
7: {
8: //其他成员
9: public virtual bool ClientValidationEnabled { get; set; }
10: public virtual bool UnobtrusiveJavaScriptEnabled { get; set; }
11:
12: public virtual TempDataDictionary TempData { get; set; }
13: [Dynamic]
14: public object ViewBag { [return: Dynamic] get; }
15: public virtual ViewDataDictionary ViewData { get; set; }
16: public virtual IView View { get; set; }
17: public virtual TextWriter Writer { get; set; }
18: }
19:
20: public abstract class HttpResponseBase
21: {
22: //其他成员
23: public virtual TextWriter Output { get; set; }
24: }
1: public interface IViewEngine
2: {
3: ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache);
4: ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache);
5: void ReleaseView(ControllerContext controllerContext, IView view);
6: }
1: public class ViewEngineResult
2: {
3: public ViewEngineResult(IEnumerable<string> searchedLocations);
4: public ViewEngineResult(IView view, IViewEngine viewEngine);
5:
6: public IEnumerable<string> SearchedLocations { get; }
7: public IView View { get; }
8: public IViewEngine ViewEngine { get; }
9: }
1: public class ViewResult : ViewResultBase
2: {
3: protected override ViewEngineResult FindView(ControllerContext context);
4: public string MasterName { get; set; }
5: }
6:
7: public abstract class ViewResultBase : ActionResult
8: {
9: public override void ExecuteResult(ControllerContext context);
10: protected abstract ViewEngineResult FindView(ControllerContext context);
11:
12: public object Model { get; }
13: public TempDataDictionary TempData { get; set; }
14: [Dynamic]
15: public object ViewBag { [return: Dynamic] get; }
16: public ViewDataDictionary ViewData { get; set; }
17: public string ViewName { get; set; }
18: public ViewEngineCollection ViewEngineCollection { get; set; }
19: public IView View { get; set; }
20: }
(参考引用:http://www.cnblogs.com/artech/archive/2012/08/22/view-engine-01.html)
自定义的View以及相关的Viewengine,参考如下:
public class StaticFileView:IView
2: {
3: public string FileName { get; private set; }
4: public StaticFileView(string fileName)
5: {
6: this.FileName = fileName;
7: }
8: public void Render(ViewContext viewContext, TextWriter writer)
9: {
10: byte[] buffer;
11: using (FileStream fs = new FileStream(this.FileName, FileMode.Open))
12: {
13: buffer = new byte[fs.Length];
14: fs.Read(buffer, 0, buffer.Length);
15: }
16: writer.Write(Encoding.UTF8.GetString(buffer));
17: }
18: }
internal class ViewEngineResultCacheKey
2: {
3: public string ControllerName { get; private set; }
4: public string ViewName { get; private set; }
5:
6: public ViewEngineResultCacheKey(string controllerName, string viewName)
7: {
8: this.ControllerName = controllerName ?? string.Empty;
9: this.ViewName = viewName ?? string.Empty;
10: }
11: public override int GetHashCode()
12: {
13: return this.ControllerName.ToLower().GetHashCode() ^ this.ViewName.ToLower().GetHashCode();
14: }
15:
16: public override bool Equals(object obj)
17: {
18: ViewEngineResultCacheKey key = obj as ViewEngineResultCacheKey;
19: if (null == key)
20: {
21: return false;
22: }
23: return key.GetHashCode() == this.GetHashCode();
24: }
25: }
1: public class StaticFileViewEngine : IViewEngine
2: {
3: private Dictionary<ViewEngineResultCacheKey, ViewEngineResult> viewEngineResults = new Dictionary<ViewEngineResultCacheKey, ViewEngineResult>();
4: private object syncHelper = new object();
5: public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
6: {
7: return this.FindView(controllerContext, partialViewName, null, useCache);
8: }
9:
10: public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
11: {
12: string controllerName = controllerContext.RouteData.GetRequiredString("controller");
13: ViewEngineResultCacheKey key = new ViewEngineResultCacheKey(controllerName, viewName);
14: ViewEngineResult result;
15: if (!useCache)
16: {
17: result = InternalFindView(controllerContext, viewName, controllerName);
18: viewEngineResults[key] = result;
19: return result;
20: }
21: if(viewEngineResults.TryGetValue(key, out result))
22: {
23: return result;
24: }
25: lock (syncHelper)
26: {
27: if (viewEngineResults.TryGetValue(key, out result))
28: {
29: return result;
30: }
31:
32: result = InternalFindView(controllerContext, viewName, controllerName);
33: viewEngineResults[key] = result;
34: return result;
35: }
36: }
37:
38: private ViewEngineResult InternalFindView(ControllerContext controllerContext, string viewName, string controllerName)
39: {
40: string[] searchLocations = new string[]
41: {
42: string.Format( "~/views/{0}/{1}.shtml", controllerName, viewName),
43: string.Format( "~/views/Shared/{0}.shtml", viewName)
44: };
45:
46: string fileName = controllerContext.HttpContext.Request.MapPath(searchLocations[0]);
47: if (File.Exists(fileName))
48: {
49: return new ViewEngineResult(new StaticFileView(fileName), this);
50: }
51: fileName = string.Format(@"\views\Shared\{0}.shtml", viewName);
52: if (File.Exists(fileName))
53: {
54: return new ViewEngineResult(new StaticFileView(fileName), this);
55: }
56: return new ViewEngineResult(searchLocations);
57: }
58:
59: public void ReleaseView(ControllerContext controllerContext, IView view)
60: { }
61: }
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: //其他操作
6: ViewEngines.Engines.Insert(0, new StaticFileViewEngine());
7: }
8: }
1: public class HomeController : Controller
2: {
3: public ActionResult ShowNonExistentView()
4: {
5: return View("NonExistentView");
6: }
7:
8: public ActionResult ShowStaticFileView()
9: {
10: return View();
11: }
12: }
我们为Action方法ShowStaticFileView创建一个StaticFileView类型的View文件ShowStaticFileView.shtml(该View文件保存在“~/Views/Home”目录下,扩展名不是.cshtml,而是shtml),其内容就是如下一段完整的HTML。
1: <!DOCTYPE html>
2: <html>
3: <head>
4: <title>Static File View</title>
5: </head>
6: <body>
7: 这是一个自定义的StaticFileView!
8: </body>
9: </html>
(参考引用:http://www.cnblogs.com/artech/archive/2012/08/23/view-engine-02.html)