2022年1月20日 星期四

HTML5 測試 : 瀏覽器的本地資料儲存區 Web Storage (一)

由於 IE 即將於今年中停止更新, 我之前用 ActiveX 在 IE 上寫的工作自動化網頁程式因為不能再使用 FileObject 等元件了, 可能需要進行改版, 打算改用 HTML5 的 localStorage 來取代. 

在 HTML5 出現以前, 瀏覽器只能用 cookie 將資料以鍵值對方式儲存於本機中, 當瀏覽器提出請求時, 它會將 cookie 同時傳送至後端, 伺服器會根據此 cookie 來處理請求, 然後將 cookie 隨同回應傳回瀏覽器, 因此使用 cookie 會增加網路流量, 且其在本機中的容量僅 4KB 而已. 

支援 HTML5 的瀏覽器除了原本的 cookie 外, 還支援了下列四種本地資料儲存方式 :
  • localStoage : 永久資料儲存區
  • sessionStorage : 連線資料儲存區
  • indexedDB database : 永久資料庫
  • web SQL database : 永久資料庫 (可使用 SQL 存取)
其中 localStoage 與 sessionStoage 用法完全一樣, 兩者差別只是儲存時間而已, localStoage 的資料除非人為清除, 否則會永久儲存於檔案系統中; 而 sessionStorage 則僅存在於連線存續期間, 只要關閉網頁就會消失. Web SQL 資料庫正式名稱為 openDatabase, 其特點是可用 SQL 語法來存取資料庫, 但因為 SQL 方言眾多與其 API 開發較複雜, 其規範已在 2010 年被 W3C 廢止, W3C 建議使用 indexedDB 資料庫, 不過 Google 的桌電版 Chrome 仍持續完整支援 web SQL. 

localStorge 與 web SQL database 其實都是在本機建立一個用來儲存鍵值對資料的 sqlite 資料庫, 差別是 localStorage 的 SQLite 資料庫較簡單, 它只有一個名為 ItemTable 的資料表, 裡面只有 key 與 value 這兩個欄位而已; 而 indexedDB 則可以建立多個資料庫與資料表, 欄位的鍵也可以自訂. 這些本地儲存的容量 W3C 建議值為每個瀏覽器每個 URL (Origin) 5MB (IE 則允許 10MB), 參考 :


這些 Web Storage 都可在 Chrome 的開發者頁面瀏覽與管理, 在 Chrome 按 F12 或 Ctrl+Shift+I 進入開發者頁面點選 "Application" 頁籤底下的 "Storage/Local Storage/File" 即可看到 : 




在每筆儲存資料上按滑鼠右鍵可進行編輯與刪除等動作. 

本篇主要是測試  localStorage 與 sessionStorage 的用法, 參考書籍如下 :

# 打造 HTML5 + CSS3 網頁設計法則 (松崗, 2013) 第 11 章
# HTML5 完美風暴 第二版 (藍海文化, 2013) 第 12 章


首先來測試一下瀏覽器對上面 4 種本地儲存的支援情形 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='supports'></div>
    <script>
      var arr=[];
      if (window.localStorage){arr.push("<li>localStorage</li>");}
      if (window.sessionStorage){arr.push("<li>sessionStorage</li>");}
      if (window.openDatabase){arr.push("<li>Web SQL Database</li>");}
      if (window.indexedDB){arr.push("<li>indexedDB Database</li>");}
      var supports='此瀏覽器支援下列本地儲存方式 :<br>' +
                   '<ul>' + arr.join('\n') + '</ul>'; 
      document.getElementById('supports').innerHTML=supports;
    </script>
  </body>
</html>

此處用陣列 arr 來儲存測試結果, 有支援就存入陣列中, 最後呼叫其 join() 方法來組成字串. 在 Chrome/Edge 瀏覽器測試結果如下 : 


可見 Chrome 四種都有支援. 但 Android, Safari, 以及 Firefox 等則不再支援 Web SQL 資料庫了, 下面是 Firefox 的結果 :


不過 Chrome 也只在桌上型或筆電才繼續支援 web SQL 資料庫, Android 上的 Chrome 並不支援 Web SQL, 下面是用 Android 手機上的 Chrome 測試的結果 : 




一. 使用 localStorage 物件儲存網頁資料 : 

如上面所述, localStorage 物件可用來長久保存網頁資料, 除非使用者主動刪除, 否則它會一直儲存於一個 SQLite 資料庫的特定資料表 ItemTable 中, 即使關閉網頁重開此資料仍存在, 亦即它能跨網頁共用 (但不能跨瀏覽器共用, 不同的瀏覽器有自己的儲存區), 參考 W3C 的  Web Storage API 規範 :


localStorage 與 sessionStorage 物件都具有一個固定屬性 length, 可用來查詢目前各自的物件儲存庫中已有多少個鍵值對 (資料筆數), 方法則有 5 個, 可用來操作資料庫, 如下表所示 : 


 localStorage 物件方法 說明
 setItem(key, value) 儲存鍵值對 "key/value" 資料 (若 key 已存在就覆蓋舊值)
 getItem(key) 傳回鍵 key 對應之值 (字串), 若 key 不存在傳回 null
 key(n) 傳回第 n 個鍵值對 ("key/value" 字串), key 不存在傳回 null
 removeItem(key) 刪除鍵為 key 之資料
 clear() 清除所有鍵值對資料


注意, 傳入 setItem() 的第一參數 key 須為字串, 值可以是字串也可以是數值, 數值會自動被轉換為字串儲存於鍵值對中, 因此 getItem() 或 key() 的傳回值都是字串, 值的部分若是數值須用 parseInt() 或 parseFloat() 轉換.  sessionStorage 物件用法與 localStorage 物件相同, 擁有相同的屬性與方法.

除了使用上表中的方法來存取鍵值對資料外, 也可以將資料中的鍵當作是 localStorage 物件的屬性, 因此也可以使用 .key 方式存取. 或者將 localStorage 與 sessionStorage 物件當成關聯式陣列, 以 [] 運算子存取鍵值對資料. 以下測試雖然針對 localStorage, 但 sessionStorage 用法完全一樣, 如上所述兩者差別只在於保存期限, localStorage 永久有效; 而 sessionStorage 則在關閉網頁後消失. 

下面範例是使用 localStorage 物件方法新增一個鍵值對來記錄網頁被瀏覽次數 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='msg'></div>
    <script>
      if (window.localStorage.getItem("page_count")==null){  //第一次瀏覽
        window.localStorage.setItem("page_count", 0);    //設定初始值=0
        }
      var current_count=window.localStorage.getItem("page_count");
      var new_count=parseInt(current_count) + 1;    //次數增量
      window.localStorage.setItem("page_count", new_count);     //更改鍵值對
      var msg="已載入此網頁 " + new_count + " 次";
      document.getElementById("msg").innerHTML=msg;
    </script>
  </body>
</html>

此例一開始先判斷是否為第一次載入網頁, 是的話建立計數器, 並將初始值設為 0, 然後呼叫 getItem() 取出現值, 呼叫 parseInt() 轉成整數後增量 1 存回去, 結果如下 :



注意, 一個網頁應用程式的 localStorage 的儲存區是一個瀏覽器一份, 同一種瀏覽器開兩個頁籤載入此網頁其值是共用的; 以此範例為例, 用不同瀏覽器開啟會各自從 0 開始計數, 不會跟另一個瀏覽器共用. 具體而言, 瀏覽器會對每一個 URL (又稱為 Origin, 即協定://主機:埠號) 都開一個 localStorage 資料儲存區, 所以不同 URL 的應用程式不會互相干擾. 

下面範例則是將 localStorage 物件視為關聯式陣列 (相當於 Python 的字典), 鍵值對中的鍵即為其鍵, 使用 [] 運算子存取 : 



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='msg'></div>
    <script>
      if(window.localStorage["page_count"]==null){
        window.localStorage["page_count"]=0;
        }
      var current_count=window.localStorage["page_count"];
      var new_count=parseInt(current_count) + 1;
      window.localStorage["page_count"]=new_count;
      var msg="已載入此網頁 " + new_count + " 次";
      document.getElementById("msg").innerHTML=msg;
    </script>
  </body>
</html>

下面是將鍵值對中的鍵當成 localStorage 物件的屬性來存取儲存區的資料 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='msg'></div>
    <script>
      if(window.localStorage.page_count==null){
        window.localStorage.page_count=0;
        }
      var current_count=window.localStorage.page_count;
      var new_count=parseInt(current_count) + 1;
      window.localStorage.page_count=new_count;
      var msg="已載入此網頁 " + new_count + " 次";
      document.getElementById("msg").innerHTML=msg;
    </script>
  </body>
</html>

結果與上面兩個完全一樣. 

由於網頁儲存區物件都位於 DOM 根結點 window 物件下, 所以存取時並不需要參考 window, 瀏覽器會在物件列表中自動搜尋, 所以為了少打些字, 上面範例中的 window.localStorage 可省略前面的 "window.", 如下面範例所示 : 



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='msg'></div>
    <script>
      if(localStorage.page_count==null){
        localStorage.page_count=0;      //使用物件屬性存取
        }
      var current_count=localStorage.getItem("page_count");  //使用物件方法存取
      var new_count=parseInt(current_count) + 1;    
      localStorage["page_count"]=new_count;       //使用陣列存取
      var msg="已載入此網頁 " + new_count + " 次";
      document.getElementById("msg").innerHTML=msg;
    </script>
  </body>
</html>

此例在存取 localStorage 物件時省略根結點 window, 同時綜合使用了物件屬性, 方法, 以及陣列這三種存取儲存區資料, 結果也是與上面三個相同. 

由於 Wen Storage 的容量照 W3C 建議預設是 5MB, 若呼叫 setItem() 方法新增資料時超過此容量會出現 QUOTA_EXCEEDED_ERR 錯誤, 因此嚴謹一點的網頁程式會用 try catch 捕捉此例外以避免被使用者看到, 例如 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <div id='msg'></div>
    <script>
      if(localStorage.page_count==null){
        try {localStorage.page_count=0;}
        catch (exception){
          document.getElementById("msg").innerHTML="容量超出限制!";
          }
        }
      var current_count=localStorage.getItem("page_count");
      var new_count=parseInt(current_count) + 1;
      try {localStorage["page_count"]=new_count;}
      catch (exception){
        document.getElementById("msg").innerHTML="容量超出限制!";
        } 
      var msg="已載入此網頁 " + new_count + " 次";
      document.getElementById("msg").innerHTML=msg;
    </script>
  </body>
</html>

哇, 一堆 try catch 看起來好亂. 其實 5MB 對一般網頁應用程式應該足夠, 所以往後測試 try catch j我看就免了吧! 

localStorage 儲存區很適合拿來儲存網頁應用程式的設定值, 例如字型, 背景顏色等網頁的設定值, 如下面的範例所示 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <p>更改並儲存網頁背景色與字型大小 : </p>
    <input type="button" value="Red" onclick="set_bg(this.value)">
    <input type="button" value="Green" onclick="set_bg(this.value)">
    <input type="button" value="Blue" onclick="set_bg(this.value)">
    <input type="button" value="Aqua" onclick="set_bg(this.value)">
    <input type="button" value="Lime" onclick="set_bg(this.value)">
    <input type="button" value="White" onclick="set_bg(this.value)"><br><br>
    <input type="button" value="Small" onclick="set_size('0.5em')">
    <input type="button" value="Medium" onclick="set_size('1em')">
    <input type="button" value="Large" onclick="set_size('2em')">
    <script>
      function set_bg(color){   //設定網頁背景色同時儲存設定值
        document.body.style.backgroundColor=color;
        localStorage["background_color"]=color;        
        }
      function set_size(size){    //設定網頁字型大小同時儲存設定值
        document.body.style.fontSize=size;
        localStorage["font_size"]=size;
        }
      if(localStorage["background_color"]==null){    //設定背景色初始值
        localStorage["background_color"]="White";
        }
      if(localStorage["font_size"]==null){     //設定字型大小初始值
        localStorage["font_size"]="1em";
        } 
      set_bg(localStorage["background_color"]);   //依據上次儲存資料設定背景色
      set_size(localStorage["font_size"]);   //依據上次儲存資料設定字型大小
    </script>
  </body>
</html>

此例自訂兩個函式 set_bg() 與 set_size() 來設定網頁背景色與字型大小同時儲存設定值. 按下個按鈕時會觸發 onclick 事件呼叫這兩個函式, 網頁重新載入時也會呼叫它們以回復上一次的設定值, 因此在同一個瀏覽器即使關掉此網頁再重開, 關掉前的設定值會被叫回來, 顯示與上次相同的環境 (但不同瀏覽器有各自儲存區, 不會互相干擾), 下面是按下 Red 與 Large 鈕的結果 : 




關於色碼參考 : 


下面這個範例是 localStorage 物件屬性與方法的綜合測試 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <p>存取 localStorage 資料 : </p>
    <label for="k">鍵(k):</label> 
    <input type="text" id="k"><br>
    <label for="v">值(v):</label>
    <input type="text" id="v"><br><br>
    <label for="k">存取:</label>
    <button id="set_data">設定</button>
    <button id="show_data">顯示</button>
    <button id="remove_data">刪除</button>
    <button id="clear_data">清除</button><br>
    <div id="output"></div>
    <script>
      function $(id) {return document.getElementById(id);}
      function set_data(){
        var k=$('k').value;
        var v=$('v').value;
        localStorage.setItem(k, v); 
        show_data();
        }
      function show_data(){
        var arr=["<ul>"];       //利用陣列來儲存要輸出之清單元素
        for (var i=0; i<localStorage.length; i++){    //走訪儲存區資料
          var k=localStorage.key(i);    
          var v=localStorage.getItem(k);          
          arr.push("<li>" + k + ":" + v + "</li>");
          }
        arr.push("</ul>");
        $("output").innerHTML=arr.join("");
        } 
      function remove_data(){
        var k=$('k').value;
        localStorage.removeItem(k);  
        show_data();
        } 
      function clear_data(){
        localStorage.clear(k); 
        show_data();
        } 
      $("set_data").onclick=set_data;   //設定按鈕事件處理函式
      $("show_data").onclick=show_data;
      $("remove_data").onclick=remove_data;
      $("clear_data").onclick=clear_data;
    </script>
  </body>
</html>

此例使用兩個 input 元素來輸入 key 與 value, 按底下四個按鈕分別執行資料儲存, 顯示, 刪除, 以及清除等操作. 與上面範例不同之處為這裡模仿 jQuery 定義了一個 $() 函式來減少透過 id 存取網頁表單元素的打字長度, 並利用此來為按鈕設定事件處理函式. 不論是按 "設定", "刪除", 或 "清除" 都會呼叫 show_data() 顯示儲存區資料. 

以下是連續按 "設定" 鈕儲存四個資料的結果 :


在 key 欄輸入 "motto2" 後按刪除結果如下 : 

 
直接按 "清除" 鈕會刪除全部 localStorage 資料 : 


此例可說完整地用到了 localStorage 物件中的全部成員了. 

雖然 localStorage 只能儲存字串, 但其實也是可以儲存物件的, 只要利用 JSON 物件的 stringfy() 方法將要儲存的物件轉成字串, 取出時再用 parse() 方法轉回物件即可, 如下例所示 : 



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <p>存取 localStorage 物件資料 : </p>
    <label for="k">存取:</label>
    <button id="set_data">設定</button>
    <button id="show_data">顯示</button>
    <button id="remove_data">刪除</button>
    <button id="clear_data">清除</button><br>
    <div id="output"></div>
    <div id="item"></div>   
    <script>
      function $(id) {return document.getElementById(id);}
      function set_data(){
        var k=Math.round(Math.random()*10000);   //key 為 0~9999 隨機數
        var obj={user:'tony', datetime:new Date};     //value 為物件資料
        var v=JSON.stringify(obj);     //將物件轉成字串
        localStorage.setItem(k, v);    //存入儲存區
        show_data();
        }
      function show_data(){
        var arr=["<ul>"];
        for (var i=0; i<localStorage.length; i++){  
          var k=localStorage.key(i);   
          var v=localStorage.getItem(k); 
          var item="<li><a href='#' onclick='get_item(" + k + ")'>" + 
                   k + ":" + v + "</li>";
          arr.push(item);
          }
        arr.push("</ul>");
        $("output").innerHTML=arr.join("");
        } 
      function remove_data(){
        var k=$('k').value;
        localStorage.removeItem(k);  
        show_data();
        } 
      function clear_data(){
        localStorage.clear(); 
        show_data();
        } 
      function get_item(k){   
        var obj=JSON.parse(localStorage.getItem(k));    //將字串轉回物件
        var item="user=" + obj.user + " datetime=" + obj.datetime;
        $("item").innerHTML=item;
        } 
      $("set_data").onclick=set_data;
      $("show_data").onclick=show_data;
      $("remove_data").onclick=remove_data;
      $("clear_data").onclick=clear_data;
    </script>
  </body>
</html>

此例是在上面範例的基礎上改寫的, 主要改變是新增的資料值為利用 JSON.stringify() 字串化的物件 (鍵為隨機數), 按 "設定" 鈕就會新增一筆資料. 關於隨機數的產生參考 :


其次是在網頁中新增一個 id=get_item 的 div 元素用來顯示每筆資料的內容, 另外是在顯示的資料清單上, 為每筆資料加上超連結按鈕, 點擊每筆資料就嘿呼叫 get_item() 函式來顯示資料內容, 結果如下 : 



操作時先連續按 "設定" 鈕, 然後再按 "顯示" 鈕, 這時底下就會出現資料清單了, 點擊任一超連結就會在最底下顯示這筆資料 (使用 JSON.parse() 轉回物件). 

瀏覽器的 Web Storage 有定義一個 StorageEvent 介面, 用來捕捉儲存區的資料變動事件, 只要儲存區內容有變動 (新增, 更新, 或刪除) 都會觸發此事件 (讀取不會觸發), 因此只要監聽 window 物件的 change 事件, 當發生儲存區資料變動時會傳回一個事件物件, 透過此事件物件的屬性可即時取得變動資訊 :
  • key : 被變動資料之鍵
  • oldValue : 被變動資料之原值
  • newValue : 被變動資料之新值
  • url : 應用程式之 URL
在下面的範例中會準備兩個網頁, 一個是用來透過新增或刪除鍵值對來觸發 StorageEvent 事件; 另一個用來偵測此事件並顯示資料變化情形, 例如 : 



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <p>觸發儲存區資料變動事件 (StorageEvent) : </p>
    <label>鍵 :</label>
    <input type="text" id="k"><br>
    <label>值 :</label>
    <input type="text" id="v"><br><br>
    <button onclick="set_data()">新增</button>
    <button onclick="remove_data()">刪除</button>
    <input type="hidden" id="keys">
    <div id="output"></div>
    <script>
      function $(id) {return document.getElementById(id);}
      function set_data(){
        var k=$("k").value;
        var v=$("v").value; 
        var keys=$("keys").value;
        var arr=$("keys").value.split(",");
        arr.push(k);
        $("keys").value=arr.join(",");
        localStorage.setItem(k, v);  
        show_data();
        }
      function remove_data(){
        localStorage.removeItem(localStorage.key(0));
        show_data();
        } 
      function show_data(){
        var arr=["<ul>"];
        for (var i=0; i<localStorage.length; i++){  
          var k=localStorage.key(i);   
          var v=localStorage.getItem(k); 
          var item="<li><a href='#' onclick='get_item(" + k + ")'>" + 
                   k + ":" + v + "</li>";
          arr.push(item);
          }
        arr.push("</ul>");
        $("output").innerHTML=arr.join("");
        } 
    </script>
  </body>
</html>

按此網頁中的 "新增" 鈕會在 localStorage 儲存區新增一組鍵值對, 按 "刪除" 鈕則會刪除索引為 0 的鍵值對, 這都會觸發 storageChange 事件, 而這事件會被下面這個偵測網頁同步捕捉而在葉面上顯示資料變動情形 :



<!DOCTYPE html>
<html>
  <head>
    <title>localStorage 測試</title>
    <meta charset="utf-8">
  </head>
  <body>
    <p>偵測儲存區資料變動事件 (StorageEvent) : </p>
    <ul id="item">
      <li id="key">null</li>
      <li id="oldValue">null</li>
      <li id="newValue">null</li>
      <li id="url">null</li>
    </ul>
    <script>
      function $(id) {return document.getElementById(id);}
      function storage_changed(e){
        $("key").innerHTML="key: " + e.key;
        $("oldValue").innerHTML="oldValue: " + e.oldValue;
        $("newValue").innerHTML="newValue: " + e.newValue;
        $("url").innerHTML="url: " + e.url;
        } 
      window.onload=function() {   
        window.addEventListener("storage", storage_changed, false);    
        }
    </script>
  </body>
</html>

此網頁中定義了 storage_changed() 函式來處理 StorageEvent 事件, 當此事件發生時會在葉面上顯示變動情形. 同時開啟上面兩個網頁, 感測頁面初始時都顯示 null : 


然後在出發網頁輸入鍵值對後按 "新增" : 


這時偵測網頁馬上會顯示資料變動 :


可見 key=aaa 原值為 null, 新值為 aaa. 

接著在觸發網頁 key=aaa 的值改為 bbb 按新增 (key 相同的話就是 update) : 


這時偵測網頁上變成如下頁面 : 


可見 k=aaa 的原值為 aaa, 新值變成 bbb 了.

參考 : 

沒有留言:

張貼留言