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 了. 
  • 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 廢止, 改為使用 indexedDB 資料庫. 

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


本篇主要是測試  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 來儲存測試結果, 有支援就存入陣列中. 在 Chrome/Edge 測試結果如下 : 


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


Android 上的 Chrome 也不支援 Web SQL 了, 結果如下 : 



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

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

localStorage 物件具有一個固定屬性 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 物件方法新增一個鍵值對來記錄網頁被瀏覽次數 :



<!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  都開一個 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() 轉回物件). 

好站: 陳蘊儂老師的 Youtube 頻道 (DL & NLP)

早上聽了一堂自然語言處理的線上講座, 講者是鼎鼎有名的台大資工系陳蘊儂老師, 她曾是台大最年輕的教授, 也是自然語言處理 (NLP) 與深度學習 (DL) 的專家, 我因調整學習順序而中斷 NLP 學習有一段時間了, 剛好可以複習一下概念. 我在簡報中看到所附的 Youtube 頻道網址, 按圖索驥發現原來這裡有這麼多教學資料 : 





據說這些都是老師錄下課堂教學實況後剪輯上傳的, 方便自學的人進修 (沒機會上台大還是可以學到東西) 與翹課的人補聽, 非常有教學熱忱, 因為做過後製就知道這真的非常花時間, 以後吃午飯時間不看網路小說, 也不看 LineTV 了, 好好施展吸星大法把老師的功力給吸過來. 

2022年1月18日 星期二

好書 : 勝率近 100% 的可轉債存股術

在讀到此書之前我對可轉債毫無概念, 上個月在圖書館新書架上看到後一時好奇借回來看, 覺得此投資工具挺有吸引力的, 因為它具有進可攻退可守特性. 此書作者蕭啟斌專研可轉債, 他創辦了目前國內唯一討論可轉債的 "悠債網", 註冊會員可瀏覽查閱可轉債表與討論文章, 額外功能需付費購買模組. 


Source : 博客來


以下是部分讀書摘要 :
  1. 由企業發行擔保的債券稱為公司債, 如果此債券可以轉換成該公司股票就稱為可轉換公司債 (Convertible Bond, CB), 它是由一般公司債加上可轉換為股票之選擇權組合而成, 即 CB=Bond + Option. 財務工程甚至從 CB 衍生出可轉債資產交換選擇權 (Convetible Bond Asset Selection, CBAS), 即散戶可向券商借錢買可轉債的特殊交易. 
  2. 持有一間公司的可轉債, 當行情不好時可選擇繼續持有領利息; 當行情來時股價大漲就可以債轉股成為股東, 形成進可攻退可守的良好投資循環 (漲時領價差, 跌時領債息). 可轉債賣回價保底的特性宛如先建好堅硬的地基, 行情來時便可蓋成摩天大樓. 
  3. 可轉債是資本市場中極受歡迎的投資工具之一, 連巴菲特也是愛好者, 為什麼? 因為巴菲特投資守則第一條是不要賠錢, 而可轉債的保底功能恰好符合此原則. 
  4. 可轉債基本上可分為有擔保與無擔保兩種, 有擔保較有保障, 無擔保可轉債可用 TCRI 指標來衡量違約風險, TCRI 1~3 為低風險, 4~6 為中風險, 7~9 為高風險. 通常財務健全信用良好的公司如鴻海等大都發行無擔保可轉債, 財務有疑慮者才會提供擔保品. 
  5. 可轉債的重要術語 :
    (1). 發行價 : 一般是每股 100 元, 每張可轉債 1000 股, 即一張面額為 10 萬元.
    (2). 轉換價 : 可轉債每股轉成現股的價格=參考價 x 轉換溢價率 
    (3). 轉換比例 : 100 / 轉換價, 即一張可轉債能換得的股票張數.
    (4). 轉換溢價率 : 從參考價計算轉換價時的乘數. 
    (5). 理論價 : 股價 x 轉換比例
    (6). 到期日 : 發行結束日期, 公司買回債權. 
    (7). 賣回日 : 投資人可提前解約賣回債權之日期
    (8). TCRI : 台灣企業信用風險指標 1 (低風險)~9 (高風險)
    (9). 轉股持有成本 : 買進成本/最新轉換股數
  6. 大部分可轉債發行期限為 3 年或 5 年, 票面利率一般為 0, 但有些公司到期回收時會加價償還 (相當於變相配息), 回收價格越高越能支撐可轉債的市價. 
  7. 公司規模與可轉債發行總額正相關, 發行總額太小會影響流動性; 發行量太大不易有行情. 
  8. 公司在訂定轉換價時會先訂一個基準日, 然後取基準日前 1, 3, 或 5 個營業日之普通股收盤價的算術平均數作為基準參考價, 從中選取一個乘以轉換溢價率而得到轉換價. 
  9. 除權息與股東會時會停止可轉債轉換, 一家有配股配息的公司每年大約會有 3 個月停止轉換, 這段期間可轉債市價會比較低, 也有可能折價, 這時若股價上漲可融券放空來鎖住價差, 等到停止轉換期間結束, 再轉股進行券償. 注意, 轉股須臨櫃辦理, 目前僅國泰證券 e 櫃台提供線上轉換, 但時間會比臨櫃慢一天.  
  10. 轉換價訂得越低, 能換到的股票越多, 股本也會越大, 使得盈餘 (EPS) 被稀釋的情況越嚴重. 轉換價不會離訂價時的股價太遠, 但轉換價訂出來後並非固定不變, 會隨股價而波動, 還會因為下列情況進行調整 :
    (1). 因合併/分割/現增/發新股使得流通股數增加時
    (2). 發放股息
    (3). 私募
    (4). 減資
    只要股本擴大 (例如現金增資或發放股息) 一定會使盈餘被稀釋, 所以公司會調降轉換價以保護可轉債投資人權益, 這種調降轉換價的作為又稱為反稀釋條款. 雖然可轉債的投資人只是借錢給公司並非股東, 無法參與盈餘分配, 與除息無關, 但反稀釋條款透過調降轉換價, 卻使得可轉債投資人因為可換得更多股數而變相領到股息, 所以持有可轉債其實也是在存股
  11. 轉換溢價率越高, 轉換價也就越高, EPS 被稀釋的程度越小, 這對原始股東越有利, 因為轉換價過低會使大量可轉債轉成股票, 股本膨脹侵害原始股東權益. 基本面越好, 對股東權益越保護的公司, 其可轉債之轉換溢價率通常都較高 (也代表公司對未來股價很有信心), 提高轉換溢價率就是避免轉換價過低而傷害股東的手段之一.  一般而言, 溢價率低於 105% 為低溢價; 高於 110% 為高溢價. 風險越高, 溢價率就越低, 股本越大風險越小, 獲利越穩定的大公司越不願意將轉換價壓低, 所以都會設定較高的溢價率.
  12. 賣回權是可轉債投資人的提前解約權利, 債權人可在公開說明書約定的日期 (賣回日) 決定執行或不執行此權利, 公司會以賣回價買回債權 (又稱最近提前償還日). 賣回權是保障投資人的機制, 離賣回日期越遠風險越高. 賣回次數可能 1 次或 2 次, 但也可能完全沒有 (即與到期日同一天), 賣回次數越多越安全. 執行賣回與否的判斷原則是, 當可轉債的市價大於賣回價時, 執行賣回較不利, 故不應執行賣回. 注意, 執行賣回必須臨櫃辦理
  13. 公司發行可轉債之後在下面兩個情況下可提前解約 (強制回收), 也就是合約中所謂的贖回權, 這是與賣回權相對的提前解約機制 :
    (1). 轉換標的股價連續 30 個營業日超過轉換價 130%
    (2). 在外流通的可轉債低於 10% 
    注意, 贖回權乃是公司的權利, 公司可決定執行與否. 
  14. 可轉債市價與理論價不會相等, 理論價會受到股價波動影響而變動, 但可轉債市價受到賣回價的制約與支撐, 不會隨著股價下滑而下滑. 當可轉債市價高於理論價時稱為溢價, 低於理論價時稱為折價. 可轉債折價時會出現套利空間, 投資人可買進可轉債並放空其現股來套利 (買可轉債 -> 賣現股 -> 轉股賺價差). 可轉債若長期處於折價狀態, 可能的原因包括處於停止轉換期, 或現股缺劵無法融券. 但要注意, 雖然停止轉換期間容易出現折價可套利空間, 但有可能看得到吃不到, 如果是用融券放空, 高額的費用可能得不償失. 
  15. 雖然可轉債違約率極低, 可從下列訊號判斷是否可能違約 :
    (1). 股價跌, 融券增
    (2). 內部人出脫持股
    (3). 強制回補前融券仍暴增

2022年1月16日 星期日

2022 年第 3 周記事

週五晚上姊姊回到高雄, 因為週日是她生日, 要請她去西堤吃飯, 菁菁還在網路訂了一顆大壽桃 (綠豆餡), 三顆小壽桃 (綠豆餡), 週五放學叫我彎到大順二路去買了一組蠟燭, 今天在西堤打開, 原來是展開後會播生日快樂歌的音樂的特製蠟燭, 也只有她才會想出這麼多點子, 是天生的康樂股長. 

因為要趕上五點半吃飯, 所以我從中午準備午飯開始就同時準備爸下周的伙食便當, 螞蟻上樹 *4, 咖哩飯 *5, 豬腳飯 *2, 水餃 *1, 總共 12 份. 另外還要煮一鍋蔬菜湯, 滷豬腳, 晚餐以及菁菁的紅豆湯, 一直忙到三點半才結束, 然後趕忙去把已有點扁的大灰熊右前輪打滿氣, 擦車子, 還要洗澡, 原訂四點出發延到四點半, 前往高雄途中還要經過岳母的農舍拿託運的青菜, 回到高雄已五點半, 到西堤已快六點矣. 今天除了起早寫完一篇讀書摘要外, 可說一整天都在做料理. 

11 月底重的玉米已經可以收成了, 下午擦完車子去菜園採了約 10 支, 都帶來高雄用來煮蔬菜湯用. 從上週開始每天晚上都熬湯 (南瓜濃湯, 綜合蔬菜排骨湯, 猴頭菇湯等) 放在悶燒鍋, 作為菁菁的早餐與我們的晚餐. 我現在只有中午在公司訂便當, 已經很久沒買晚餐的便當了, 因為吃太多米飯易胖. 




我有在想於玉米旁空地搭建一個小型網室, 這樣以後就可以在裡面種高麗菜, 花椰菜等容易有蟲害的青菜了. 

好書 : 打破選股迷思的獲利心法

此書為美國紐約大學教授 Aswath Damoodaran 所著, 他同時也是華爾街投銀的估值專家. 書中詳細解說了 13 種常見選股方法的優劣點, 並以歷史經驗作為佐證 (故篇幅頗長), 目的是要向讀者呈現每一種投資策略的完整面貌, 以便能自行判斷哪些方法有效, 運用主動管理策略創造報酬. 作者想要強調的是, 身為投資人應該做哪些事才能提高勝率. 


Source : 博客來


最後一章作者提出投資獲利的十大心法頗值得借鏡, 摘要如下 : 
  1. 越是改頭換面, 越是萬變不離其宗 :
    投資顧問每隔一段時間就會創造出華麗的名詞來包裝行銷他們新瓶裝舊酒的投資策略, 越是內容複雜, 越是名稱花俏的都要特別當心. 
  2. 想要保證獲利的話別投資股票 :
    股票具有波動性, 沒有一種策略能保證成功. 
  3. 沒有不勞而獲的事情 :
    低風險高報酬是不可能的事情. 如果你對虧損的風險會感到不安, 最好避開所有高風險策略, 不論他的前景看起來多美好. 
  4. 記住基本面 : 
    每次牛市過後投資人都會發現一件事, 那就是基本面 (現金流, 預期成長率, 風險等) 很重要, 公司必須有賺錢, 獲利成長非常重要. 企業的價值永遠與利用資產創造現金流的能力息息相關, 但股市熱絡時投資人常忘記決定價值的是基本面, 忽略基本面後果自負.
  5. 大多數看起來很便宜的股票背後都有原因 :
    很多看起來很便宜的股票 (低本益比或股價淨值比) 背後只少都有一個理由, 例如獲利能力差, 波動風險大, 或是成長前景乏善可陳等. 
  6. 萬物皆有價 : 
    投資人通常會強化自己投資的股票有那些優點, 例如響亮的品牌, 看好的獲利成長, 以及擁有絕佳競爭力的產品等. 但市場通常都已經做了該做的事, 所以這種好公司本益比都很高. 投資人該注意的是, 一個好品牌所定出的價格是過高還是過低, 好公司不一定是好的投資標的
  7. 數字有可能騙人 :
    雖然透過大量長期數據來檢驗策略的有效性能讓投資人心安, 但數字常讓人一葉遮目, 投資人永遠不要忘記一件事 : 過去的績效不保證將來的獲利, 市場一直在變化, 風險永遠都存在, 即使回測數據非常亮眼.
  8. 尊重市場 :
    每種投資策略都是與市場對賭, 賭市場錯了而你對了, 但更重要的是你在賭市場有沒有發現自己的錯誤而進行修正, 進而推高被低估的股價, 這樣你與市場的賭局才算是獲得勝利. 你認為市場錯了也許是正確的, 但在下注前先對市場寄予適當的敬意會是明智之舉. 雖然市場在定價時確實會犯錯, 但一般而言, 市場對的時候比錯的時候還多. 所以當你分析之後發現市場可能錯了, 出現了一個絕佳的投資機會時, 你要重新審視分析過程, 因為有可能是你遺漏了某些幽微卻重要的因素, 只有在確認已排除定價錯誤的所有可能原因後, 再考慮下注.
  9. 了解自己 :
    如果與投資人的偏好或投資屬性不匹配, 一個設計得再好的投資策略也無法發揮作用. 因此投資人在選擇投資策略時要先進行兩個檢驗 : 
    (1). 適合度檢驗 : 如果價格波動讓你坐立難安, 那就應該放棄此策略. 
    (2). 耐性檢驗 : 如果你常微調長期策略的投資組合, 那表示你適合做波段. 
    沒有一套適合所有投資人的策略, 選擇了不適合的策略通常不會有好果子吃
  10. 運氣比技能重要 (至少短期這樣) :
    投資組合能否賺錢, 你能控制的其實只有一部分而已, 運氣才是主宰. 努力, 耐性, 以及做好準備不一定能帶來回報. 雖然聽起來有點讓人喪氣, 但金融市場確實就是這樣. 但隨著投資時間變長, 適合的投資策略會讓運氣的因素淡化, 真正的技能才會顯露出來. 投資人要保持一個態度, 投資獲利並不代表你很有能力, 永遠不要排除好運. 雖然好運無法創造, 但你要做好準備, 這樣好運上門時才能善加利用.     
選股真的是需要時間與耗費心力, 所以綠角大師在推薦序的結尾提到 : 如果你覺得這些選股方法太累人, 又不能保證投資獲利, 那就考慮指數化投資吧! (怎麼感覺像是一覺醒來回到解放前), 會讀這本書的人, 應該就是不滿足於被動投資的收益率吧? 我覺得比較穩健的做法是部分資金做指數化投資, 其餘做主動選股, 比率就要看年齡等投資屬性自行拿捏了.  

2022年1月14日 星期五

Python 學習筆記 : 使用雲端 NoSQL 資料庫 MongoDB Atlas (一)

測試過 Google 的 Firebase 後, 我想試試看另一款 NoSQL 資料庫 MongoDB, 它是一種文件式 (document-oriented)  NoSQL 資料庫, 適合用來儲存像 HTML 那樣的非結構式資料 (Firebase 則是 key-value 鍵值對類型的 NoSQL 資料庫), 關於 NoSQL 資料庫得分類與說明參考下列文章 : 


傳統的關聯式資料庫需先定義資料的欄位, 資料以列記錄為單位儲存, 但 MongoDB 則是以文件 (document) 為單位儲存, 所謂的 document 其實就是 Python 字典那樣的鍵值對 (相當於一列紀錄), 多個 document 集合在一起稱為 collection (相當於一個資料表, 裡面有很多列).

MongoDB 支援 Windows, Linux, 與 macOS 等作業系統, 它曾經內建於主要的 Linux 發布版本, 但後來授權條款變更而被移除. MongoDB 支援各種程式語言的介接例如 Python, Java 等. 參考 : 


iT 邦幫忙有一系列介紹 MongoDB 用法的好文章 (我主要關心的是 CRUD 操作), 參考 :


因我不想在筆電內再安裝一套資料庫系統, 所以選擇註冊線上的 MongoDB 資料庫來測試, 它其實是架在 AWS, GCP, 以及 Azure 三大雲端服務上, 因此品質非常穩定, 且提供 512MB 的免費資料庫空間, 這對於小專案而言是夠用的, 對於需求更多的專案則可以考慮其付費方案. 


一. 申請線上 MongoDB 帳號 : 

首先到 MongoDB 官網按右上角的 "Try Free" 鈕 :





然後填妥個人資料與 E-mail 信箱與設定登入密碼後按 "Create account" 鈕建立帳號 :




然後在兩小時內到信箱收信, 按下信中的 "Verify Email" 確認鈕即可 : 




接著會跳轉到下列諮詢頁面, 詢問要用 MongoDB 做甚麼用途以及所使用的程式語言 : 




接著是選擇免費還是計費方案, 選最右邊的 "Shared" 的 Free 方案 : 




然後選最右邊的 "Free Shared" 按底下的 "Ctreate Cluster" 建立伺服器集群 : 




接下來選擇雲端服務提供者 (我選 AWS) 後按右下角的 "Create Cluster" : 




最後顯示如下設定結果 : 




這樣就算是把帳號搞定了. 


二. 設定資料庫使用者與允入 IP : 

上面將帳號設定成功後會進入 MongoDB 主控台頁面, 此處要做兩個存取權限設定 :
  1. 資料庫存取 (Database Access)  : 存取帳密或權杖設定
  2. 網路設定 (Network Access) : 允入 IP 管控
在左邊 "Security" 項目下點 "Quick Start" 連結, 右邊第 1 項設定是連線認證方式, 預設是用帳號密碼進行存取, 往下拉就會看到帳密設定欄  : 




設定好按右邊的 "Create User" 鈕新增使用者 : 





然後往下拉到第 2 項的存取 IP 設定, 如果有固定 IP 就將其輸入到 IP Address 欄位中, 再按 "Add Entry" 鈕, 就會在下面顯示允入之 IP 名單. 但因為大部分使用者都是浮動 IP, 此情況要輸入 0.0.0.0/0, 表示可從任何 IP 連線資料庫 : 





也可以分別點 "Quick Start" 底下的  "Database Access" 與 "Network Access" 連結進去分別設定, 設定的結果也是顯示在這兩個頁面上 :





這樣就完成所有設定了.