2013年7月12日 星期五

跨網站利用 document.write 顯示 ASP.NET MVC ViewResult 內容, 使用 ActionFilter

一直忙於工作,好久沒有更新部落格了 (其實列了很多 backlog 都沒有時間整理完 XD),最近在實作專案的過程中,遇上題目所談到這個小小問題,雖然已經先解決了,只是一直不甘心使用了不是很喜歡的作法,久久掛念在心上,今天突然想到有簡單的方式可以解決,很慚愧的來補一篇

文章題目是經過斟酌過才決定的,已經將原本複雜的問題情境都拿掉,最終目的就是需要一個可以跨網站顯示 View 的簡單方式

研究

解決的想法來自於同樣是工作中所接觸到的網路廣告系統,網路廣告內容通常都是由遠端廣告主機拉下來的 html code,因為這件事情如果要依賴網站自行上傳廣告內容,以及安排露出時間表,那個廣告所能觸及的網站就會非常受限,廣告普及的難度就提高了許多

因此所以接觸過的人就知道要植入網路廣告,通常靠的是埋 code 就好,來提升部署速度,我目前接觸到廣告投放有幾種方式

  • iframe
  • script (document.write)

廣告內容、格式以及時間表都由專責伺服器安排,這個 scenario 跟我所要的結果幾乎是一模一樣,那就來借用一下網路廣告的智慧吧 XD

對應以上作法,我需要把我的 View 顯示到他人網站上,應該這麼做:

  • 讓對方嵌入 iframe,src 指向我們為他設計的 view 的 URL
  • 讓對方嵌入 script,src 指向我們為他設計的 view 的 URL,並透過 script 中的 document.write 將 View 結果寫到他人網頁上
NOTE: 這裡就暫時先不討論兩個作法的優缺點、衍生的問題與限制

第一點應該隨便都可以達到,第二點就需要思考一下作法了

假設與模擬

假設我的 View 輸出結果是

<ul>
 <li></li>
 <li></li>
</ul>

我希望別人埋的廣告 code (埋這樣的 code 夠簡單了吧...)

<script type="type/javascript" src="http://test-server/Ad/StackBanner"></script>

廣告 code 最後應該是輸出

document.write("<ul><li></li><li></li></ul>");

這樣一來就可以達到預期目標! (應該不會跳太快吧... :P)

實作

好的,現在我們已經將問題範圍限縮到如何將 View 輸出結果轉為 document.write 就好,靈機一動,這或許是一個利用 ActionFilter 的好時機,我想將 View 的顯示結果轉為字串之後,就能夠做到 document.write 了

那麼就來儘速實作這個 ActionFilter 吧

public class ScriptalizedOutputAttribute : ActionFilterAttribute
{
    public bool WrapScriptTag { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var response = filterContext.HttpContext.Response;

        response.Filter = new StreamFilter(response.Filter, s =>
        {
            s = Regex.Replace(s, @"(\r\n|\n|\r)", "\\n"); // 換行字元都改掉
            s = Regex.Replace(s, @"\""", "\\\"");      // 要考慮逸出字元

            if (WrapScriptTag)
            {
                return string.Format("<script type=\"text/javascript\">\n//<!--\ndocument.write(\"{0}\");\n//-->\n</script>", s);
            }

            return string.Format("document.write(\"{0}\");", s);
        });

        response.ContentType = "text/javascript";
    }

    class StreamFilter : Stream
    {
        private Stream _shrink;
        private Func _filter;

        public StreamFilter(Stream shrink, Func filter)
        {
            _shrink = shrink;
            _filter = filter;
        }

        public override bool CanRead { get { return true; } }
        public override bool CanSeek { get { return true; } }
        public override bool CanWrite { get { return true; } }
        public override void Flush() { _shrink.Flush(); }
        public override long Length { get { return 0; } }
        public override long Position { get; set; }
        public override int Read(byte[] buffer, int offset, int count)
        {
            return _shrink.Read(buffer, offset, count);
        }
        public override long Seek(long offset, SeekOrigin origin)
        {
            return _shrink.Seek(offset, origin);
        }
        public override void SetLength(long value)
        {
            _shrink.SetLength(value);
        }
        public override void Close()
        {
            _shrink.Close();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            // capture the data and convert to string 
            byte[] data = new byte[count];
            Buffer.BlockCopy(buffer, offset, data, 0, count);
            string s = Encoding.UTF8.GetString(buffer);

            // filter the string
            s = _filter(s);

            // write the data to stream 
            byte[] outdata = Encoding.UTF8.GetBytes(s);
            _shrink.Write(outdata, 0, outdata.GetLength(0));
        }
    }
}

這一個手法基本上來自 Minify HTML with .NET MVC ActionFilter 這篇文章,只是原來的作用是在於壓縮 View 內容中的空白字元

我更換了一下使用情境,只是單純的將 View 的結果 script 化,因為這樣一來要在他人網站上利用 document.write 顯示我們網站 View 內容的作法就變為可能了

document.write("<ul><li></li><li></li></ul>");

最後我的 Controller/Action 套上 ScriptalizedOutputAttribute 寫法變成

public class AdController : ControllerBase
{
 [ScriptalizedOutput]
 public ActionResult StackBanner()
 {
  // ...

  return View();  // 這裡還是單純 render HTML 就好
 }
}

就達到原來的目標啦,順利完成,我沒有特意針對特定 MVC 版本設計,按照概念來看 ASP.NET MVC 2 以上應該都適用這招 (我沒寫過更早之前的版本,歡迎提供見解),先整理到此囉。

2013/7/15 提示: 能夠 document.write 到其他網站,當然也可以在自己網站內使用,這一個方式可以有效處理 OutputCache 頁面中不想被 cache 的區塊喔,同樣情境下與 Ajax 方法比較,可以避免掉 Ajax 方法通常需要在 document ready 之後才會進行的缺點。

排版粗獷,用字鄙俗,還請多多海涵 XD

Keywords: ASP.NET MVC, ActionFilter, ActionResult, ViewResult, document.write

1 則留言:

Dino Wang 提到...

剛剛參考了這篇文章提供的寶貴經驗,調整了一下 ScriptalizedOutputAttribute 的實作 ^^

http://ithelp.ithome.com.tw/question/10095309?tag=hp.share