Hello World
Spiga

适合ASP.NET MVC的视图片断缓存方式(中):更实用的API

2009-09-21 15:49 by 老赵, 13043 visits

上一篇文章中我们提出了了片断缓存的基本方式,也就是构建HtmlHelper的扩展方法Cache,接受一个用于生成字符串的委托对象。在缓存命中时,则直接返回缓存中的字符串片断,否则则使用委托生成的内容。因此,缓存命中时委托的开销便节省了下来。不过这个方法并不实用,如果您要缓存大片的HTML,还需要准备一个Partial View,再用它来生成网页片段:

<%= Html.Cache(..., () => Html.Partial("MyPartialViewToCache")) %>

但是在实际开发过程中,我们最乐于看到的使用方法,应该只是使用某个标记来“围绕”一段现有的代码。也就是说,我们希望的API使用方式可能是这样的:

<% Html.Cache("cache_key", DateTime.Now.AddSeconds(10), () => { %>

    <% foreach (var article in Model.Articles) { %> 
        <p><%= article.Body %></p>
    <% } %>
    
<% }); %>

我们可以从这种“表现形式”上推断出这个Cache方法的签名:

public static void Cache(
    this HtmlHelper htmlHelper,
    string cacheKey,
    CacheDependency cacheDependencies,
    DateTime absoluteExpiration,
    TimeSpan slidingExpiration,
    Action action)
{
    ...
}

与前一个扩展相比,最后一个委托参数变成了Action,而不是Func<string>。这是因为ASP.NET页面在编译时,会将页面Cache块中的代码,编译为内容的输出方式——这点在之前的文章中已经有过比较详细的描述。不过有一点还是与之前相同的,我们要省下的是action委托的开销。也就是说,如果缓存命中,则不执行action。缓存没有命中,则执行action,获得action生成的字符串,加入缓存并输出。

看似比较简单,但这里有个问题:如之前的Func<string>参数,我们执行后自然可以获得一个字符串作为结果。但是现在是个action,执行后它又把内容输出到什么地方去,我们又该如何得到这里生成的字符串呢?根据页面输出行为,我们可以推断出页面上的内容是被写入一个HtmlTextWriter中的。那么,这个HtmlTextWriter又是如何生成的呢?

它是根据Page类型的CreateHtmlTextWriter方法生成的:

protected virtual HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw) { ... }

在页面准备生成内容之前,Page会调用其CreateHtmlTextWriter来包装一个TextWriter,这个TextWriter一般即是由Response.Output暴露出来的HttpWriter对象。CreateHtmlTextWriter方法生成的HtmlTextWriter,便会交给Page的Render方法用于输出页面内容了。这便是我们的入手点,我们可以趁此机会在HtmlTextWriter和CreateHtmlTextWriter之间“插入”一个组件。这个组件除了将外部传入的数据传入内部的TextWriter以外,还有着“纪录”内容的功能:

internal class RecordWriter : TextWriter
{
    public RecordWriter(TextWriter innerWriter)
    {
        this.m_innerWriter = innerWriter;
    }

    private TextWriter m_innerWriter;
    private List<StringBuilder> m_recorders = new List<StringBuilder>();

    public override Encoding Encoding
    {
        get { return this.m_innerWriter.Encoding; }
    }

    public override void Write(char value) { ... }

    public override void Write(string value)
    {
        if (value != null)
        {
            this.m_innerWriter.Write(value);

            if (this.m_recorders.Count > 0)
            {
                foreach (var recorder in this.m_recorders)
                {
                    recorder.Append(value);
                }
            }
        }
    }

    public override void Write(char[] buffer, int index, int count) { ... }

    public void AddRecorder(StringBuilder recorder)
    {
        this.m_recorders.Add(recorder);
    }

    public void RemoveRecorder(StringBuilder recorder)
    {
        this.m_recorders.Remove(recorder);
    }
}

一个TextWriter有数十个可以覆盖的成员,但是一般情况下我们只需覆盖其中三个Write方法就可以了。以上代码用Write(string)作为示例,可以看出,如果RecordWriter中添加了Recorder之后,便会将外界写入的内容再交给Recorder一次。换句话说,如果我们希望纪录页面上写入Writer的内容,只要在RecordWriter里添加Recorder就可以了。当然,在此之前我们需要为视图页面“开启”缓存功能:

// 定义在CacheExtensions中
public static TextWriter CreateCacheWriter(this HtmlHelper htmlHelper, TextWriter writer)
{
    var recordWriter = new RecordWriter(writer);
    htmlHelper.SetRecordWriter(recordWriter);
    return recordWriter;
}

// 定义在视图页面(aspx)中
<script runat="server">
    protected override HtmlTextWriter CreateHtmlTextWriter(System.IO.TextWriter tw)
    {
        return base.CreateHtmlTextWriter(Html.CreateCacheWriter(tw));
    }
</script>

当然,在实际开发过程中不会在aspx中重写CreateHtmlTextWriter方法,我们往往会将其放在视图页面的共同基类中。例如在我的项目中,我就为所有的视图“开启”了这种纪录功能。由于在没有缓存的情况下这层薄薄的封装只是在做一个“转发”功能,因此不会带来性能问题。

此时,新的Cache方法便非常直观了:

public static void Cache(
    this HtmlHelper htmlHelper,
    string cacheKey,
    CacheDependency cacheDependencies,
    DateTime absoluteExpiration,
    TimeSpan slidingExpiration,
    Action action)
{
    var cache = htmlHelper.ViewContext.HttpContext.Cache;
    var content = cache.Get(cacheKey) as string;
    var writer = htmlHelper.GetRecordWriter();

    if (content == null)
    {
        var recorder = new StringBuilder();
        writer.AddRecorder(recorder);

        action();

        writer.RemoveRecorder(recorder);
        content = recorder.ToString();
        cache.Insert(cacheKey, content, cacheDependencies, absoluteExpiration, slidingExpiration);
    }
    else
    {
        htmlHelper.Output.Write(content);
    }
}

如果缓存没有命中,则我们会向RecordWriter中添加一个Recorder,然后再执行action委托,这样action中的所有内容便会被纪录下来。action执行完毕后,我们再摘除Recorder即可。现在Cache方法已经可用了,例如:

<%= DateTime.Now %>
<br />

<% Html.Cache("now", DateTime.Now.AddSeconds(5), () => { %>

    <%= DateTime.Now %>
    
<% }); %>

那么,Html.Cache能否嵌套呢?答案也是肯定的。

<%= DateTime.Now %>
<br />

<% Html.Cache("now", DateTime.Now.AddSeconds(5), () => { %>

    <%= DateTime.Now %>
    <br />
    
    <% Html.Cache("inner_now", DateTime.Now.AddSeconds(10), () => { %>
    
        <% Html.RenderPartial("CurrentTime"); %>
    
    <% }); %>
    
<% }); %>

外层缓存块5秒后过期,内存缓存块10秒钟过期,因此在某一时刻(如第一次刷新后7秒后),您会发现页面上会出现这样的结果:

2009/9/21 15:36:10 
2009/9/21 15:36:08 
2009/9/21 15:36:03

我们的RecordWriter支持同时拥有多个recorder,您可以根据上面得出的结果来理解内外层循环是以何种顺序向RecordWriter添加Recorder的,这并不困难。

从代码中我们也可以发现,Cache块内部也可以直接使用Html.RenderPartial。您也可以在Cache块内部使用各种辅助方法,它们的结果会被一并缓存下来。

不过它们还是有“前提”的,至于这个前提是什么,我们下次在讨论吧。如果您想先睹为快,可以关注MvcPatch项目。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

36 条回复

  1. 青羽
    *.*.*.*
    链接

    青羽 2009-09-21 15:57:00

    现在还在webform上开发。

  2. 老赵
    admin
    链接

    老赵 2009-09-21 15:58:00

    @青羽
    这个解决方案和思路完全可以用在webform上。

  3. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-09-21 16:00:00

    除了顶,我还能说什么呢

  4. 第一控制.NET
    *.*.*.*
    链接

    第一控制.NET 2009-09-21 16:05:00

    有一点不明白啊。foreach (var article in Model.Articles)这里的article应该是在Controller里面就加载好了吧。这样缓存过以后能省掉加载这部分开销不?还是值省掉了生产html这部分?

  5. 老赵
    admin
    链接

    老赵 2009-09-21 16:09:00

    @第一控制.NET
    按照Rails的做法,可以在Controller里判断缓存是否过期,如果过期了才加载Articles。
    我认为较理想的做法是:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/05/simple-over-complex.html

  6. JiaruiStone
    *.*.*.*
    链接

    JiaruiStone 2009-09-21 16:13:00

    MVC还在入门阶段,没怎么看懂...

  7. 老赵
    admin
    链接

    老赵 2009-09-21 16:16:00

    @JiaruiStone
    其实这两篇和MVC没有必然联系,主要是在操作ASP.NET。

  8. JiaruiStone
    *.*.*.*
    链接

    JiaruiStone 2009-09-21 16:52:00

    这个十一啥也不干了,把您blog上的mvc相关的技术文章一定好好的全都读一遍,遇到不明白的还请不吝赐教啊,^_^

  9. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 17:11:00

    这个缓存的书写方式 以前没想过阿!
    <% Html.Cache("cache_key", DateTime.Now.AddSeconds(10), () => { %>

    <%})%>

  10. 老赵
    admin
    链接

    老赵 2009-09-21 17:20:00

    @韦恩卑鄙
    在我看来这种写法还是相当漂亮的。

  11. monkey-猴子
    *.*.*.*
    链接

    monkey-猴子 2009-09-21 17:38:00

    我知道你想推IE7 IE8 Firefox 是对的~因为IE6 实在是太烂了。
    但是 中国建设银行网络银行 偏偏就不支持IE7。
    它的签约证书,只支持IE6.你说怎么办? 是用户去适应网站 还是叫网站去适应用户。 这是中国。

  12. Leven
    *.*.*.*
    链接

    Leven 2009-09-21 17:46:00

    这个方式真有意思....
    受到启发了

  13. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-09-21 17:59:00

    Cool 极了!

  14. egmkang
    *.*.*.*
    链接

    egmkang 2009-09-21 18:27:00

    @monkey-猴子
    那你就换农行的,我Windows Server 2008自带的IE,可以跑农行的证书.

  15. qmxle
    *.*.*.*
    链接

    qmxle 2009-09-21 18:32:00

    可嵌套使用,相当酷!

  16. 王德水
    *.*.*.*
    链接

    王德水 2009-09-21 20:01:00

    创意,创意很好

  17. .msnet[未注册用户]
    *.*.*.*
    链接

    .msnet[未注册用户] 2009-09-21 21:22:00

    可悲!!!

  18. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 21:28:00

    monkey-猴子:
    我知道你想推IE7 IE8 Firefox 是对的~因为IE6 实在是太烂了。
    但是 中国建设银行网络银行 偏偏就不支持IE7。
    它的签约证书,只支持IE6.你说怎么办? 是用户去适应网站 还是叫网站去适应用户。 这是中国。


    firefox 不影响ie6哦 弄个吧 :P

  19. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 21:30:00

    Jeffrey Zhao:
    @韦恩卑鄙
    在我看来这种写法还是相当漂亮的。


    是很漂亮
    我白天家里事比较忙 还没来得及看代码 等我慢慢研究研究


  20. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 21:38:00

    理解了~~ 这个方法很NB 我太喜欢了 :D
    你不把它发给asp.net mvc小组让他们把这个作为推荐方式 我都觉得替你亏得慌

  21. 小城故事
    *.*.*.*
    链接

    小城故事 2009-09-21 21:47:00

    好像明白了MVC是怎么回事,一直以来自己的代码其实也有点MVC的意思,一个月内应该能看懂老赵的多数MVC文章了。

  22. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 22:08:00

    前提是什么 ? context线程相关否?

  23. 老赵
    admin
    链接

    老赵 2009-09-21 22:40:00

    @韦恩卑鄙
    不是,是输出位置的问题,一个Http请求基本上总是单线程的,就算是“异步”也只是两个线程依次执行。
    简单地说,如果你不是从页面的writer输入,而是直接Response.Write的话,RecordWriter就记录不下来了。
    因为此时会直接写到HttpWriter里面去,标准做法应该是从HtmlTextWriter写到RecordWriter再写到HttpWriter。
    这也是我搭建MvcPatch而不是作为ASP.NET MVC扩展的主要原因(没有之一),明天我会详细谈谈。

  24. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-21 23:01:00

    @Jeffrey Zhao
    "如果你不是从页面的writer输入,而是直接Response.Write的话,RecordWriter就记录不下来了"

    原来你一开始说的response bug 也牵扯进来了

    好吧 我等你把这一堆基石串起....

    莫名其妙的火大...- -+++

  25. 老赵
    admin
    链接

    老赵 2009-09-21 23:33:00

    @韦恩卑鄙
    是啊是啊,你看我最近写的好多东西都是有关联的,比如TextWriter啥的,嘿嘿。

  26. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-09-23 22:47:00

    这几篇都很不错啊,有所启发

  27. 打算发生[未注册用户]
    *.*.*.*
    链接

    打算发生[未注册用户] 2009-09-27 11:16:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @第一控制.NET
    按照Rails的做法,可以在Controller里判断缓存是否过期,如果过期了才加载Articles。
    我认为较理想的做法是:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/05/simple-over-complex.html


    如果在Controller中判断缓存的时候未过期,但是在view呈现时发现缓存过期,如何解决?

  28. SvnHosting[未注册用户]
    *.*.*.*
    链接

    SvnHosting[未注册用户] 2009-10-31 11:15:00

    HtmlHelper 增加 Cache 扩展方法
    在哪增加呢?

  29. SvnHosting[未注册用户]
    *.*.*.*
    链接

    SvnHosting[未注册用户] 2009-10-31 11:32:00

    @SvnHosting
    已经知道了,谢谢。

  30. SvnHosting[未注册用户]
    *.*.*.*
    链接

    SvnHosting[未注册用户] 2009-10-31 11:48:00

    var writer = htmlHelper.GetRecordWriter();
    ...
    htmlHelper.Output.Write(content);
    两行都有问题(不包含定义)

  31. 老赵
    admin
    链接

    老赵 2009-10-31 11:52:00

    @SvnHosting
    看一下MvcPatch吧。

  32. 侠女[未注册用户]
    *.*.*.*
    链接

    侠女[未注册用户] 2009-12-25 16:50:00

    您好,我是刚刚接触mvc就要做项目了,我准会用到缓存的,控制器中的方法头部加上【outputcach】等这样得标记不可以吗,还要自己重新去写啊?麻烦你给我发一份详细的 适合ASP.NET MVC的视图片断缓存方式(中):更实用的API 的代码可以吗?我的邮箱wanglin20061064@126.com,感激不尽

  33. 侠女[未注册用户]
    *.*.*.*
    链接

    侠女[未注册用户] 2009-12-25 16:52:00

    或许你讲的这些还不是我这层次的人们所能理解,所以想麻烦你发一份完整代码,周末去研究,好吗?先谢谢啦,圣诞快乐!!

  34. 侠女[未注册用户]
    *.*.*.*
    链接

    侠女[未注册用户] 2009-12-25 16:53:00

    @SvnHosting
    你是在哪里加的啊,我就不知道啊

  35. jianshao810
    113.108.134.*
    链接

    jianshao810 2010-10-26 11:40:17

    我想问 在mvc里 像 http://mm.ooo.com/ 这样的第一个页面怎样设置缓存?我这个是url重写的,其实还有 qq.ooo.com等很多域名。但是缓存之后 所有二级域名都变成同一个网站啦

  36. hans
    218.82.168.*
    链接

    hans 2011-02-10 16:11:53

    这个思路不错 借鉴下~

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我