一直忙於工作,好久沒有更新部落格了 (其實列了很多 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 則留言:
剛剛參考了這篇文章提供的寶貴經驗,調整了一下 ScriptalizedOutputAttribute 的實作 ^^
http://ithelp.ithome.com.tw/question/10095309?tag=hp.share
張貼留言