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 的打開(創建)、存儲對象(可理解成有關係數據的」表「)的創建及數據存取、遍歷基本功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
var
db;
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
//瀏覽器是否支持IndexedDB
if
(window.indexedDB) {
//打開數據庫,如果沒有,則創建
var
openRequest = window.indexedDB.open(
"people_db"
, 1);
//DB版本設置或升級時回調
openRequest.onupgradeneeded =
function
(e) {
console.log(
"Upgrading..."
);
var
thisDB = e.target.result;
if
(!thisDB.objectStoreNames.contains(
"people"
)) {
console.log(
"Create Object Store: people."
);
//創建存儲對象,類似於關係數據庫的表
thisDB.createObjectStore(
"people"
, { autoIncrement:
true
});
//創建存儲對象, 還創建索引
//var objectStore = thisDB.createObjectStore("people",{ autoIncrement:true });
// //first arg is name of index, second is the path (col);
//objectStore.createIndex("name","name", {unique:false});
//objectStore.createIndex("email","email", {unique:true});
}
}
//DB成功打開回調
openRequest.onsuccess =
function
(e) {
console.log(
"Success!"
);
//保存全局的數據庫對象,後面會用到
"Success!"
);
//保存全局的數據庫對象,後面會用到
db = e.target.result;
//綁定按鈕點擊事件
document.querySelector(
"#addButton"
).addEventListener(
|