1. 程式人生 > >適合ASP.NET MVC的檢視片斷快取方式(中):更實用的API

適合ASP.NET MVC的檢視片斷快取方式(中):更實用的API

上一篇文章中我們提出了了片斷快取的基本方式,也就是構建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專案。

相關文章