1 H5 緩存機制介紹 H5,即 HTML5,是新一代的 HTML 標準,加入很多新的特性。離線存儲(也可稱為緩存機制)是其中一個非常重要的特性。H5 引入的離線存儲,這意味著 web 應用可進行緩存,並可在沒有網際網路連接時進行訪問。 H5 應用程式緩存為應用帶來三個優勢: 離線瀏覽 用戶可在應用離
1 H5 緩存機制介紹
H5,即 HTML5,是新一代的 HTML 標準,加入很多新的特性。離線存儲(也可稱為緩存機制)是其中一個非常重要的特性。H5 引入的離線存儲,這意味著 web 應用可進行緩存,並可在沒有網際網路連接時進行訪問。
H5 應用程式緩存為應用帶來三個優勢:
-
離線瀏覽 用戶可在應用離線時使用它們
-
速度 已緩存資源載入得更快
-
減少伺服器負載 瀏覽器將只從伺服器下載更新過或更改過的資源。
根據標準,到目前為止,H5 一共有6種緩存機制,有些是之前已有,有些是 H5 才新加入的。
-
瀏覽器緩存機制
-
Dom Storgage(Web Storage)存儲機制
-
Web SQL Database 存儲機制
-
Application Cache(AppCache)機制
-
Indexed Database (IndexedDB)
-
File System API
下麵我們首先分析各種緩存機制的原理、用法及特點;然後針對 Anroid 移動端 Web 性能載入優化的需求,看如果利用適當緩存機制來提高 Web 的載入性能。
2 H5 緩存機制原理分析
2.1 瀏覽器緩存機制
瀏覽器緩存機制是指通過 HTTP 協議頭裡的 Cache-Control(或 Expires)和 Last-Modified(或 Etag)等欄位來控制文件緩存的機制。這應該是 WEB 中最早的緩存機制了,是在 HTTP 協議中實現的,有點不同於 Dom Storage、AppCache 等緩存機制,但本質上是一樣的。可以理解為,一個是協議層實現的,一個是應用層實現的。
Cache-Control 用於控制文件在本地緩存有效時長。最常見的,比如伺服器回包:Cache-Control:max-age=600 表示文件在本地應該緩存,且有效時長是600秒(從發出請求算起)。在接下來600秒內,如果有請求這個資源,瀏覽器不會發出 HTTP 請求,而是直接使用本地緩存的文件。
Last-Modified 是標識文件在伺服器上的最新更新時間。下次請求時,如果文件緩存過期,瀏覽器通過 If-Modified-Since 欄位帶上這個時間,發送給伺服器,由伺服器比較時間戳來判斷文件是否有修改。如果沒有修改,伺服器返回304告訴瀏覽器繼續使用緩存;如果有修改,則返回200,同時返回最新的文件。
Cache-Control 通常與 Last-Modified 一起使用。一個用於控制緩存有效時間,一個在緩存失效後,向服務查詢是否有更新。
Cache-Control 還有一個同功能的欄位:Expires。Expires 的值一個絕對的時間點,如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在這個時間點之前,緩存都是有效的。
Expires 是 HTTP1.0 標準中的欄位,Cache-Control 是 HTTP1.1 標準中新加的欄位,功能一樣,都是控制緩存的有效時間。當這兩個欄位同時出現時,Cache-Control 是高優化級的。
Etag 也是和 Last-Modified 一樣,對文件進行標識的欄位。不同的是,Etag 的取值是一個對文件進行標識的特征字串。在向伺服器查詢文件是否有更新時,瀏覽器通過 If-None-Match 欄位把特征字串發送給伺服器,由伺服器和文件最新特征字串進行匹配,來判斷文件是否有更新。沒有更新回包304,有更新回包200。Etag 和 Last-Modified 可根據需求使用一個或兩個同時使用。兩個同時使用時,只要滿足基中一個條件,就認為文件沒有更新。
另外有兩種特殊的情況:
-
手動刷新頁面(F5),瀏覽器會直接認為緩存已經過期(可能緩存還沒有過期),在請求中加上欄位:Cache-Control:max-age=0,發包向伺服器查詢是否有文件是否有更新。
-
強制刷新頁面(Ctrl+F5),瀏覽器會直接忽略本地的緩存(有緩存也會認為本地沒有緩存),在請求中加上欄位:Cache-Control:no-cache(或 Pragma:no-cache),發包向服務重新拉取文件。
下麵是通過 Google Chrome 瀏覽器(用其他瀏覽器+抓包工具也可以)自帶的開發者工具,對一個資源文件不同情況請求與回包的截圖。
首次請求:200
緩存有效期內請求:200(from cache)
緩存過期後請求:304(Not Modified)
一般瀏覽器會將緩存記錄及緩存文件存在本地 Cache 文件夾中。Android 下 App 如果使用 Webview,緩存的文件記錄及文件內容會存在當前 app 的 data 目錄中。
分析:Cache-Control 和 Last-Modified 一般用在 Web 的靜態資源文件上,如 JS、CSS 和一些圖像文件。通過設置資源文件緩存屬性,對提高資源文件載入速度,節省流量很有意義,特別是移動網路環境。但問題是:緩存有效時長該如何設置?如果設置太短,就起不到緩存的使用;如果設置的太長,在資源文件有更新時,瀏覽器如果有緩存,則不能及時取到最新的文件。
Last-Modified 需要向伺服器發起查詢請求,才能知道資源文件有沒有更新。雖然伺服器可能返回304告訴沒有更新,但也還有一個請求的過程。對於移動網路,這個請求可能是比較耗時的。有一種說法叫“消滅304”,指的就是優化掉304的請求。
抓包發現,帶 if-Modified-Since 欄位的請求,如果伺服器回包304,回包帶有 Cache-Control:max-age 或 Expires 欄位,文件的緩存有效時間會更新,就是文件的緩存會重新有效。304回包後如果再請求,則又直接使用緩存文件了,不再向伺服器查詢文件是否更新了,除非新的緩存時間再次過期。
另外,Cache-Control 與 Last-Modified 是瀏覽器內核的機制,一般都是標準的實現,不能更改或設置。以 QQ 瀏覽器的 X5為例,Cache-Control 與 Last-Modified 緩存不能禁用。緩存容量是12MB,不分HOST,過期的緩存會最先被清除。如果都沒過期,應該優先清最早的緩存或最快到期的或文件大小最大的;過期緩存也有可能還是有效的,清除緩存會導致資源文件的重新拉取。
還有,瀏覽器,如 X5,在使用緩存文件時,是沒有對緩存文件內容進行校驗的,這樣緩存文件內容被修改的可能。
分析發現,瀏覽器的緩存機制還不是非常完美的緩存機制。完美的緩存機制應該是這樣的:
-
緩存文件沒更新,儘可能使用緩存,不用和伺服器交互;
-
緩存文件有更新時,第一時間能使用到新的文件;
-
緩存的文件要保持完整性,不使用被修改過的緩存文件;
-
緩存的容量大小要能設置或控制,緩存文件不能因為存儲空間限制或過期被清除。
以X5為例,第1、2條不能同時滿足,第3、4條都不能滿足。
在實際應用中,為瞭解決 Cache-Control 緩存時長不好設置的問題,以及為了”消滅304“,Web前端採用的方式是:
-
在要緩存的資源文件名中加上版本號或文件 MD5值字串,如 common.d5d02a02.js,common.v1.js,同時設置 Cache-Control:max-age=31536000,也就是一年。在一年時間內,資源文件如果本地有緩存,就會使用緩存;也就不會有304的回包。
-
如果資源文件有修改,則更新文件內容,同時修改資源文件名,如 common.v2.js,html頁面也會引用新的資源文件名。
通過這種方式,實現了:緩存文件沒有更新,則使用緩存;緩存文件有更新,則第一時間使用最新文件的目的。即上面說的第1、2條。第3、4條由於瀏覽器內部機制,目前還無法滿足。
2.2 Dom Storage 存儲機制
DOM 存儲是一套在 Web Applications 1.0 規範中首次引入的與存儲相關的特性的總稱,現在已經分離出來,單獨發展成為獨立的 W3C Web 存儲規範。 DOM 存儲被設計為用來提供一個更大存儲量、更安全、更便捷的存儲方法,從而可以代替掉將一些不需要讓伺服器知道的信息存儲到 cookies 里的這種傳統方法。
上面一段是對 Dom Storage 存儲機制的官方表述。看起來,Dom Storage 機制類似 Cookies,但有一些優勢。
Dom Storage 是通過存儲字元串的 Key/Value 對來提供的,並提供 5MB (不同瀏覽器可能不同,分 HOST)的存儲空間(Cookies 才 4KB)。另外 Dom Storage 存儲的數據在本地,不像 Cookies,每次請求一次頁面,Cookies 都會發送給伺服器。
DOM Storage 分為 sessionStorage 和 localStorage。localStorage 對象和 sessionStorage 對象使用方法基本相同,它們的區別在於作用的範圍不同。sessionStorage 用來存儲與頁面相關的數據,它在頁面關閉後無法使用。而 localStorage 則持久存在,在頁面關閉後也可以使用。
Dom Storage 提供了以下的存儲介面:
1 2 3 4 5 6 7 8 |
interface Storage {
readonly attribute unsigned long length;
[IndexGetter] DOMString key( in unsigned long index);
[NameGetter] DOMString getItem( in DOMString key);
[NameSetter] void setItem( in DOMString key, in DOMString data);
[NameDeleter] void removeItem( in DOMString key);
void clear();
};
|
sessionStorage 是個全局對象,它維護著在頁面會話(page session)期間有效的存儲空間。只要瀏覽器開著,頁面會話周期就會一直持續。當頁面重新載入(reload)或者被恢復(restores)時,頁面會話也是一直存在的。每在新標簽或者新視窗中打開一個新頁面,都會初始化一個新的會話。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 當頁面刷新時,從sessionStorage恢復之前輸入的內容
window.onload = function (){
if (window.sessionStorage) {
var name = window.sessionStorage.getItem( "name" );
if (name != "" || name != null ){
document.getElementById( "name" ).value = name;
}
}
};
// 將數據保存到sessionStorage對象中
function saveToStorage() {
if (window.sessionStorage) {
var name = document.getElementById( "name" ).value;
window.sessionStorage.setItem( "name" , name);
window.location.href= "session_storage.html" ;
}
}
|
當瀏覽器被意外刷新的時候,一些臨時數據應當被保存和恢復。sessionStorage 對象在處理這種情況的時候是最有用的。比如恢復我們在表單中已經填寫的數據。
把上面的代碼複製到 session_storage.html(也可以從附件中直接下載)頁面中,用 Google Chrome 瀏覽器的不同 PAGE 或 WINDOW 打開,在輸入框中分別輸入不同的文字,再點擊“Save”,然後分別刷新。每個 PAGE 或 WINDOW 顯示都是當前PAGE輸入的內容,互不影響。關閉 PAGE,再重新打開,上一次輸入保存的內容已經沒有了。
Local Storage 的介面、用法與 Session Storage 一樣,唯一不同的是:Local Storage 保存的數據是持久性的。當前 PAGE 關閉(Page Session 結束後),保存的數據依然存在。重新打開PAGE,上次保存的數據可以獲取到。另外,Local Storage 是全局性的,同時打開兩個 PAGE 會共用一份存數據,在一個PAGE中修改數據,另一個 PAGE 中是可以感知到的。
1 2 3 4 5 6 7 8 |
//通過localStorage直接引用key, 另一種寫法,等價於:
//localStorage.getItem("pageLoadCount");
//localStorage.setItem("pageLoadCount", value);
if (!localStorage.pageLoadCount)
localStorage.pageLoadCount = 0;
localStorage.pageLoadCount = parseInt(localStorage.pageLoadCount) + 1;
document.getElementById( 'count' ).textContent = localStorage.pageLoadCount; You have viewed this page
an untold number of time(s).
|
將上面代碼複製到 local_storage.html 的頁面中,用瀏覽器打開,pageLoadCount 的值是1;關閉 PAGE 重新打開,pageLoadCount 的值是2。這是因為第一次的值已經保存了。
用兩個 PAGE 同時打開 local_storage.html,並分別交替刷新,發現兩個 PAGE 是共用一個 pageLoadCount 的。
分析:Dom Storage 給 Web 提供了一種更錄活的數據存儲方式,存儲空間更大(相對 Cookies),用法也比較簡單,方便存儲伺服器或本地的一些臨時數據。
從 DomStorage 提供的介面來看,DomStorage 適合存儲比較簡單的數據,如果要存儲結構化的數據,可能要藉助 JASON了,將要存儲的對象轉為 JASON 字串。不太適合存儲比較複雜或存儲空間要求比較大的數據,也不適合存儲靜態的文件等。
在 Android 內嵌 Webview 中,需要通過 Webview 設置介面啟用 Dom Storage。
1 2 3 |
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDomStorageEnabled( true );
|
拿 Android 類比的話,Web 的 Dom Storage 機制類似於 Android 的 SharedPreference 機制。
2.3 Web SQL Database存儲機制
H5 也提供基於 SQL 的資料庫存儲機制,用於存儲適合資料庫的結構化數據。根據官方的標準文檔,Web SQL Database 存儲機制不再推薦使用,將來也不再維護,而是推薦使用 AppCache 和 IndexedDB。
現在主流的瀏覽器(點擊查看瀏覽器支持情況)都還是支持 Web SQL Database 存儲機制的。Web SQL Database 存儲機制提供了一組 API 供 Web App 創建、存儲、查詢資料庫。
下麵通過簡單的例子,演示下 Web SQL Database 的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
if (window.openDatabase){
//打開資料庫,如果沒有則創建
var db = openDatabase( 'mydb' , '1.0' , 'Test DB' , 2 * 1024);
//通過事務,創建一個表,並添加兩條記錄
db.transaction( function (tx) {
tx.executeSql( 'CREATE TABLE IF NOT EXISTS LOGS (id unique, log)' );
tx.executeSql( 'INSERT INTO LOGS (id, log) VALUES (1, "foobar")' );
tx.executeSql( 'INSERT INTO LOGS (id, log) VALUES (2, "logmsg")' );
});
//查詢表中所有記錄,並展示出來
db.transaction( function (tx) {
tx.executeSql( 'SELECT * FROM LOGS' , [], function (tx, results) {
var len = results.rows.length, i;
msg = "Found rows: " + len + "" ;
for (i=0; i " + results.rows.item(i).log + " ";
}
document.querySelector( '#status' ).innerHTML = msg;
}, null );
});
}Status Message
|
將上面代碼複製到 sql_database.html 中,用瀏覽器打開,可看到下麵的內容。
官方建議瀏覽器在實現時,對每個 HOST 的資料庫存儲空間作一定限制,建議預設是 5MB(分 HOST)的配額;達到上限後,可以申請更多存儲空間。另外,現在主流瀏覽器 SQL Database 的實現都是基於 SQLite。
分析:SQL Database 的主要優勢在於能夠存儲結構複雜的數據,能充分利用資料庫的優勢,可方便對數據進行增加、刪除、修改、查詢。由於 SQL 語法的複雜性,使用起來麻煩一些。SQL Database 也不太適合做靜態文件的緩存。
在 Android 內嵌 Webview 中,需要通過 Webview 設置介面啟用 SQL Database,同時還要設置資料庫文件的存儲路徑。
1 2 3 4 5 |
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setDatabaseEnabled( true );
final String dbPath = getApplicationContext().getDir( "db" , Context.MODE_PRIVATE).getPath();
webSettings.setDatabasePath(dbPath);
|
Android 系統也使用了大量的資料庫用來存儲數據,比如聯繫人、短消息等;資料庫的格式也 SQLite。Android 也提供了 API 來操作 SQLite。Web SQL Database 存儲機制就是通過提供一組 API,藉助瀏覽器的實現,將這種 Native 的功能提供給了 Web App。
2.4 Application Cache 機制
Application Cache(簡稱 AppCache)似乎是為支持 Web App 離線使用而開發的緩存機制。它的緩存機制類似於瀏覽器的緩存(Cache-Control 和 Last-Modified)機制,都是以文件為單位進行緩存,且文件有一定更新機制。但 AppCache 是對瀏覽器緩存機制的補充,不是替代。
先拿 W3C 官方的一個例子,說下 AppCache 機制的用法與功能。
1 |
Get Date and TimeTry opening this page, then go offline, and reload the page. The script and the image should still work.
|
上面 HTML 文檔,引用外部一個 JS 文件和一個 GIF 圖片文件,在其 HTML 頭中通過 manifest 屬性引用了一個 appcache 結尾的文件。
我們在 Google Chrome 瀏覽器中打開這個 HTML 鏈接,JS 功能正常,圖片也顯示正常。禁用網路,關閉瀏覽器重新打開這個鏈接,發現 JS 工作正常,圖片也顯示正常。當然也有可能是瀏覽緩存起的作用,我們可以在文件的瀏覽器緩存過期後,禁用網路再試,發現 HTML 頁面也是正常的。
通過 Google Chrome 瀏覽器自帶的工具,我們可以查看已經緩存的 AppCache(分 HOST)。
上面截圖中的緩存,就是我們剛纔打開 HTML 的頁面 AppCache。從截圖中看,HTML 頁面及 HTML 引用的 JS、GIF 圖像文件都被緩存了;另外 HTML 頭中 manifest 屬性引用的 appcache 文件也緩存了。
AppCache 的原理有兩個關鍵點:manifest 屬性和 manifest 文件。
HTML 在頭中通過 manifest 屬性引用 manifest 文件。manifest 文件,就是上面以 appcache 結尾的文件,是一個普通文件文件,列出了需要緩存的文件。
上面截圖中的 manifest 文件,就 HTML 代碼引用的 manifest 文件。文件比較簡單,第一行是關鍵字,第二、三行就是要緩存的文件路徑(相對路徑)。這隻是最簡單的 manifest 文件,完整的還包括其他關鍵字與內容。引用 manifest 文件的 HTML 和 manifest 文件中列出的要緩存的文件最終都會被瀏覽器緩存。
完整的 manifest 文件,包括三個 Section,類型 Windows 中 ini 配置文件的 Section,不過不要中括弧。
-
CACHE MANIFEST - Files listed under this header will be cached after they are downloaded for the first time
-
NETWORK - Files listed under this header require a connection to the server, and will never be cached
-
FALLBACK - Files listed under this header specifies fallback pages if a page is inaccessible
完整的 manifest 文件,如:
1 2 3 4 5 6 7 8 9 |
CACHE MANIFEST
# 2012-02-21 v1.0.0
/theme.css
/logo.gif
/main.js
NETWORK:
login.asp
FALLBACK:
/html/ /offline.html
|
總的來說,瀏覽器在首次載入 HTML 文件時,會解析 manifest 屬性,並讀取 manifest 文件,獲取 Section:CACHE MANIFEST 下要緩存的文件列表,再對文件緩存。
AppCache 的緩存文件,與瀏覽器的緩存文件分開存儲的,還是一份?應該是分開的。因為 AppCache 在本地也有 5MB(分 HOST)的空間限制。
AppCache 在首次載入生成後,也有更新機制。被緩存的文件如果要更新,需要更新 manifest 文件。因為瀏覽器在下次載入時,除了會預設使用緩存外,還會在後臺檢查 manifest 文件有沒有修改(byte by byte)。發現有修改,就會重新獲取 manifest 文件,對 Section:CACHE MANIFEST 下文件列表檢查更新。manifest 文件與緩存文件的檢查更新也遵守瀏覽器緩存機制。
如用用戶手動清了 AppCache 緩存,下次載入時,瀏覽器會重新生成緩存,也可算是一種緩存的更新。另外, Web App 也可用代碼實現緩存更新。
分析:AppCache 看起來是一種比較好的緩存方法,除了緩存靜態資源文件外,也適合構建 Web 離線 App。在實際使用中有些需要註意的地方,有一些可以說是”坑“。
-
要更新緩存的文件,需要更新包含它的 manifest 文件,那怕只加一個空格。常用的方法,是修改 manifest 文件註釋中的版本號。如:# 2012-02-21 v1.0.0
-
被緩存的文件,瀏覽器是先使用,再通過檢查 manifest 文件是否有更新來更新緩存文件。這樣緩存文件可能用的不是最新的版本。
-
在更新緩存過程中,如果有一個文件更新失敗,則整個更新會失敗。
-
manifest 和引用它的HTML要在相同 HOST。
-
manifest 文件中的文件列表,如果是相對路徑,則是相對 manifest 文件的相對路徑。
-
manifest 也有可能更新出錯,導致緩存文件更新失敗。
-
沒有緩存的資源在已經緩存的 HTML 中不能載入,即使有網路。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/
-
manifest 文件本身不能被緩存,且 manifest 文件的更新使用的是瀏覽器緩存機制。所以 manifest 文件的 Cache-Control 緩存時間不能設置太長。
另外,根據官方文檔,AppCache 已經不推薦使用了,標準也不會再支持。現在主流的瀏覽器都是還支持 AppCache的,以後就不太確定了。
在Android 內嵌 Webview中,需要通過 Webview 設置介面啟用 AppCache,同時還要設置緩存文件的存儲路徑,另外還可以設置緩存的空間大小。
1 2 3 4 5 6 |
WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setAppCacheEnabled( true );
final String cachePath = getApplicationContext().getDir( "cache" , Context.MODE_PRIVATE).getPath();
webSettings.setAppCachePath(cachePath);
webSettings.setAppCacheMaxSize(5*1024*1024);
|
2.5 Indexed Database
IndexedDB 也是一種資料庫的存儲機制,但不同於已經不再支持的 Web SQL Database。IndexedDB 不是傳統的關係資料庫,可歸為 NoSQL 資料庫。IndexedDB 又類似於 Dom Storage 的 key-value 的存儲方式,但功能更強大,且存儲空間更大。
IndexedDB 存儲數據是 key-value 的形式。Key 是必需,且要唯一;Key 可以自己定義,也可由系統自動生成。Value 也是必需的,但 Value 非常靈活,可以是任何類型的對象。一般 Value 都是通過 Key 來存取的。
IndexedDB 提供了一組 API,可以進行數據存、取以及遍歷。這些 API 都是非同步的,操作的結果都是在回調中返回。
下麵代碼演示了 IndexedDB 中 DB 的打開(創建)、存儲對象(可理解成有關係數據的”表“)的創建及數據存取、遍歷基本功能。