2013年9月22日 星期日

MVC搭配HTML5的離線瀏覽功能

Web應用程式的主要限制之一就是連線的穩定性。在HTML5之前,曾經想過挖掘瀏覽器所有的能力,讓Web應用程式能夠像一般Windows Form那樣強大和易於使用,但瀏覽器始終令人感到失望。雖然之前已經出現了一些能夠在瀏覽器上暫存/快取的技術,但這些快取技術的設計初衷並非是為了讓Web應用程式能夠完全的離線(Off-line)執行,令人感到沮喪的是,事實上使用這些技術的Web應用程式非常容易發生一些意想不到的問題,而且使用者會感到難以使用。HTML5試圖藉由離線執行快取(Off-line application cache)的技術來填補瀏覽器能力空缺,該技術更加可靠。

為什麼Web應用程式需要離線執行    
一般來說,PC的Web應用程式即便可以完全離線執行也無法帶來太多好處,因為PC一般來說都是一直連線的,真正要期待的是智慧型手機的Web應用程式能夠從離線應用程式快取技術得到多少好處。    

智慧型手機的普及率在逐年成長,但如果能夠填補掉網路斷線的鴻溝,就能夠讓智慧型手機瀏覽器的Web應用程式對於使用者來說更加方便。    

在某些特殊狀況下,整個應用程式能夠離線執行,就意味著僅需要創建一個跨平台的瀏覽器解決方案,而不需要創建太多原生應用程式。    

試想一下,一位銷售人員需要隨時隨地向他的客戶展示商品型錄。他可以使用任何他想要的電子展示設備,當他曾經瀏覽過商品型錄之後,接下來便可以隨時隨地的離線瀏覽。     應用程式快取技術並不只是在離線狀態才有用武之地。亦可以將應用程式快取作為一個超級快取,用於本機儲存資源,如此一來,便可以加速應用程式啟動。伺服器上更新了的資源可以在後台重新載入,載入完畢之後就替換掉本地舊的資源並更新到正在執行中的應用程式上。這種方式非常適合用於PC的重量級Web應用程式。  

清單檔案    

要使用應用程式快取,並不需要撰寫大量的程式碼,可以在一個簡單的檔案文件中定義需要離線使用的資源,這個檔案被稱之為清單(Manifest)檔案。   

一個簡單的清單檔案內容格式如下:
CACHE MANIFEST
# Version 1.0

CACHE:
/home/index
/content/style.css
/scripts/main.js

NETWORK:
/service/status

FALLBACK:
/logo.png /logo_offline.png
 
   需要CACHE MANIFEST標頭放在清單檔案的第一行。

   以數字符號#開頭的代表是註解。這個通常是用在顯示的修改清單檔案以通知瀏覽器更新快取。譬如說,當你更新了一張圖片但沒有修改圖片的名稱時,這種情境下就非常適用,因為瀏覽器並沒有其它方式可以檢測到伺服器上的圖片已經被更新。

   接下來,清單檔案包含了以下三個段落: CACHE, NETWORK以及FALLBACK。在CACHE段落中你可以指定需要快取的資源。需要一直從伺服器下載的資源(即便是在離線的狀況下)則在NETWORK段落中指定。若有大量的資源需要不斷從伺服器上下載,可以在NETWORK段落中使用*符號表示。在FALLBACK段落中,可以指定在離線狀態下可使用的備用資源。

   清單檔案的格式並不嚴謹。以上介紹的段落是可以任何修改順序的,它們甚至可以在一個清單檔案中多次出現。

   在清單檔案中你可以使用相對路徑或是絕對路徑來定位資源。如果你使用相對路徑,則必須以清單檔案的位置作為參考的基準位置來定位資源。

引用清單檔案

   要將清單檔案綁定到應用程式,需要將manifest屬性添加到html標籤上。每個引用清單檔案的頁面本身會預設為快取。然而,仍是建議在清單檔案中列出你想要快取的資源。若某個頁面沒有在清單檔案中被指定,同時也不曾在瀏線非離線狀態下瀏覽過,則當處於離線狀態下就無法看到這個頁面,這是因為瀏覽器無法得知頁面是否存在本機快取中。
<html manifest="cache.manifest" />

檢查快取狀態

   使用應用程式快取API,可以讓我們檢查應用程式的快取狀態。使用window.applicationCache這個屬性就可以查詢當前快取的狀態。該狀態屬性的值是一個介於0~5的數字,每個數字有其對應的狀態:

表1. 快取狀態
狀態描述
0Uncached
頁面不在應用程式快取中。應用程式快取第一次載入時,頁面預設會處於這個狀態。
1Idle
當應用程式快取是最新的時候,瀏覽器會將狀態設定為Idel。
2Checking
當應用程式檢查是否有更新清單檔案時,瀏覽器會處於這個狀態。
3Downloading
當應用程式正在下載新的快取時,瀏覽器將狀態設定為Downloading。
4UpdateReady
當新的快取下載完成時,並且已可以替換掉現有的舊資源,瀏覽器會將狀態設定為UpdateReady
5Obsolete
當找到清單檔案時,瀏覽器會將狀態設定為Obsolete。


SetInterval(function() {
   console.log(window.applicationCache.status)
},500);
事件處理
   除了要檢查快取狀態之外,還可以處理特定事件。
表2. 事件

事件

描述

Checking

當瀏覽器在檢查是否有清單檔案被更新時,這個事件會被觸發。這個通常是第一個被觸發的事件。

Downloading

當瀏覽器開始下載新資源時,該事件會被觸發。

Cached

當所有資源下載完畢並儲存至快取時,該事件會被觸發。

Error

當應用程式快取機制出現問題時,該事件會被觸發。這可能是因為找不到清單檔案,或者是找不到清單檔案中某個指定的資源。亦可能是資源已超過了瀏覽器離線暫存的空間限制。一般來說,每當發生致命錯誤時該事件就會被觸發。

NoUpdate

第一次下載清單檔案時,該事件會被觸發。

Progress

每當應用程式快取下載完一項資源後該事件會被觸發。

UpdateReady

當新資原下載完成並可以更新舊快取中的內容時,此事件會被觸發。

Obsolete

當找不到清單檔案時,該事件會被觸發。

快取置換
   當新的快取下載完畢後,它並不會立刻換掉舊的快取資漁,而是一直到主動通知應用程式該使用新的快取資源時,它才會進行置換。可以藉由處理UpdateReady事件,使用SwapCache將舊快取置換成為新的快取內容。更新資源要在刷新頁面後才能看到。
window.applicationCache.onupdateready = function() {
     window.applicationCache.swapCache();
};
如何讓用戶知道應用程式是可以離線運作的呢?
   沒有那一種瀏覽器會主動通知使用者目前的應用程式是可以支援離線瀏覽的。但是,我們可以自行通知使用者:透過監聽應用程式快取的特定事件,當應用程式已經可以離線作業時通知使用者。甚至還可以將應用程式快取生命週期的每個階段都通知使用者。
   應用程式快取相關事件的處理是直接了當的,其中一個非常有用的事見是Progress事件。每當一個資源下載完畢後這個事件會被觸發,其包含三個非常有用的屬性,可以利用這三個屬性來顯示下載進度:  
   1. lengthComputable  
   2. loaded  
   3. total
   首先,要先檢查lengthComputable屬性來判斷loaded和total屬性是否可用,接著使用loaded和total屬性計算資源下載的百分比進度。
window.applicationCache.onchecking = function(e) {
     updateCacheStatus("檢查新版本");
};

window.applicationCache.ondownloading = function(e) {
     updateCacheStatus("下載新版本的離線資源");
};

window.applicationCache.oncached = function(e) {
     updateCacheStatus("應用程式所需離線資源已下載完畢");
};

window.applicationCache.onnoupdate = function(e) {
     updateCacheStatus("無法順利更新離線資源,找不到清單檔案");
};

window.applicationCache.onprogress = function(e) {
     var message = "下載離線資源";
     if(e.lengthComputable)
          updateCacheStatus(message + Math.round(e.loaded/ e.total*100)+"%");
     else
          updateCacheStatus(message);
};
 
檢測是否在線
   理論上檢測是否在線是非常簡單的,以標準狀態下,使用navigator的onLine屬性就可以檢測出目前瀏覽器是否在線。
console.log(navigator.onLine);
   但事實上並非如此簡單,因為各種瀏覽器對於在線與離線的定義並不相同,譬如說,舊版本的FireFox只有當使用者直接進行在線與離線狀態切換時才會更新onLine屬性的值,而忽略了實際的網路情況。拋開這些實做上的不一致,檢查網路連線狀況本身就不是一件簡單的事,譬如說,假若你的電腦是連線了,但是你的路由器出了問題,這時候應該顯示什麼狀態呢?
   一種大家常用的方法就是檢查Ajax的狀態碼,然後當狀態為不成功時則進入離線機制。
事件處理
   若想要在瀏覽器中改變連線狀態時做一些事,可以透過處理offline和online事件來達成。但是請注意,和檢查onLine屬性一樣,使用這兩個事件亦有相同的問題。
window.addEventListener("offline",function(e) {
     console.log("offline");
}, false);

window.addEventListener("online",function(e) {
     alert("online");
}, false);

瀏覽器支援度
   除了IE舊版本(i.e. 10以下)外,目前主流的瀏覽器都已支援離線Web應用程式。在Caniuse.com上可以查看每種瀏覽器及其版本對這一規範的支援情況。
   對於大部份實做,各個主流瀏覽器基本上都是一致的,但是在實做儲存限制以及對限制的管理上,各個瀏覽器的差異頗大。在測試你的Web應用程式時應考慮這個問題。智慧型設備的瀏覽器在快取大小上可是斤斤計較的。
使用ASP.Net MVC來產生和提交清單檔案
   1. 產生清單檔案
       利用ASP.Net MVC產生和提交清單檔案好好幾種方式,其中最簡單的方式就是讓ASP.Net MVC提供靜態檔案。然而,若我們想要使用內建的ASP.Net MVC特性來解析路由,又或者想要撰寫程式碼來動態產生清單檔案,最好使用自定義的ActionResult。
public class ManifestResult: FileResult{
     public ManifestResult(string version) :base("text/cache-manifest") {}
}
       ManifestResult類別有四個屬性,其中三個屬性對應清單檔案的三個段落,另一個屬性對應版本號碼。CACH和NETWORK段落的兩個屬性僅是字串列舉,而FALLBAK段落的屬性是Dictionary型別,用於將資源對應到FALLBACK指定的資源。
public class ManifestResult: FileResult{
     public string Version {get; set;}
     public IEnumerable<string> CacheResources {get; set;}
     public IEnumerable<string> NetworkResources {get; set;}
     public Dictionary<string, string> FallbackResources {get; set;}

     public ManifestResult(string version): base("text/cache-manifest") {
          Version = version;
          CacheReasources = new List<string>();
          NetworkResources = new List<string>();
          FallbackResources = new Dictionary<string, string>();
     }
}
       要將格式化的清單檔案輸出到Response串流,需要覆寫掉WriteFile函數。
protected override void WriteFile(HttpResponseBase response){

      WriteManifestHeader(response);
      WriteCacheResources(response);
      WriteNetwork(response);
      WriteFallback(response);
}

private void WriteManifestHeader(HttpResponoseBase response) {
      response.Output.WriteLine("Cache Manifest");
      response.Output.WriteLine("#V" + Version ?? string.Empty);
}

private void WriteCacheResources(HttpResponseBase response) {
     response.Output.WriteLine("CACHE:");
     foreach(var cacheResource in CacheResources)
          response.Output.WriteLine(cacheResource);
}

private void WriteNetwork(HttpResponseBase response) {
     response.Output.WriteLine();
     response.Output.WriteLine("NETWORK:");
     foreach(var networkResource in NetworkResources)
          response.Output.WriteLine(networkResource);
}

private void WriteFallback(HttpResponseBase response) {
     response.Output.WriteLine();
     response.Output.WriteLine("FALLBACK:");
     foreach(var fallbackResource in FallbackResources)
          response.Output.WriteLine(fallbackResource.Key + " " + fallResource.Value);
}
提交清單檔案服務

   為了提供清單檔案服務,需要將相對應的Action添加到相對應的控制器中,藉此產生和返回清單檔案的ActionResult。在該Action中,藉由MVC的UrlHelper物件來正確地解析路由。
public ActionResult Manifest(){
     var manifestResult = new ManifestResult("1.0");
     {
          CacheResources = new List<string>() {
               Url.Action("Index", "Home"),
               "/content/style.css",
               "/scripts/main.js"
          },
          NetworkResources = new string[] { Url.Action("Status", "Service")},
          FallbackResources = { {"/logo.png", "/logo_offline.png"}}
     };

     return manifestResult;
}
 
為清單檔案設定路由
   應當為清單檔案設定特定的路由,大多數瀏覽器對於清單檔案的位置並沒有嚴格的規定,而最可靠的跨瀏覽器的方式及為將清單檔案放置在根目錄,並且將其命名為Cache Manifest。當Web應用程式啟動時,下面的程式碼會將這個新的Cache Manifest路由添加到路由表中。
routes.MapRoute("Cache.Manifest", "Cache.Manifest", new { controller = "Resources", action = "Manifest"})
參考項目
1. http://www.infoq.com/cn/articles/Offline-Web-Apps?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_news_clk

沒有留言:

張貼留言