说到网站性能优化,没有什么比“缓存”更重要了。即便是某些朋友口中念念不忘的“静态页”,说到底也只是缓存了整张页面内容而已。但是,显然这样大粒度的缓存策略,在如今“牵一发而动全身”的Web 2.0站点中几乎是无法使用的。试想,在Twitter中的某个名人被数十万人订阅,那么他发一条消息,难道此时网站要去修改数十万用户的静态页面?因此,我们需要粒度更小的缓存。而比“整页缓存”粒度小一号的缓存,便是所谓“视图片断缓存”了。
视图片断缓存非常重要,因为它缓存的也是页面内容,这表示它比更低级别的缓存更有效率,也比静态页等整页内容缓存的适用面要大得多。在ASP.NET WebForm模型中提供了控件级别的缓存,我们可以为控件标记输出缓存策略,这样控件便不会每次都完整执行一遍。当然这个策略还不够灵活,因为它缓存的最小单元是“控件”,而不是页面中任意的部分。因此我在一年多前提出了一个CachePanel,由它包装的页面内容都可以被缓存,无论其内部是控件还是普通输出的内容。在实际生产过程中,CachePanel起到了非常重要的作用,许多场景下只要在页面中包裹一个<ext:CachePanel runat="server" />,性能立即就有了质的飞跃。
只可惜,在如今ASP.NET MVC的时代无法直接使用CachePanel这样的服务器端控件。因为CachePanel需要服务器端代码的配合,而ASP.NET MVC中的页面只是“视图模板”,除了呈现之外就不应该有其他职责。因此,我们必须提出一种脱离于后端代码的“标记”方式,将视图中的内容片断进行随意地缓存。在Rails或Django中都有类似的特性,但ASP.NET MVC甚至在2.0的Road Map中还没有包含这一功能,于是我们只能自己动手丰衣足食。不过有了ASP.NET WebForm作为强大的视图引擎,加这样的功能简直是举手之劳:
public static class CacheExtensions { public static string Cache( this HtmlHelper htmlHelper, string cacheKey, CacheDependency cacheDependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, Func<object> func) { var cache = htmlHelper.ViewContext.HttpContext.Cache; var content = cache.Get(cacheKey) as string; if (content == null) { content = func().ToString(); cache.Insert(cacheKey, content, cacheDependencies, absoluteExpiration, slidingExpiration); } return content; } }
我们为HtmlHelper增加了一个Cache扩展方法,接受一些缓存参数(缓存键,绝对过期时间,偏移过期时间),以及一个生成缓存内容的Func<object>委托。Cache方法的逻辑非常简单:首先根据缓存键来获取内容,如果存在则直接返回,否则即调用委托对象获得新内容,并将其放入缓存。这样在缓存命中的情况下,委托的开销便可以节省下来了。
例如,我们可以使用这样的代码进行测试:
Before Rendering: <%= DateTime.Now %> <br /> Rendering: <%= Html.Cache("Now", null, DateTime.Now.AddSeconds(60), Cache.NoSlidingExpiration, () => { System.Threading.Thread.Sleep(5000); return DateTime.Now; }) %> <br /> After Rendering: <%= DateTime.Now %>
在实际情况中,我们是不会在代码中调用Thread.Sleep方法的,不过这里我们需要模拟一段开销,因此通过暂停当前线程来实现时间消耗。于是我们第一次打开页面:
Before Rendering: 2009/9/17 16:52:37 Rendering: 2009/9/17 16:52:42 After Rendering: 2009/9/17 16:52:42
从结果中可以看出,Before Rendering和After Rendering相差了5秒钟,这就是Thread.Sleep(5000)的效果。但是如果您在60秒以内再次刷新页面,便可以看到缓存的效果:
Before Rendering: 2009/9/17 16:52:55 Rendering: 2009/9/17 16:52:42 After Rendering: 2009/9/17 16:52:55
可以看出,Rendering阶段显示的还是刚才的时间,而Before Rendering和After Rendering是即时更新的。此外,由于Cache方法将Thread.Sleep(5000)的开销节省了下来,因此Before Rendering和After Rendering两个阶段打印出的时间完全相同。
怎么样,简单吧。不过您应该会感到疑惑,这不是我们想要的结果啊,我们想缓存的是页面上的一个片断,但是现在必须将被缓存的内容作为一个完整的字符串输出,那么我们又该如何实现呢?难道我们要这么写吗?
<%= Html.Cache(..., () => "<span style=\"color:red;\">" + Model.Title + "</span>") %>
当然不可能这样。如果只是这样的话,那么这个Cache的可用性毫无疑问会非常低。因此,我们还需要寻找更好的解决方案——关于这点,我们下次再聊。而目前的Cache方法,最方便的输出大端页面内容的做法则是将内容放在一个Partial View中,然后使用Html.Partial方法输出内容:
<%= Html.Cache(..., () => Html.Partial("MyPartialViewToCache")) %>
请注意,我们这里使用的是扩展后的Partial方法,而不是自带的RenderPartial。之前我们谈过WebForm页面的输出方式,而RenderPartial方法是直接向Response.Output输出页面内容,因此我们无法将其捕捉为一个字符串。不过,之前文章中的Partial方法是“山寨”版本,而符合“标准”的Partial方法实现已经包含在MvcPatch项目中。如果您感兴趣的话,可以获取它的源代码并编译。我这段时间在一部分一部分地将以前项目中较为通用的扩展及修改提取至MvcPatch中,希望可以使MvcPatch成为一个可复用的强大组件。