H5 緩存機制淺析 - 移動端 Web 加載性能優化

騰訊 Bugly 特約做者:賀輝超javascript

1. H5 緩存機制介紹

H5,即 HTML5,是新一代的 HTML 標準,加入不少新的特性。離線存儲(也可稱爲緩存機制)是其中一個很是重要的特性。H5 引入的離線存儲,這意味着 web 應用可進行緩存,並可在沒有因特網鏈接時進行訪問。css

H5 應用程序緩存爲應用帶來三個優點:html

  • 離線瀏覽 用戶可在應用離線時使用它們前端

  • 速度 已緩存資源加載得更快html5

  • 減小服務器負載 瀏覽器將只從服務器下載更新過或更改過的資源。java

根據標準,到目前爲止,H5 一共有6種緩存機制,有些是以前已有,有些是 H5 才新加入的。web

  1. 瀏覽器緩存機制sql

  2. Dom Storgage(Web Storage)存儲機制數據庫

  3. Web SQL Database 存儲機制api

  4. Application Cache(AppCache)機制

  5. Indexed Database (IndexedDB)

  6. 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,在使用緩存文件時,是沒有對緩存文件內容進行校驗的,這樣緩存文件內容被修改的可能。

分析發現,瀏覽器的緩存機制還不是很是完美的緩存機制。完美的緩存機制應該是這樣的:

  1. 緩存文件沒更新,儘量使用緩存,不用和服務器交互;

  2. 緩存文件有更新時,第一時間能使用到新的文件;

  3. 緩存的文件要保持完整性,不使用被修改過的緩存文件;

  4. 緩存的容量大小要能設置或控制,緩存文件不能由於存儲空間限制或過時被清除。
    以X5爲例,第一、2條不能同時知足,第三、4條都不能知足。

在實際應用中,爲了解決 Cache-Control 緩存時長很差設置的問題,以及爲了」消滅304「,Web前端採用的方式是:

  1. 在要緩存的資源文件名中加上版本號或文件 MD5值字串,如 common.d5d02a02.js,common.v1.js,同時設置 Cache-Control:max-age=31536000,也就是一年。在一年時間內,資源文件若是本地有緩存,就會使用緩存;也就不會有304的回包。

  2. 若是資源文件有修改,則更新文件內容,同時修改資源文件名,如 common.v2.js,html頁面也會引用新的資源文件名。

經過這種方式,實現了:緩存文件沒有更新,則使用緩存;緩存文件有更新,則第一時間使用最新文件的目的。即上面說的第一、2條。第三、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 提供瞭如下的存儲接口:

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)時,頁面會話也是一直存在的。每在新標籤或者新窗口中打開一個新頁面,都會初始化一個新的會話。

<script type="text/javascript">
 // 當頁面刷新時,從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";
     }
 }
 </script>

<form action="./session_storage.html">
    <input type="text" name="name" id="name"/>
    <input type="button" value="Save" onclick="saveToStorage()"/>
</form>

當瀏覽器被意外刷新的時候,一些臨時數據應當被保存和恢復。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 中是能夠感知到的。

<script>
  //經過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;
</script>

<p>
    You have viewed this page
    <span id="count">an untold number of</span>
    time(s).
</p>

將上面代碼複製到 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。

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 的使用。

<script>
    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 = "<p>Found rows: " + len + "</p>";
             for(i=0; i<len; i++){
                 msg += "<p>" + results.rows.item(i).log + "</p>";
             }
             document.querySelector('#status').innerHTML =  msg;
             }, null);
      });
}

</script>

<div id="status" name="status">Status Message</div>

將上面代碼複製到 sql_database.html 中,用瀏覽器打開,可看到下面的內容。

官方建議瀏覽器在實現時,對每一個 HOST 的數據庫存儲空間做必定限制,建議默認是 5MB(分 HOST)的配額;達到上限後,能夠申請更多存儲空間。另外,如今主流瀏覽器 SQL Database 的實現都是基於 SQLite。

分析:SQL Database 的主要優點在於可以存儲結構複雜的數據,能充分利用數據庫的優點,可方便對數據進行增長、刪除、修改、查詢。因爲 SQL 語法的複雜性,使用起來麻煩一些。SQL Database 也不太適合作靜態文件的緩存。

在 Android 內嵌 Webview 中,須要經過 Webview 設置接口啓用 SQL Database,同時還要設置數據庫文件的存儲路徑。

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 機制的用法與功能。

<!DOCTYPE html>
<html manifest="demo_html.appcache">
<body>

<script src="demo_time.js"></script>

<p id="timePara"><button onclick="getDateTime()">Get Date and Time</button></p>
<p><img src="img_logo.gif" width="336" height="69"></p>
<p>Try opening <a href="tryhtml5_html_manifest.htm" target="_blank">this page</a>, then go offline, and reload the page. The script and the image should still work.</p>

</body>
</html>

上面 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 文件。文件比較簡單,第一行是關鍵字,第2、三行就是要緩存的文件路徑(相對路徑)。這只是最簡單的 manifest 文件,完整的還包括其餘關鍵字與內容。引用 manifest 文件的 HTML 和 manifest 文件中列出的要緩存的文件最終都會被瀏覽器緩存。

完整的 manifest 文件,包括三個 Section,類型 Windows 中 ini 配置文件的 Section,不過不要中括號。

  1. CACHE MANIFEST - Files listed under this header will be cached after they are downloaded for the first time

  2. NETWORK - Files listed under this header require a connection to the server, and will never be cached

  3. FALLBACK - Files listed under this header specifies fallback pages if a page is inaccessible

完整的 manifest 文件,如:

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。在實際使用中有些須要注意的地方,有一些能夠說是」坑「。

  1. 要更新緩存的文件,須要更新包含它的 manifest 文件,那怕只加一個空格。經常使用的方法,是修改 manifest 文件註釋中的版本號。如:# 2012-02-21 v1.0.0

  2. 被緩存的文件,瀏覽器是先使用,再經過檢查 manifest 文件是否有更新來更新緩存文件。這樣緩存文件可能用的不是最新的版本。

  3. 在更新緩存過程當中,若是有一個文件更新失敗,則整個更新會失敗。

  4. manifest 和引用它的HTML要在相同 HOST。

  5. manifest 文件中的文件列表,若是是相對路徑,則是相對 manifest 文件的相對路徑。

  6. manifest 也有可能更新出錯,致使緩存文件更新失敗。

  7. 沒有緩存的資源在已經緩存的 HTML 中不能加載,即便有網絡。例如:http://appcache-demo.s3-website-us-east-1.amazonaws.com/without-network/

  8. manifest 文件自己不能被緩存,且 manifest 文件的更新使用的是瀏覽器緩存機制。因此 manifest 文件的 Cache-Control 緩存時間不能設置太長。

另外,根據官方文檔,AppCache 已經不推薦使用了,標準也不會再支持。如今主流的瀏覽器都是還支持 AppCache的,之後就不太肯定了。

在Android 內嵌 Webview中,須要經過 Webview 設置接口啓用 AppCache,同時還要設置緩存文件的存儲路徑,另外還能夠設置緩存的空間大小。

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 的打開(建立)、存儲對象(可理解成有關係數據的」表「)的建立及數據存取、遍歷基本功能。

<script type="text/javascript">

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!");

    //保存全局的數據庫對象,後面會用到
    db = e.target.result;

   //綁定按鈕點擊事件
     document.querySelector("#addButton").addEventListener("click", addPerson, false);

    document.querySelector("#getButton").addEventListener("click", getPerson, false);

    document.querySelector("#getAllButton").addEventListener("click", getPeople, false);

    document.querySelector("#getByName").addEventListener("click", getPeopleByNameIndex1, false);
}

  //DB打開失敗回調
  openRequest.onerror = function(e) {
      console.log("Error");
      console.dir(e);
   }

}else{
    alert('Sorry! Your browser doesn\'t support the IndexedDB.');
}

//添加一條記錄
function addPerson(e) {
    var name = document.querySelector("#name").value;
    var email = document.querySelector("#email").value;

    console.log("About to add "+name+"/"+email);

    var transaction = db.transaction(["people"],"readwrite");
var store = transaction.objectStore("people");

   //Define a person
   var person = {
       name:name,
       email:email,
       created:new Date()
   }

   //Perform the add
   var request = store.add(person);
   //var request = store.put(person, 2);

   request.onerror = function(e) {
       console.log("Error",e.target.error.name);
       //some type of error handler
   }

   request.onsuccess = function(e) {
      console.log("Woot! Did it.");
   }
}

//經過KEY查詢記錄
function getPerson(e) {
    var key = document.querySelector("#key").value;
    if(key === "" || isNaN(key)) return;

    var transaction = db.transaction(["people"],"readonly");
    var store = transaction.objectStore("people");

    var request = store.get(Number(key));

    request.onsuccess = function(e) {
        var result = e.target.result;
        console.dir(result);
        if(result) {
           var s = "<p><h2>Key "+key+"</h2></p>";
           for(var field in result) {
               s+= field+"="+result[field]+"<br/>";
           }
           document.querySelector("#status").innerHTML = s;
         } else {
            document.querySelector("#status").innerHTML = "<h2>No match!</h2>";
         }
     }
}

//獲取全部記錄
function getPeople(e) {

    var s = "";

     db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) {
        var cursor = e.target.result;
        if(cursor) {
            s += "<p><h2>Key "+cursor.key+"</h2></p>";
            for(var field in cursor.value) {
                s+= field+"="+cursor.value[field]+"<br/>";
            }
            s+="</p>";
            cursor.continue();
         }
         document.querySelector("#status2").innerHTML = s;
     }
}

//經過索引查詢記錄
function getPeopleByNameIndex(e)
{
    var name = document.querySelector("#name1").value;

    var transaction = db.transaction(["people"],"readonly");
    var store = transaction.objectStore("people");
    var index = store.index("name");

    //name is some value
    var request = index.get(name);

    request.onsuccess = function(e) {
       var result = e.target.result;
       if(result) {
           var s = "<p><h2>Name "+name+"</h2><p>";
           for(var field in result) {
               s+= field+"="+result[field]+"<br/>";
           }
           s+="</p>";
    } else {
        document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
     }
   }
}

//經過索引查詢記錄
function getPeopleByNameIndex1(e)
{
    var s = "";

    var name = document.querySelector("#name1").value;

    var transaction = db.transaction(["people"],"readonly");
    var store = transaction.objectStore("people");
    var index = store.index("name");

    //name is some value
    index.openCursor().onsuccess = function(e) {
        var cursor = e.target.result;
        if(cursor) {
            s += "<p><h2>Key "+cursor.key+"</h2></p>";
            for(var field in cursor.value) {
                s+= field+"="+cursor.value[field]+"<br/>";
            }
            s+="</p>";
            cursor.continue();
         }
         document.querySelector("#status3").innerHTML = s;
     }
}

</script>

<p>添加數據<br/>
<input type="text" id="name" placeholder="Name"><br/>
<input type="email" id="email" placeholder="Email"><br/>
<button id="addButton">Add Data</button>
</p>

<p>根據Key查詢數據<br/>
<input type="text" id="key" placeholder="Key"><br/>
<button id="getButton">Get Data</button>
</p>
<div id="status" name="status"></div>

<p>獲取全部數據<br/>
<button id="getAllButton">Get EveryOne</button>
</p>
<div id="status2" name="status2"></div>

<p>根據索引:Name查詢數據<br/>
    <input type="text" id="name1" placeholder="Name"><br/>
    <button id="getByName">Get ByName</button>
</p>
<div id="status3" name="status3"></div>

將上面的代碼複製到 indexed_db.html 中,用 Google Chrome 瀏覽器打開,就能夠添加、查詢數據。在 Chrome 的開發者工具中,能查看建立的 DB 、存儲對象(可理解成表)以及表中添加的數據。

IndexedDB 有個很是強大的功能,就是 index(索引)。它可對 Value 對象中任何屬性生成索引,而後能夠基於索引進行 Value 對象的快速查詢。

要生成索引或支持索引查詢數據,需求在首次生成存儲對象時,調用接口生成屬性的索引。能夠同時對對象的多個不一樣屬性建立索引。以下面代碼就對name 和 email 兩個屬性都生成了索引。

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});

生成索引後,就能夠基於索引進行數據的查詢。

function getPeopleByNameIndex(e)
{
var name = document.querySelector("#name1").value;

var transaction = db.transaction(["people"],"readonly");
var store = transaction.objectStore("people");
var index = store.index("name");

//name is some value
var request = index.get(name);
request.onsuccess = function(e) {
    var result = e.target.result;
    if(result) {
        var s = "<p><h2>Name "+name+"</h2><p>";
        for(var field in result) {
            s+= field+"="+result[field]+"<br/>";
        }
        s+="</p>";
    } else {
        document.querySelector("#status3").innerHTML = "<h2>No match!</h2>";
    }
  }
}

分析:IndexedDB 是一種靈活且功能強大的數據存儲機制,它集合了 Dom Storage 和 Web SQL Database 的優勢,用於存儲大塊或複雜結構的數據,提供更大的存儲空間,使用起來也比較簡單。能夠做爲 Web SQL Database 的替代。不太適合靜態文件的緩存。

  1. 以key-value 的方式存取對象,能夠是任何類型值或對象,包括二進制。

  2. 能夠對對象任何屬性生成索引,方便查詢。

  3. 較大的存儲空間,默認推薦250MB(分 HOST),比 Dom Storage 的5MB 要大的多。

  4. 經過數據庫的事務(tranction)機制進行數據操做,保證數據一致性。

  5. 異步的 API 調用,避免形成等待而影響體驗。

Android 在4.4開始加入對 IndexedDB 的支持,只需打開容許 JS 執行的開關就行了。

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);

2.6 File System API

File System API 是 H5 新加入的存儲機制。它爲 Web App 提供了一個虛擬的文件系統,就像 Native App 訪問本地文件系統同樣。因爲安全性的考慮,這個虛擬文件系統有必定的限制。Web App 在虛擬的文件系統中,能夠進行文件(夾)的建立、讀、寫、刪除、遍歷等操做。

File System API 也是一種可選的緩存機制,和前面的 SQLDatabase、IndexedDB 和 AppCache 等同樣。File System API 有本身的一些特定的優點:

  1. 能夠知足大塊的二進制數據( large binary blobs)存儲需求。

  2. 能夠經過預加載資源文件來提升性能。

  3. 能夠直接編輯文件。

瀏覽器給虛擬文件系統提供了兩種類型的存儲空間:臨時的和持久性的。臨時的存儲空間是由瀏覽器自動分配的,但可能被瀏覽器回收;持久性的存儲空間須要顯示的申請,申請時瀏覽器會給用戶一提示,須要用戶進行確認。持久性的存儲空間是 WebApp 本身管理,瀏覽器不會回收,也不會清除內容。持久性的存儲空間大小是經過配額來管理的,首次申請時會一個初始的配額,配額用完須要再次申請。

虛擬的文件系統是運行在沙盒中。不一樣 WebApp 的虛擬文件系統是互相隔離的,虛擬文件系統與本地文件系統也是互相隔離的。

File System API 提供了一組文件與文件夾的操做接口,有同步和異步兩個版本,可知足不一樣的使用場景。下面經過一個文件建立、讀、寫的例子,演示下簡單的功能與用法。

<script type="text/javascript">

window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;

//請求臨時文件的存儲空間
if (window.requestFileSystem) {
     window.requestFileSystem(window.TEMPORARY, 5*1024*1024, initFS, errorHandler);
}else{
  alert('Sorry! Your browser doesn\'t support the FileSystem API');
}

//請求成功回調
function initFS(fs){

  //在根目錄下打開log.txt文件,若是不存在就建立
  //fs就是成功返回的文件系統對象,fs.root表明根目錄
  fs.root.getFile('log.txt', {create: true}, function(fileEntry) {

  //fileEntry是返回的一個文件對象,表明打開的文件

  //向文件寫入指定內容
  writeFile(fileEntry);

  //將寫入的內容又讀出來,顯示在頁面上
  readFile(fileEntry);

  }, errorHandler);
}

//讀取文件內容
function readFile(fileEntry)
{
    console.log('readFile');

   // Get a File object representing the file,
   // then use FileReader to read its contents.
   fileEntry.file(function(file) {

     console.log('createReader');

      var reader = new FileReader();

      reader.onloadend = function(e) {

        console.log('onloadend');

        var txtArea = document.createElement('textarea');
        txtArea.value = this.result;
        document.body.appendChild(txtArea);
      };

      reader.readAsText(file);
   }, errorHandler);
}

//向文件寫入指定內容
function writeFile(fileEntry)
{
    console.log('writeFile');

    // Create a FileWriter object for our FileEntry (log.txt).
    fileEntry.createWriter(function(fileWriter) {

      console.log('createWriter');

      fileWriter.onwriteend = function(e) {
        console.log('Write completed');
      };

        fileWriter.onerror = function(e) {
          console.log('Write failed: ' + e.toString());
        };

        // Create a new Blob and write it to log.txt.
        var blob = new Blob(['Hello, World!'], {type: 'text/plain'});

        fileWriter.write(blob);

     }, errorHandler);
}

function errorHandler(err){
 var msg = 'An error occured: ' + err;
 console.log(msg);
};

 </script>

將上面代碼複製到 file_system_api.html 文件中,用 Google Chrome 瀏覽器打開(如今 File System API 只有 Chrome 43+、Opera 32+ 以及 Chrome for Android 46+ 這三個瀏覽器支持)。因爲 Google Chrome 禁用了本地 HTML 文件中的 File System API功能,在啓動 Chrome 時,要加上」—allow-file-access-from-files「命令行參數。

上面截圖,左邊是 HTML 運行的結果,右邊是 Chrome 開發者工具中看到的 Web 的文件系統。基本上 H5的幾種緩存機制的數據都能在這個開發者工具看到,很是方便。

分析:File System API 給 Web App 帶來了文件系統的功能,Native 文件系統的功能在 Web App 中都有相應的實現。任何須要經過文件來管理數據,或經過文件系統進行數據管理的場景都比較適合。

到目前,Android 系統的 Webview 還不支持 File System API。

3 移動端 Web 加載性能(緩存)優化

分析完 H5提供的各類緩存機制,回到移動端(針對 Android,可能也適用於 iOS)的場景。如今 Android App(包括手 Q 和 WX)大多嵌入了 Webview 的組件(系統 Webview 或 QQ 遊覽器的 X5組件),經過內嵌Webview 來加載一些H5的運營活動頁面或資訊頁。這樣可充分發揮Web前端的優點:快速開發、發佈,靈活上下線。但 Webview 也有一些不可忽視的問題,比較突出的就是加載相對較慢,會相對消耗較多流量。

經過對一些 H5頁面進行調試及抓包發現,每次加載一個 H5頁面,都會有較多的請求。除了 HTML 主 URL 自身的請求外,HTML外部引用的 JS、CSS、字體文件、圖片都是一個獨立的 HTTP 請求,每個請求都串行的(可能有鏈接複用)。這麼多請求串起來,再加上瀏覽器解析、渲染的時間,Web 總體的加載時間變得較長;請求文件越多,消耗的流量也會越多。咱們可綜合使用上面說到幾種緩存機制,來幫助咱們優化 Web 的加載性能。

結論:綜合各類緩存機制比較,對於靜態文件,如 JS、CSS、字體、圖片等,適合經過瀏覽器緩存機制來進行緩存,經過緩存文件可大幅提高 Web 的加載速度,且節省流量。但也有一些不足:緩存文件須要首次加載後纔會產生;瀏覽器緩存的存儲空間有限,緩存有被清除的可能;緩存的文件沒有校驗。要解決這些不足,能夠參考手 Q 的離線包,它有效的解決了這些不足。

對於 Web 在本地或服務器獲取的數據,能夠經過 Dom Storage 和 IndexedDB 進行緩存。也在必定程度上減小和 Server 的交互,提升加載速度,同時節省流量。

固然 Web 的性能優化,還包括選擇合適的圖片大小,避免 JS 和 CSS 形成的阻塞等。這就須要 Web 前端的同事根據一些規範和一些調試工具進行優化了。


想了解更多幹貨,請搜索關注公衆號:騰訊Bulgy,或搜索微信號:weixinBugly,關注咱們。

騰訊Bugly

Bugly是騰訊內部產品質量監控平臺的外發版本,支持iOS和Android兩大主流平臺,其主要功能是App發佈之後,對用戶側發生的crash以及卡頓現象進行監控並上報,讓開發同窗能夠第一時間瞭解到app的質量狀況,及時修改。目前騰訊內部全部的產品,均在使用其進行線上產品的崩潰監控。騰訊內部團隊4年打磨,目前騰訊內部全部的產品都在使用,基本覆蓋了中國市場的移動設備以及網絡環境,可靠性有保證。使用Bugly,你就使用了和手機QQ、QQ空間、手機管家相同的質量保障手段

相關文章
相關標籤/搜索