使用ASP.NET MVC構建HTML5離線web應用程序
web 應用程序的主要制約之一就是連接性。在 HTML5 到來之前我們就曾想挖掘瀏覽器的能力,以使 web 應用程序能像桌面應用程序一樣功能強大和易于使用,但瀏覽器始終讓我們感到失望。雖然之前已出現了一些瀏覽器緩存技術,但這些緩存技術的設計初衷并不是為了使 web 應用程序能夠完全地離線運行,令人遺憾的是,事實上使用這些技術的 web 應用程序很容易出問題,而且難于使用。HTML5試圖通過離線應用程序緩存( offline application cache)技術來填補瀏覽器的能力空缺,該技術能更加可靠地使 web 應用程序離線工作。
為什么 web 應用程序需要離線運行呢?
老實講,一般來說,桌面電腦的 web 應用程序即使能夠完全離線運行也不能帶來多大的好處,因為桌面電腦一般都是一直連線的。我特別期待看到的是,移動設備 web 應用程序能夠從離線應用程序緩存技術得到多大的好處。
在許多地方,移動電話普及率都在持續增長。如果能夠自然地填補網絡斷線的鴻溝,移動設備瀏覽器中的 web 應用程序對用戶來說就更加友好了。
在一些特定場景中,使整個應用程序能夠離線運行,意味著我們只需創建一個跨平臺的瀏覽器解決方案,而不必創建多個內建應用程序。
試想一下,一位銷售員需要隨時隨地向她的顧客展示商品目錄單。她可以使用任何她想要的電子設備,她首次瀏覽商品目錄單時需要連線,之后便能夠隨時隨地離線瀏覽。
應用程序緩存技術并不只是在離線狀態下才有用武之地。我們可以將應用程序緩存作為一個超級緩存,用于本地存儲資源,這樣可以加速應用程序啟動。服務器上更新了的資源可以在后臺線程重新加載,加載完成之后便替換掉本地舊的資源并更新到正在運行的應用程序上。這種方式非常適用于桌面電腦的重量級 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 之間的數字,每個數字對應一個特定的緩存狀態。
數字 |
狀態 |
描述 |
uncached |
頁面不在應用程序緩存中。應用程序緩存第一次加載時頁面也處于這種狀態。 |
|
1 |
idle |
當應用程序緩存是最新的時,瀏覽器將狀態設置為 idle。 |
2 |
checking |
當應用程序檢查是否有更新了的清單文件時,瀏覽器將狀態設置為 checking。 |
3 |
downloading |
當應用程序正在下載新緩存時,瀏覽器將狀態設置為 downloading。 |
4 |
updateready |
當新緩存已下載完畢,可以替換舊有資源時,瀏覽器將狀態設置為updateready。 |
5 |
obsolete |
當找不到清單文件時,瀏覽器將狀態設置為 obsolete。 |
你可以使用 setInterval 函數來快速顯示狀態變化。
setInterval (function () {
console.log (window.applicationCache.status)
}, 500);
事件處理
除了檢查緩存狀態,我們還可以處理特定事件。
事件 |
描述 |
checking |
當瀏覽器在檢查是否有清單文件被更新過時這個事件被觸發。這通常是第一個被觸發的事件。 |
downloading |
當瀏覽器開始下載新資源時該事件被觸發。 |
cached |
當所有資源下載完畢并提交到緩存時,該事件被觸發。 |
error |
當應用程序緩存機制出問題時該事件被觸發。這可能是因為找不到清單文件,或者找不到清單文件中指定的某個資源。也可能是超過了瀏覽器離線緩存限額。通常來說,每當發生致命錯誤時該事件被觸發。 |
noupdate |
第一次下載清單文件時該事件被觸發。 |
progress |
每當應用程序緩存下載完一項資源時該事件被觸發。 |
updateready |
當新資源下載完畢并可以更新舊緩存時該事件被觸發。 |
obsolete |
當找不到清單文件時該事件被觸發。 |
緩存替換
當新緩存下載完成之后,它并不會立即替換掉舊的緩存,而是直到我們通知應用程序使用新緩存時它才進行替換。我們可以通過處理 updateready 事件,使用 swapCache 將舊緩存替換為新緩存。更新的資源要在刷新頁面后才能見到。
window.applicationCache.onupdateready = function (){ window.applicationCache.swapCache (); });
怎樣讓用戶知道你的應用程序可以離線運行呢?
據我所知,沒有哪種瀏覽器會通知用戶當前應用程序是能離線運行的。不過,我們可以自己通知用戶:通過監聽應用程序緩存的特定事件,當應用程序已經可以離線工作時通知用戶。我們甚至可以將應用程序緩存生命周期的每個階段都通知用戶。
應用程序緩存相關事件的處理是直截了當的。其中一個特別有用的事件是 progress 事件。每當一個資源下載完畢時這個事件被觸發,其包含三個非常有用的屬性,我們可以用這三個屬性來顯示下載進度:lengthComputable、 loaded 以及 total。首先,我們需檢查 lengthComputable 屬性來判斷 loaded 和 total 屬性是否可用,接著我們使用 loaded 和 total 屬性計算出資源下載的百分比進度。
window.applicationCache.onchecking = function (e) { updateCacheStatus ('Checking for a new version of the application.'); }; window.applicationCache.ondownloading = function (e) { updateCacheStatus ('Downloading a new offline version of the application'); }; window.applicationCache.oncached = function (e) { updateCacheStatus ('The application is available offline.'); }; window.applicationCache.onerror = function (e) { updateCacheStatus ('Something went wrong while updating the offline version of the application. It will not be available offline.'); }; window.applicationCache.onupdateready = function (e) { window.applicationCache.swapCache (); updateCacheStatus ('The application was updated. Refresh for the changes to take place.'); }; window.applicationCache.onnoupdate = function (e) { updateCacheStatus ('The application is also available offline.'); }; window.applicationCache.onobsolete = function (e) { updateCacheStatus ('The application cannot be updated, no manifest file was found.'); }; window.applicationCache.onprogress = function (e) { var message = 'Downloading offline resources.. '; if (e.lengthComputable) { updateCacheStatus (message + Math.round (e.loaded / e.total * 100) + '%'); } else { updateCacheStatus (message); }; };
怎樣檢測瀏覽器是處于在線狀態還是離線狀態呢?
你需要知道瀏覽器是在線的還是離線的有以下幾個原因:也許是因為你想通知用戶其正在離線工作,也許是因為你想在網絡斷開時禁用應用程序的某些功能,還或許是因為你想通過本地存儲(local storage)技術以支持離線用戶輸入,然后在上線時將用戶輸入的文本同步到服務器。要實現這些需求,你可以通過自造基礎架構,也可以通過使用開源項目或第三方項目。
檢測在線狀態
從原理上講檢測在線狀態應該是非常簡單的,比如在標準狀況下,你使用 navigator 單件的 onLine 屬性就可以檢測出當前瀏覽器是否在線。
console.log (navigator.onLine)
但事實上并非如此簡單,因為各種瀏覽器對在線和離線的定義不盡相同。比如,舊版本的火狐瀏覽器只當用戶顯示地進行在線和離線狀態切換時才更新 onLine 屬性的值,而忽略了實際的網絡狀況。拋開實現上的不一致,檢測網絡連接狀況本身就不是一件微不足道的事情。比如,假設你的電腦是連接上了的,但是你的路由器出問題了,這時應該顯示什么狀態呢?
一種流行的 hack 方法是檢查每個 AJAX 請求的狀態碼,然后當狀態碼為不成功時則回退到離線機制。
事件處理
如果你想在瀏覽器改變連線狀態時做一些事情,你可以通過處理 offline 和 online 事件來實現。但是請注意,和檢查 onLine 屬性一樣,使用這兩個事件也有類似問題。
window.addEventListener ('offline', function (e) { console.log ('offline'); }, false); window.addEventListener ('online', function (e) { alert ('online'); }, false);
瀏覽器支持
除了 Internet Explorer,所有主流現代瀏覽器都支持離線 web 應用程序。Internet Explorer 10 也實現了相關規范,只是目前它還未發布。在 caniuse.com 上可以查看到每種瀏覽器及其版本對這一規范的支持情況。
對于大部分實現,各主流瀏覽器基本上是相一致的。但在實現存儲限額以及對限額的管理(這兩項沒有定義在規范中)上,各瀏覽器差異比較大。在測試你的 web 應用程序時應該考慮這個問題,移動設備中的瀏覽器在緩存大小上可是斤斤計較的。
使用 ASP.NET MVC 生成和提供清單文件
生成清單文件
利用 ASP.NET MVC 創建和提供清單文件有幾種方式。最簡單地方式就是讓 ASP.NET MVC 提供靜態文本文件。然而,如果我們想要使用內建的 ASP.NET MVC 特性來解析路由,或者想編寫代碼來動態操控清單文件,我們最好使用自定義的 action result。
我把這個自定義的 action result 命名為 ManifestResult,它繼承自 MVC 框架中的 FileResult 類。提供清單文件服務時應該使用'text/cache-manifest' MIME 類型,我把這個字符串傳遞給了父類的構造函數。
public class ManifestResult : FileResult { public ManifestResult (string version) : base("text/cache-manifest") { } }
ManifestResult 類具有四個屬性,其中三個屬性對應清單文件的三個節,另外一個屬性對應版本號。表示 CACHE 節和 NETWORK 節的兩個屬性僅僅是字符串枚舉,而表示 FALLBACK 節的屬性是字典類型的,用于將資源映射到 FALLBACK 指定的資源。
public class ManifestResult : FileResult { public ManifestResult (string version) : base("text/cache-manifest") { Version = version; CacheResources = new List<string>(); NetworkResources = new List<string>(); FallbackResources = new Dictionary<string, string>(); } public string Version { get; set; } public IEnumerable<string> CacheResources { get; set; } public IEnumerable<string> NetworkResources { get; set; } public Dictionary<string, string> FallbackResources { get; set; } }
要將格式化的清單文件輸出到響應流,需要重寫 WriteFile 方法。
protected override void WriteFile (HttpResponseBase response) { WriteManifestHeader (response); WriteCacheResources (response); WriteNetwork (response); WriteFallback (response); }private void WriteManifestHeader (HttpResponseBase 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 + " " + fallbackResource.Value); }
提供清單文件服務
為了提供清單文件服務,我們要將相應的 action 添加到相應的控制器類中,以生成和返回清單文件的動作結果(action result)。在該 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。在應用程序啟動時,下面的代碼將這個新的“cache.manifest”路由添加到路由表中。
routes.MapRoute ("cache.manifest", "cache.manifest", new { controller = "Resources", action = "Manifest" });
結論
離線 web 應用程序是正處于不斷發展中的 HTML 規范的重要內容之一。根據實際用例,你可能僅僅是利用這個特性來緩存資源或者讓 web 應用程序完全離線運行。這個特性的中心就是清單文件。清單文件的格式和要求一點也不復雜,使用 ASP.NET MVC 或其他服務端技術可以直截了當地生成和提供清單文件服務。編寫好清單文件之后,使用應用程序緩存 API 就可以很容易地進行緩存更新。你也可以使用這組 API 來查詢緩存狀態和處理應用程序緩存的特定事件。想知道瀏覽器處于在線狀態還是離線狀態,你可以通過檢查 navigator 對象的 onLine 屬性,或者處理特定的在線和離線事件來判斷。
關于作者
Jef Claes 熱衷于創建新玩意。在日常工作中,他是 Euricom 的一名軟件工程師,負責構建基于微軟技術的企業應用軟件。在業余時間,他喜歡將隨機的想法或創新的方案付諸實踐。除了這些,Jef 一般會在他的博客上寫他感興趣的事情。Jef 不羞于演講,他在一些本地會議上談論和 web 相關的技術和問題。你可以通過 推ter 和他聯系。
查看英文原文:HTML5 offline web applications using ASP.NET MVC