我喜欢创建单独页面的web应用,而且我将web性能优化做为我的架构中的第一级特性。这就意味着我需要在用户第一次登录网站的时候,让整个应用的标记可以获得,但是与此同时我也不希望“所有”的标记都每一次分发到每一个客户端。我能做到这一点是因为我将每个视图的标记存储在浏览器的 localStorage。几年以前我解释过其中的基本机理。
那么我怎样能在给网站做出变更的时候,可以提供以刷新后的标记呢?我用了一个cookie,其中保存了用户上一次向服务器发出请求的时间。时间是以DateTime滴答的数值形式保存的(DateTime.Now.ToUniversalTime().Ticks),这是一个long型数值。当用户请求标记,这个数值就在cookie中发送给服务器。在服务器上,在我的Razor视图中,我对每一个视图使用了最近一次访问时间值与文件的上一次更新时间做比较。代码看起来像这样(path是视图的物理文件路径,lastRead是由cookie传递过来的数值):
var fileTime = File.GetLastWriteTimeUtc(path).Ticks; if (lastRead < fileTime) { @RenderPage(path, data); }
path变量是实际的razor视图文件所在的路径。File.GetLastWriteTimeUtc方法返回了文件最后一次更新的datetime数值。之后我就可以将这个滴答形式的数值与cookie提供的数值进行比较,如果自从上一次用户向服务器请求内容之后,文件发生了改变,那么就会渲染新的文件,或者说response中就包含了新的文件。如果不是这样,就不向用户发送任何东西。这就意味着我在随后的访问中,大大的减少了标记的发送量,而且它们从来也不会遗漏对应用的变更。
这是对我如何运用技术手段来管理单页应用(Single Page Application)框架中那些标记的一个简单的解释,但是今天我想回顾一下,我是如何通过使用一个自定义的ViewWebPage来优化这个机制的。当我首次构造出这个机制的时候,是在Razor视图内部运用的,也就是说是服务器上标记文件的一个部分。当应用只有数个视图的时候这还是不错的。但随着应用规模的增长,你将会很快意识到这不是一个很易于管理的代码模式。
我决定在view类本身中重构我的代码。我此前从来也没有在这个层次上自定义过Razor视图引擎,或者任何MVC之中的东西。开始我想我只需要写一个HtmlHelper扩展方法。可是问题在于它不会渲染视图,我可以创建一个string和一个if语句来返回它,但如果你想使用一个Model的话,那就没有什么作用。我需要执行的是RenderPage, ViewWebPage类的一个成员。
这个时候,我就需要从ViewWebPage继承,并且添加一个覆盖方法来RenderPage,这样就可以接收最近一次读时间,并将其与文件最后一次更新时间做比较,而且当然的,如果有变化就渲染它。如果没有那么我就希望是不渲染或者渲染一个空的字符串。我选择了一个空字符串,因为我觉得要不然的话,给出怎样渲染空视图的算法将会麻烦得多。
浏览了一圈网站,我发现很少有能帮助实现我目标的东西。我发现两篇博客帖子,将我领上了正确的道路。第一篇是Phil Haack于2011年二月所贴,关于如何改变Razor视图的基础类型。它说明了怎样创建一个自定义的ViewWebPage,它继承自ViewWebPage并添加了一个附加的HTMLHelper。他展示了怎样创建一个自定义视图,之后再由它进一步包含进视图的模型。我不太明白第二部分,他显示了一个自定义的ViewWebPage<T>是如何工作的。
我还发现一篇帖子,Scott Allen在其中谈论了如何从ViewWebPage实现继承。在他的这个帖子中,他只是继承了传送模型的那个版本,ViewWebPage<T>。我决定运行Scott的例子,发现它运行的很好。
我遇到的代码是这样的:
namespace SPAHelper { public abstract class SPAWebViewPage<T> : WebViewPage<T> { public IHtmlString RenderPage( long lastRead, string path, params Object[] data ) { var fileTime = File.GetLastWriteTimeUtc(path).Ticks; if (lastRead < fileTime) { return base.RenderPage(path, data); } return new SPAHelperResult(); } public new ViewDataDictionary<T> ViewData { get; private set; } } }
重点看一下自定义的RenderPage方法,我增加了一个lastRead参数,通过它我就可以传送进cookie提供的滴答数值。我有点拿不定主意,是采取这个方法,还是只是简单的覆盖现存的RenderPage方法,在其内部读取cookies的数值。我决定重载这个方法,保留现存的方法,因为我可能并不是真的需要它,而且我喜欢有一个专有的版本,这样我就可以再次获取上一次的读数,并重复用于每个视图了,这会使我的页面渲染稍微更快一点。
RenderPage方法返回了一个HelperResult,这是一个自定义的IHtmlString接口的实现。我用ILSpy来检查HelperResult是如何工作的,并断定对于我的需求这个方法趋于复杂。如果页面需要渲染,我的RenderPage方法实际调用了基类的RenderPage方法并返回结果。如果不需要渲染,我返回了一个我创建的一个自定义的IHtmlString类,称为SPAHelperResult。
IHtmlString接口只有一个ToHtmlString方法。在这个类中,我只是返回一个空字符串。现在当视图不需要提供给最终用户,那么就不需要添加任何信息到MVC管道中,这意味着实际的响应要小得多。这篇博客中区别了一个39 kb请求和略低于8 kb 的请求对服务器来说是没有任何改变。
public class SPAHelperResult : IHtmlString { public string ToHtmlString() { return ""; } }
我们还没有做完,自定义ViewWebPage需要集成到网站中。这只需要改变位于网站视图文件夹下面的web.config文件来实现。在这个文件中你会发现一个<pages/ >区域页面节点上有pageBaseType属性。这个属性的值需要改为我们自定义的SPAWebViewPage。一旦完成这个配置,Razor View引擎就可以使用自定义ViewWebPage了。
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="SPAHelper.SPAWebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> </namespaces> </pages> </system.web.webPages.razor>
事实上使用自定义RenderPage方法,你可以得到cookie中的值并传值给下一个视图文件。
long lastLoad = Html.SetUpdateCookie("dt", 1, 360); @RenderPage(lastLoad, "home-view.cshtml") @RenderPage(lastLoad, "events-view.cshtml") @RenderPage(lastLoad, "about-view.cshtml")
我确实写了一个HTMLHelper扩展方法(SetUpdateCookie)来访问上一次存放的值。我将会在下一个帖子中对此回顾。
创建一个自定义ViewWebPage并没有什么稀奇的,但它也不是那么的困难。我希望这个能帮助你创建更多自定义ASP.NET Razor的视图版本。你可以从 github中的SPAHelper库 下载源代码。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务