2022年1月30日 星期日

大掃除與貼春聯

因為菁菁週六週日肯德基要上班, 所以昨天下午我先回鄉下幫爸大掃除, 主要是擦拭祖堂內的供桌, 祖宗牌位外的玻璃框, 清洗外面露台, 窗戶等等. 今日早上則是去附近山上的五穀宮拜神農大帝, 往年都是準備水果餅乾, 前幾天松美伯母送來好幾斤豬肉, 爸說那我們再買隻雞, 今年備牲禮吧! 

下午主要是張貼紅紙與春聯, 春聯是岳母給的, 似乎是鎮公所有請人揮毫, 上週拿了一付給我. 但春聯 哪一個要貼左邊 (下聯)? 哪個要貼右邊 (上聯)? 我查到下面這篇文章, 原來是要根據最後一個字的平仄來決定 : 右上仄, 左下平, 亦即最後一個字是仄聲 (第三, 四聲) 為上聯要貼右邊; 若是平聲 (第一, 二聲) 為下聯要貼左邊, 參考 : 


紅紙主要是五福臨門, 每個門窗都要貼 (估計需要 30 張), 樓上難貼的除外. 祖牌上方 (5 張) 與土地龍神 (3 張) , 天神  (3 張) , 以及灶君 (3 張) 則要貼天官賜福, 共需要 14 張. 傍晚提早吃晚餐後回高雄載大家回鄉下. 

今天可說是從早忙到晚. 

2022年1月26日 星期三

2022 年第 4 周記事

怎麼才剛跨年, 一月份已近尾聲? 實在令人心慌慌, 感覺事情沒做多少, 時間卻跑得飛快. 其實不管你做多做少, 時間一樣向前跑, 或許, 不做甚麼反而會覺得日子過得好慢哩. 本周六補班 (年初三), 但我卻不知道, 還好週五打電話回家時, 爸說月曆上明天不是紅字ㄟ, 我問同事才知要補班, 以前遇到補班就會有朋友傳唐伯虎點秋香來提醒, 這次很奇怪居然沒有. 因為要補班只好把原先預約的周六早上 Nissan 裕昌汽車保養取消. 

這幾天在忙著寫個同事拜託的小 App, 所以周記到了周三才想到沒寫. 本周由於 omicron 本土疫情擴散, 鬧得人心又惶惶不可終日, 開店生意的心最苦啦, 像小阿姨的火鍋店才剛剛恢復點元氣, 突然的疫情爆發又要擔心店租, 營收這些. 因為疫情轉趨嚴峻, 週五接到愛心通知, 取消所有探視預約. 

去年底種植的玉米本周已開始採收, 大約採了十餘支帶來高雄, 煮蔬菜湯加玉米湯汁就很有味道了. 冬天快過去, 菜園的草莓也開花結果, 本周爸採收了一盒較我帶來高雄給菁菁她們, 清洗時才發現非常大顆, 只是有些被蟲咬過, 這讓我下定決心, 今年一定要把直立式草莓栽培架做起來, 就建在就豬舍牆上, 加上自動灑水系統一定衛生又肥美. 

本周除了看了一些 pandas 外, 大部分時間都在測試 localStorage 與用此技術搭建的備忘錄 App, 還好我每學習一項東西都會花時間將心得與做法寫成筆記, 才能在短短幾天把已快忘光的東西快速恢復功力. 這次的經驗也讓我學到一些 jQuery Mobile 與 Javacript 的新技巧.  

周六早上上班前發現 QRV 大灰熊右前輪漏氣扁一半, 下班後先用電動打氣機打飽後開到八百屋查修, 發現是以前補胎的地方會慢慢漏氣, 難怪班個月來打飽後大約六~七天又會漏到一半. 八百屋的技師說補胎過的地方不能再補必須換新胎, 且現胎再跑頂多一萬公里也該換了,  建議四個一起換, 剛好她們米其林 2019 年的有特價, 一條 2600 元, 加購一個胎壓計 250 元, 合計 10450 元. 

jQuery Mobile 學習筆記 (二十三) : 用 Web Storage 製作備忘錄 App (中)

今天同事安裝了這款 App 試用後說還不錯, 但少了一個備忘分類的欄位, 這在以前工作日誌系統中可以很方便地用下拉式選單選取, 也可以做為搜尋欄位, 但 localStorage 就很陽春的 key:value 物件資料庫啊! 沒辦法多出其他欄位. 不過仔細想想也不是沒辦法, 至少有兩個解決方案 :
  1. 將多出來的分類欄位與備忘記事欄以字串方式串接存入 value.
  2. 將分類與備忘記事以物件方式儲存, 然後用 JSON.stringify() 轉成字串存入 value.
於是晚上便動手來試試看. 首先測試第一個方案, 參考上一篇來改 :


這次想連同 render_memo_list() 都改成用 "|" 號來串接, 因為此符號較少用, 拆分字串時誤拆的機率比較低. 儲存在 localStorage 中的物件以寫入的日期時間當作 key, 分類與備忘記事以 "|" 符號串接後做為 value, 例如 :

{"2022-01-24T08:45:27": "問候|Hello World"}
{"2022-01-24T09:31:27": "問候|你是在說哈囉嗎?"}

在函式 sort_local_storage() 中, 也是將 key 與 value 用 "|" 符號串接, 其傳回陣列例如 :

"2022-01-24T09:31:27|問候|你是在說哈囉嗎?", "2022-01-24T08:45:27|問候|Hello World"]

由於 key=日期時間放在最前面, 故會先以日期時間排序, 先呼叫 sort() 再呼叫 reverse() 結果最近的時間會排前面. 完整的網頁與程式碼如下 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-icon="recycle" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增事項</a>
        <table>
          <thead>
            <tr>
              <th>事項</th>
              <th>類別</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">事項 : </label>
            <textarea id="add_memo"></textarea>
            <label for="add_category">類別 : </label>
            <input type="text" id="add_category">
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">事項 : </label>
            <textarea id="edit_memo_value"></textarea>
            <label for="edit_memo_category">類別 : </label>
            <input type="text" id="edit_memo_category">
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "|" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var cv=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        var arr=cv.split("|");
        var c=arr[0];
        var v=arr[1];
        $("#edit_memo_value").val(v);
        $("#edit_memo_category").val(c);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        var item=[];
        if (memo_arr.length == 0){
          item.push("<tr><td colspan='3' style='text-align:center;'>" + 
                    "資料庫內容為空, 請新增事項</td></tr>");
          }
        else {
          for (var i=0; i<memo_arr.length; i++){
            var arr=memo_arr[i].split("|");
            var k=arr[0];
            var c=arr[1];
            var v=arr[2];
            item.push("<tr><td>" + v + "</td><td>" + c + "</td>" + 
                      "<td style='text-align: center;'>" + 
                      "<a href='#' data-role='button' " + 
                      "data-icon='edit' data-iconpos='notext' " + 
                      "data-mini='true' data-inline='true' " + 
                      "onclick='set_edit_memo(" + '"' + k + '"' +
                      ")'>編輯</a></td></tr>");            
            }
          }
        $(item.join("")).appendTo("#memo_list").trigger("create");
        }
      $(document).one("pageinit", function(){
        render_memo_list();
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v=$("#add_category").val() + "|" + $("#add_memo").val();
          localStorage.setItem(k, v); 
          $("#add_memo").val("");
          $("#add_category").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v=$("#edit_memo_category").val() + "|" + 
                $("#edit_memo_value").val();
          localStorage.setItem(k, v);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

程式部分與前一篇的主要差別是新增了記事的類別欄位與其配套處理, 主要就是將分類冠在記事內容前面用 "|" 串接當作 value 存入 localStorage, 顯示或編輯時用 split("|") 將 key (日期時間), 分類與記事內容拆出來. 另外也改寫了 render_memo_list() 等函式, 簡化了資料庫為空時的處理方式, 結果如下 : 




 


測試 OK 後用線上服務 appgeyser 轉成 apk 檔, 可從 GitHub 下載 : 





不過這個第二版是利用 "|" 字元來將分類欄位與記事欄位串起來當作 value 存入 localStorage, 雖然 "|" 字元很少人會用到, 但萬一用在分類欄位上就會拆解時出現分類被裁減掉 "|" 字元後面的部分 (被歸入記事欄位), 所以並不完美. 

較安全的處理方式是將分類與記事兩個欄位存入物件中, 然後用 JSON.stringify() 方法轉成字串, 取出時再呼叫 JSON.parse() 方法還原成物件. 分類欄位 key 可用 category, 記事欄位 key 可用 memo,  例如 : 

{catelogue: "問候", memo: "Hello World"}
{catelogue: "問候", memo: "你是在說哈囉嗎?"}

將上面第二版程式碼中的欄位合併與拆解方式修改為物件存取方式, 成為如下之第三版 : 



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-icon="recycle" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增事項</a>
        <table>
          <thead>
            <tr>
              <th>事項</th>
              <th>類別</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">事項 : </label>
            <textarea id="add_memo"></textarea>
            <label for="add_category">類別 : </label>
            <input type="text" id="add_category">
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">事項 : </label>
            <textarea id="edit_memo_value"></textarea>
            <label for="edit_memo_category">類別 : </label>
            <input type="text" id="edit_memo_category">
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "|" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var cv=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        var obj=JSON.parse(cv);   
        var c=obj.category;
        var v=obj.memo;
        $("#edit_memo_value").val(v);
        $("#edit_memo_category").val(c);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        var item=[];
        if (memo_arr.length == 0){
          item.push("<tr><td colspan='3' style='text-align:center;'>" + 
                    "資料庫內容為空, 請新增事項</td></tr>");
          }
        else {
          for (var i=0; i<memo_arr.length; i++){
            var arr=memo_arr[i].split("|");
            var k=arr[0];
            var obj=JSON.parse(arr[1]);
            var c=obj.category;
            var v=obj.memo;
            item.push("<tr><td>" + v + "</td><td>" + c + "</td>" + 
                      "<td style='text-align: center;'>" + 
                      "<a href='#' data-role='button' " + 
                      "data-icon='edit' data-iconpos='notext' " + 
                      "data-mini='true' data-inline='true' " + 
                      "onclick='set_edit_memo(" + '"' + k + '"' +
                      ")'>編輯</a></td></tr>");            
            }
          }
        $(item.join("")).appendTo("#memo_list").trigger("create");
        }
      $(document).one("pageinit", function(){
        render_memo_list();
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v={category:$("#add_category").val(),
                 memo:$("#add_memo").val()};
          localStorage.setItem(k, JSON.stringify(v)); 
          $("#add_memo").val("");
          $("#add_category").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v={category:$("#edit_memo_category").val(),
                 memo:$("#edit_memo_value").val()};
          localStorage.setItem(k, JSON.stringify(v)); 
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

藍色部分是與上面第 2 版不同之處, 差別只在於可防止使用者的記事類別含有 "|" 字元而已, 但缺點是會占較大儲存空間, 因為每一筆資料都會重複 catelogue 與 memo 這兩個 key, 頗為冗餘, 所以 我覺得其實用第 2 版就可以啦. 

# 下載我的備忘錄 v3 apk 檔 (使用字串化物件) 




2022-01-31 補充 :

今天發現當事項欄位很長時, 類別欄位嘿被擠壓也跳行了 : 




可以用 CSS 的 td:nth-child(2) 選取第二個 td 後設定 "white-space: nowrap;" 屬性即可, 參考 :


所以我又將上面的 v2, v3 程式碼中的樣式添加了如下設定 : 

      table tr td:nth-child(2) {
          white-space: nowrap;
          }

這樣就可以解決此問題了. 

2022年1月25日 星期二

jQuery Mobile 學習筆記 (二十二) : 用 Web Storage 製作備忘錄 App (上)

上週因為要因應 IE 走入歷史須將輔助工作自動化的網頁程式改版, 在翻閱 HTML5 書籍尋求 ActiveX 的替代技術時看到 localStorage 物件, 覺得是可行方案之一, 於是便做了一些測試, 覺得雖然僅有約 5MB 容量, 倒也足以應付工作需求. 

做完測試後剛好想起同事去年底的囑託, 希望我可以將我十幾年前幫單位寫的工作日誌系統原始碼給他, 因為覺得很好用想要做為自己的私人資訊系統, 但我說那系統太大也太舊了不合用, 現在是行動通訊時代人手一機, 有空我寫個微小版的個人工作日誌系統給他. 既然應承了, 剛好又學完 localStorage, 不如打鐵趁熱, 結合 jQuery Mobile 與 localStorage 寫個網頁應用程式吧! 還可以用線上服務轉成 App. 

同事的要求很簡單, 就是可以新增, 編輯, 以及刪除記事, 相當於以前工作日誌系統裡面的 CRUD 最基本功能, 但去掉可追蹤工作流程的堆疊式文章架構, 當然也不需要稽核要看的主管閱覽等管理功能, 純粹就是表格式的記事本. 希望保留記事類別欄位, 還可以搜尋云云, 但這對 localStoage 來說有點過分了, 等後續我測試完 indexedDB 再看看唄! 

本篇先來完成備忘錄的 CRUD 基本功能就好, 順便複習一下 jQuery Mobile 的用法. 比起現在紅到發紫的 React 等, jQuery Mobile 算是上個世紀的技術了, 雖然老狗玩不出新把戲, 但老把戲還是能用的. 

關於 jQuery Mobile 參考 : 


我的規劃是使用單頁應用架構 (SPA), 所以 CRUD 似乎需要四個 page, 但其實 Delete 動作是透過函式完成, 刪除完就跳回備忘錄列表頁面, 所以其實只需要三個 Page (C, R, 與 U), 另外加上兩個 Dialog 對話框頁面 (負責確認 removeItem 與 clear). 

在這三天測試過程中才發現我對 jQuery Mobile 的操控還是有許多盲點, 例如用程式碼 $.mobile.changePage() 換頁, 以及用 trigger("create") 來動態新增 DOM 元素, 這是這幾天才學會的新用法. 

記事列表我使用 TABLE 表格來呈現, 原本想用 ListView, 但發現要放編輯 icon 有困難, 所以還是用表格唄, 這也跟之前的工作日誌系統較像. 在 jQuery 中使用表格的方法參考 :


不過本篇用的是下列這篇文章中所使用的特殊樣式表格, 它的功能較簡單, 就是單純顯示表列資料而已, 沒有分頁功能, 下回再試試有分頁的表格 :



一. 製作 jQuery Mobile 網頁程式 : 

儲存在 localStorage 中的備忘記事以格式為 YYYY-MM-DDTHH:mm:SS 的日期時間為鍵, 以記事內容為值, 例如 :

{"2022-01-24T08:45:27": "Hello World"}
{"2022-01-24T09:31:27": "你是在說哈囉嗎?"}

其中 key 是用呼叫 new Date() 得到的日期時間物件傳給 JSON.stringify() 轉成字串後再取出前 19 個字元而得. 省略秒後面的資訊, 畢竟一般不會在同一秒內輸入兩筆備忘事項. 

底下先列出整個 SPA 專案的完整原始碼, 然後再進行說明 :



<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
    <meta http-equiv="cache-control" content="no-cache">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
    <script src="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
    <link href="https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" rel="stylesheet">
    <style>
      table {
          color: black;
          background: #fff;
          border: 1px solid #b4b4b4;
          padding: 0;
          margin-top:10px;
          width: 100%;
          -webkit-border-radius: 8px;
          }
      table tr td {
          color: #666;
          border-bottom: 1px solid #b4b4b4;
          border-right: 1px solid #b4b4b4;
          padding: 5px 5px 5px 5px;
          background-image: -webkit-linear-gradient(top, #fdfdfd, #eee);
          }
      table tr:first-child td {
          border-top: 1px solid #b4b4b4;
          }
      table tr td:last-child {
          border-right: none;
          }
      table tr:last-child td {
          border-bottom: none;
          }
    </style>
  </head>
  <body>
    <!-- 第一頁頁面 (顯示備忘事項列表) -->
    <section data-role="page" id="page1">
      <header data-role="header" data-position="fixed">
        <h1>我的備忘錄</h1>
        <a href="#confirm_clear_dialog" id="clear_memo_btn" class="ui-btn-right" data-rel="dialog">清除</a>
      </header>
      <article data-role="content">
        <a href="#page2" data-role="button" data-icon="plus">新增</a>
        <table>
          <thead>
            <tr>
              <th>備忘事項</th>
              <th>編輯</th>
            </tr>
          </thead>
          <tbody id="memo_list">
          </tbody>
        </table>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第二頁頁面 (新增備忘事項) -->
    <section data-role="page" id="page2">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>新增備忘事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">
            <label for="add_memo">備忘事項 : </label>
            <textarea id="add_memo"></textarea>
          </div>
          <input type="button" id="add_memo_btn" value="儲存" data-icon="check">
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 第三頁頁面 (編輯備忘事項) -->
    <section data-role="page" id="page3">
      <header data-role="header" data-position="fixed" data-add-back-btn="true" data-back-btn-text="返回">
        <h1>編輯備忘事項</h1>
      </header>
      <article data-role="content">
        <form data-ajax="false">
          <div data-role="fieldcontain">          
            <input type="hidden" id="edit_memo_key">
            <label for="edit_memo_value">備忘事項 : </label>
            <textarea id="edit_memo_value"></textarea>
          </div>
          <div data-role="controlgroup" data-type="horizontal">
            <a href="#" data-role='button' data-rel="back" data-icon="back" data-inline="true">取消</a>
            <a href="#confirm_remove_dialog" id="remove_memo_btn" data-role='button' data-rel="dialog" data-icon="delete" data-inline="true">刪除</a>
            <a href="#" id="update_memo_btn" data-role='button' data-icon="check" data-inline="true">更新</a>
          </div>
        </form>
      </article>
      <footer data-role="footer" data-position="fixed">
        <h3>© 2022 LF.Studio</h3>
      </footer>
    </section>
    <!-- 對話框頁面 1 (清除全部資料) -->
    <section data-role="dialog" id="confirm_clear_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要清除全部資料嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back'  data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_clear_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <!-- 對話框頁面 2 (刪除單筆資料) -->
    <section data-role="dialog" id="confirm_remove_dialog" data-overlay-theme="b">
      <header data-role="header">
        <h1>請確認</h1>
      </header>
      <article data-role="content">
        <p>確定要刪除這筆備忘事項嗎?</p>
        <div data-role="controlgroup" data-type="horizontal">
          <a href="#" data-role='button' data-icon='back' data-inline="true" data-rel="back">取消</a>
          <a href="#" id="confirm_remove_btn" data-role='button' data-icon='check'  data-inline="true">確定</a>
        </div>
      </article>
    </section>
    <script>
      function sort_local_storage(){
        var arr=[];
        for (var i=0; i<localStorage.length; i++){
          var k=localStorage.key(i);
          var v=localStorage.getItem(k); 
          arr.push(k + "+" + v);
          }
        arr.sort();
        arr.reverse();
        return arr;
        }
      function set_edit_memo(k){
        var v=localStorage.getItem(k);
        $("#edit_memo_key").val(k);
        $("#edit_memo_value").val(v);
        render_memo_list();
        $.mobile.changePage($("#page3"));
        }
      function render_memo_list(){
        $("#memo_list").empty();
        var memo_arr=sort_local_storage();
        for (var i=0; i<memo_arr.length; i++){
          var arr=memo_arr[i].split("+");
          var k=arr[0];
          var v=arr[1];
          var item="<tr><td>" + v + "</td>" +
                   "<td style='text-align: center;'>" + 
                   "<a href='#' data-role='button' " + 
                   "data-icon='edit' data-iconpos='notext' " + 
                   "data-mini='true' data-inline='true' " + 
                   "onclick='set_edit_memo(" + '"' + k + '"' +
                   ")'>編輯</a></td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          }
        }
      $(document).one("pageinit", function(){
        if (localStorage.length == 0){
          var item="<tr><td colspan=2>無備忘事項</td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          }
        else {render_memo_list();}
        $("#add_memo_btn").on("click", function(e) {
          var k=JSON.stringify(new Date()).substr(1,19);
          var v=$("#add_memo").val();
          localStorage.setItem(k, v); 
          $("#add_memo").val("");
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#confirm_clear_btn").on("click", function(e) {
          localStorage.clear();
          $("#memo_list").empty();
          var item="<tr><td colspan=2>無備忘事項</td></tr>";
          $(item).appendTo("#memo_list").trigger("create");
          $.mobile.changePage($("#page1"));
          });
        $("#edit_memo_btn").on("click", function(e) {
          $.mobile.changePage($("#page3"));
          });
        $("#confirm_remove_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          localStorage.removeItem(k);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        $("#update_memo_btn").on("click", function(e) {
          var k=$("#edit_memo_key").val();
          var v=$("#edit_memo_value").val();
          localStorage.setItem(k, v);
          render_memo_list();
          $.mobile.changePage($("#page1"));
          });
        }); 
    </script>
  </body>
</html>

上面的網頁程式原始碼中有三個 page 與兩個 dialog, 功能說明如下 : 
  • page1 :
    以表格顯示備忘記事列表 (上方有新增按鈕至 page2, 表格第二欄有編輯鈕至 page3.
  • page2 : 
    新增記事表單, 按 "儲存" 鈕將記事存入 localStorage 後自動返回 page1 記事列表.
  • page3 :
    編輯記事表單, 更改記事內容後按 "確定" 更新 localStorage 後自動返回 page1 記事列表; 按 "刪除" 則從 localStorage 刪除該資料後自動返回 page1 記事列表.
  • dialog1 : 
    詢問是否清除全部 localStorage 資料的對話框, 按 "確定" 清除 localStorage 全部資料後自動返回 page1 記事列表.
  • dialog2 :
    詢問是否刪除 localStorage 中指定 key 資料的對話框, 按 "確定" 刪除 localStorage 該筆資料後自動返回 page1 記事列表 
Javascript 程式碼部分有三個函式 :
  • sort_local_storage() :
    此函式用來將 localStorage 內儲存的資料進行排序, 由於物件的內容 (鍵值對) 是無序的, 因此沒有 sort() 方法可用來排序, 此函式的作法是用迴圈走訪 localStorage 儲存區的全部資料, 然後將 key 與 value 用 "+" 串接後存入空陣列, 再用陣列的 sort() 方法做升序排序, 最後呼叫 reverse() 做降序排序, 再將此陣列傳回. 呼叫者走訪此陣列, 將元素以 "+" 拆分即可得到排序後的 key 與 value 了, 參考 :
    HTML5 local storage sort
  • set_edit_memo(k) : 
    此函式是使用者在按下記事列表中的編輯鈕後被呼叫, 目的是將傳進來的鍵 (k) 與查得之值設定到 page2 的編輯欄位中 (key 用隱藏欄位儲存), 以便稍後切換到 page2 時會顯示被編輯的記事內容. 
  • render_memo_list() : 
    此函式會先呼叫 sort_local_storage() 取得排序後的資料陣列, 然後在走訪此陣列的迴圈中以 "+" 為界拆解出 key 與 value, 用來產生表格內容 (tbody 元素的子元素 tr 與 td), 然後呼叫 appendTo() 方法將這些內容掛到 tbody 底下, 接著呼叫 trigger("create") 方法更新 DOM 樹. 每次做新增與刪除這兩種改變儲存區內容的動作後都會先呼叫此函式來更新內容, 再切換到 page1 去. 
其他在 pageinit 事件內部的程式碼都是用 jQuery 來處理元件觸發的事件. 結果如下 : 

剛使用此網頁 App 時因為 localStorage 為空, 故記事列表亦為空 :




按 "新增" 鈕會切換到記事輸入頁 (page2), 填寫後按 "確定" 會將此資料存入 localStorage 儲存區, 並轉回記事列表頁 (page1) : 





繼續輸入第二筆備忘記事 : 





按下第二欄的編輯按鈕會切換到 page3, 可對記事內容進行修改, 按 "確定" 會更新 localStorage 內的資料, 然後轉回記事列表頁 (page1) : 





若在編輯頁面按下 "刪除" 鈕, 則會跳出確認對話框 (dialog2), 按 "確定" 會從 localStorage 內刪除該筆資料, 然後轉到記事列表 (page1) 頁面 : 





按頁面右上角的 "清除" 鈕也會跳出對話框 (dialog1), 詢問是否要清除全部 localStorage 資料, 按 "確定" 會刪除全部資料後跳轉 page1, 這時就會回復到空的記事列表了 : 




感覺似乎與後端資料庫的 CRUD 操作一模一樣, 但其實是在存取本機客戶端的 localStorage 而已, 資料並沒有透過網路傳輸, 亦即完全是是離線作業, 非常適合用來儲存個人資料. 


二. 將 jQuery Mobile 行動網頁程式轉成 Android App (apk 檔) : 

完成 jQuery Mobile 網頁應用程式設計後即可利用 appsgeyser 這個免費的線上轉換服務將行動網頁 App 轉成 Android 的 App, 其網址為 :


作法參考 :


為了讓 appgeyser 能讀取網頁原始檔, 要先將上面的 jQuery Mobile 網頁放到一個伺服器上, 最方便的是靜態網頁伺服器 GitHub, 參考 : 


然後還要幫 App 準備一個 icon, 我搜尋 "free memo icon" 找到一個適合的免費圖檔來修改, appgeyser 要求圖檔大小不可超過 512*512. 準備好就可以按下 "CREATE APP NOW" 鈕了 : 




在 "Website URL" 欄填入行動網頁 App 的網址後按 "NEXT" : 




在 "APP NAME" 欄填入 App 名稱 (中英文皆可, 不要太長), 按 "NEXT" :




點選 "Custom icon", 再按右邊的 "Upload 512*512" 上傳圖檔 : 



在彈出頁面中移動底下的滑桿選擇範圍, 然後按底下的 "Crop" 鈕裁切並返回設定頁面 : 




按 "NEXT" 鈕 :




按 "CREATE" 鈕進行 Apk 檔製作, 這時會要求註冊帳號 (過程略) : 




Apk 檔建立完成顯示如下頁面, 按最上方的下載按鈕即可下載 apk 檔 : 

 


也可以用手機掃 QR code 下載或郵寄到註冊信箱 : 






下載後開啟 apk 檔安裝 : 






彈出安全性提示, 選擇 "仍要安裝" 與 "不傳送" : 






完成安裝後桌面就出現此 App 了, 點選執行結果與上面行動網頁 App 完全一樣 : 





OK, 總算完工啦!

參考資料 : 


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 了.

參考 :