フロントjsでも使えそうなデータ保存方法

## はじめに

趣味でプログラミングをする際にフロントjsを使うことが多いのですが、
フロントjsのみでデータを保存することができるのか気になったので調べました。

※なんだかんだ言って面倒だからGPTにコード生成してもらった。

使用ライブラリに関しては下記記事のデータストレージの項目から選びました。

https://qiita.com/rana_kualu/items/96b303307d6435aedf8b#%E3%83%87%E3%83%BC%E3%82%BF%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8

## 自分が使いたいもの(メモ)

どれか一つを選ぶとしたらDexie.jsを使おうと思っています。

正直、indexedDBを使用していて[フロントjsで使えるもの](#フロントjsで使えるもの)であればなんでもよく、
その中からDexie.jsを選んだ理由は他のフロントjsで使用できるライブラリと比べて性能が
どうこうというよりただの好き嫌いで選んでいます。

Dexie.jsはLocalForageやPouchdbと比べて更新や削除処理に違和感がないです。
LocalForageやPouchdbはデータ削除するのにget(テーブル)やgetItem(テーブル)が
必要になります。

Dexie.jsもdb.テーブル.delete()という風に実行するのでテーブルにアクセスしていることに変わりはないですが、個人的に削除といっているのにget()が記載されているのに違和感がありました。

またDexie.jsのデータ更新系メソッドはSQLと同じメソッド名なのでしっくりきます。

今まで上げた内容はlovefieldも当てはまるのですが、
両方を調べてみた感じ参考記事の量はDexie.jsのが多そうでした。

Dexie.js
``` js
// データの削除
function deleteData(id) {
    db.friends.delete(id).then(() => {
        console.log("Data deleted from IndexedDB");
        displayData(); // 再表示
    }).catch(error => {
        console.error("Error deleting data:", error);
    });
}
```

LocalForage
``` js
// データの削除
function deleteData(id) {
    db.getItem('friends').then(friends => {
        friends.splice(id, 1);
        return db.setItem('friends', friends);
    }).then(() => {
        console.log("Data deleted from LocalForage");
        displayData();
    }).catch(error => {
        console.error("Error deleting data:", error);
    });
}
```

Pouchdb
``` js
// データの削除
function deleteData(id) {
    db.get(id).then(doc => {
        return db.remove(doc);
    }).then(() => {
        console.log("Data deleted from PouchDB");
        displayData(); // 再表示
    }).catch(error => {
        console.error("Error deleting data:", error);
    });
}
```

lovefield
``` js
// データの削除
function deleteData(id) {
    todoDb.delete().from(item).where(item.id.eq(id)).exec().then(() => {
        console.log("Data deleted from IndexedDB");
        displayData(); // 再表示
    }).catch(error => {
        console.error("Error deleting data:", error);
    });
}
```

## フロントjsで使えるもの

### Dexie.js

IndexedDBのラッパー

indexedDB
フロントjsでデータ保存ができればいいだけなら
これだけでもOK。そのままだと使いにくいらしい。
https://zenn.dev/awyaki/scraps/06a41ea81f55cf
https://qiita.com/Akihiro0711/items/8d68c8bd3cec33708e7a

### LocalForage

下記ストレージが使えるみたいです。
- IndexedDB
- Web SQL(W3Cにより非推奨)
- localStorage(セキュアではない)

### Pouchdb

IndexedDBを使用
オフライン時にデータを貯蓄しオンライン時に同期できるみたいです。
(同期機能は使わない)

### Lovefield

IndexedDBのラッパー
SQLのようにIndexedDBを操作できます。

## コード

実行して画面に表示されたボタンをポチポチしてもらうと
データが保存されているのを確認できると思います。

### Dexie.js

``` js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dexie Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dexie/4.0.4/dexie.min.js"></script>
</head>
<body>
  <button onclick="addData()">Add Data to IndexedDB</button>
  <button onclick="reloadPage()">Reload Page</button>

  <ul id="dataList"></ul>

  <script>
      // Dexieインスタンスの作成
      const db = new Dexie("myDatabase");
      
      // データベースのテーブル定義
      db.version(1).stores({
          friends: '++id,name,age'
      });

      // データの登録
      function addData() {
          db.friends.add({name: "Alice", age: 30}).then(() => {
              console.log("Data added to IndexedDB");
              displayData();
          }).catch(error => {
              console.error("Error adding data:", error);
          });
      }

      // データの参照と表示
      function displayData() {
          const dataList = document.getElementById("dataList");
          dataList.innerHTML = ""; // Clear previous data

          db.friends.toArray().then(friends => {
              friends.forEach(friend => {
                  const listItem = document.createElement("li");
                  listItem.textContent = `${friend.name}, ${friend.age} years old`;

                  // 削除ボタンの追加
                  const deleteButton = document.createElement("button");
                  deleteButton.textContent = "Delete";
                  deleteButton.onclick = () => deleteData(friend.id);
                  listItem.appendChild(deleteButton);

                  // 更新ボタンの追加
                  const updateButton = document.createElement("button");
                  updateButton.textContent = "Update";
                  updateButton.onclick = () => updateData(friend.id);
                  listItem.appendChild(updateButton);

                  dataList.appendChild(listItem);
              });
          }).catch(error => {
              console.error("Error displaying data:", error);
          });
      }

      // データの削除
      function deleteData(id) {
          db.friends.delete(id).then(() => {
              console.log("Data deleted from IndexedDB");
              displayData(); // 再表示
          }).catch(error => {
              console.error("Error deleting data:", error);
          });
      }

      // データの更新
      function updateData(id) {
          const newName = prompt("Enter new name:");
          const newAge = parseInt(prompt("Enter new age:"));

          db.friends.update(id, {name: newName, age: newAge}).then(() => {
              console.log("Data updated in IndexedDB");
              displayData(); // 再表示
          }).catch(error => {
              console.error("Error updating data:", error);
          });
      }

      // ページの再読み込み
      function reloadPage() {
          location.reload();
      }

      // ページロード時にデータ表示
      window.onload = displayData;
  </script>
</body>
</html>
```

### LocalForage

``` js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LocalForage Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js"></script>
</head>
<body>
  <button onclick="addData()">Add Data to LocalForage</button>
  <button onclick="reloadPage()">Reload Page</button>

  <ul id="dataList"></ul>

  <script>
      // LocalForageインスタンスの作成
      const db = localforage.createInstance({
          name: "myDatabase"
      });
      
      // データの登録
      function addData() {
          const friend = { name: "Alice", age: 30 };
          db.getItem('friends').then(existingFriends => {
              const friends = existingFriends || [];
              friends.push(friend);
              return db.setItem('friends', friends);
          }).then(() => {
              console.log("Data added to LocalForage");
              displayData();
          }).catch(error => {
              console.error("Error adding data:", error);
          });
      }

      // データの参照と表示
      function displayData() {
          const dataList = document.getElementById("dataList");
          dataList.innerHTML = ""; // Clear previous data

          db.getItem('friends').then(friends => {
              friends.forEach((friend, id) => {
                  const listItem = document.createElement("li");
                  listItem.textContent = `${friend.name}, ${friend.age} years old`;

                  // 削除ボタンの追加
                  const deleteButton = document.createElement("button");
                  deleteButton.textContent = "Delete";
                  deleteButton.onclick = () => deleteData(id);
                  listItem.appendChild(deleteButton);

                  // 更新ボタンの追加
                  const updateButton = document.createElement("button");
                  updateButton.textContent = "Update";
                  updateButton.onclick = () => updateData(id);
                  listItem.appendChild(updateButton);

                  dataList.appendChild(listItem);
              });
          }).catch(error => {
              console.error("Error displaying data:", error);
          });
      }

      // データの削除
      function deleteData(id) {
          db.getItem('friends').then(friends => {
              friends.splice(id, 1);
              return db.setItem('friends', friends);
          }).then(() => {
              console.log("Data deleted from LocalForage");
              displayData();
          }).catch(error => {
              console.error("Error deleting data:", error);
          });
      }

      // データの更新
      function updateData(id) {
          const newName = prompt("Enter new name:");
          const newAge = parseInt(prompt("Enter new age:"));

          db.getItem('friends').then(friends => {
              friends[id] = { name: newName, age: newAge };
              return db.setItem('friends', friends);
          }).then(() => {
              console.log("Data updated in LocalForage");
              displayData();
          }).catch(error => {
              console.error("Error updating data:", error);
          });
      }

      // ページの再読み込み
      function reloadPage() {
          location.reload();
      }

      // ページロード時にデータ表示
      window.onload = displayData;
  </script>
</body>
</html>
```

### Pouchdb

``` js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PouchDB Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pouchdb/7.2.2/pouchdb.min.js"></script>
</head>
<body>
    <button onclick="addData()">Add Data to PouchDB</button>
    <button onclick="reloadPage()">Reload Page</button>

    <ul id="dataList"></ul>

    <script>
        // PouchDBインスタンスの作成
        const db = new PouchDB('myDatabase');

        // データの登録
        function addData() {
            const doc = {
                _id: new Date().toISOString(),
                name: 'Alice',
                age: 30
            };
            
            db.put(doc).then(() => {
                console.log("Data added to PouchDB");
                displayData();
            }).catch(error => {
                console.error("Error adding data:", error);
            });
        }

        // データの参照と表示
        function displayData() {
            const dataList = document.getElementById("dataList");
            dataList.innerHTML = ""; // Clear previous data

            db.allDocs({include_docs: true}).then(docs => {
                docs.rows.forEach(row => {
                    const doc = row.doc;
                    if (doc.name && doc.age) {
                        const listItem = document.createElement("li");
                        listItem.textContent = `${doc.name}, ${doc.age} years old`;

                        // 削除ボタンの追加
                        const deleteButton = document.createElement("button");
                        deleteButton.textContent = "Delete";
                        deleteButton.onclick = () => deleteData(doc._id);
                        listItem.appendChild(deleteButton);

                        // 更新ボタンの追加
                        const updateButton = document.createElement("button");
                        updateButton.textContent = "Update";
                        updateButton.onclick = () => updateData(doc._id, doc.name, doc.age);
                        listItem.appendChild(updateButton);

                        dataList.appendChild(listItem);
                    }
                });
            }).catch(error => {
                console.error("Error displaying data:", error);
            });
        }

        // データの削除
        function deleteData(id) {
            db.get(id).then(doc => {
                return db.remove(doc);
            }).then(() => {
                console.log("Data deleted from PouchDB");
                displayData(); // 再表示
            }).catch(error => {
                console.error("Error deleting data:", error);
            });
        }

        // データの更新
        function updateData(id, name, age) {
            const newName = prompt("Enter new name:", name);
            const newAge = parseInt(prompt("Enter new age:", age));

            db.get(id).then(doc => {
                doc.name = newName;
                doc.age = newAge;
                return db.put(doc);
            }).then(() => {
                console.log("Data updated in PouchDB");
                displayData(); // 再表示
            }).catch(error => {
                console.error("Error updating data:", error);
            });
        }

        // ページの再読み込み
        function reloadPage() {
            location.reload();
        }

        // ページロード時にデータ表示
        window.onload = displayData;
    </script>
</body>
</html>
```

### Lovefield

``` js
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lovefield Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lovefield/2.1.12/lovefield.min.js"></script>
</head>
<body>
  <button onclick="addData()">Add Data to IndexedDB</button>
  <button onclick="reloadPage()">Reload Page</button>

  <ul id="dataList"></ul>

  <script>
      var schemaBuilder = lf.schema.create('todo', 1);

      schemaBuilder.createTable('Item')
          .addColumn('id', lf.Type.INTEGER)
          .addColumn('description', lf.Type.STRING)
          .addColumn('deadline', lf.Type.DATE_TIME)
          .addColumn('done', lf.Type.BOOLEAN)
          .addPrimaryKey(['id'], true)
          .addIndex('idxDeadline', ['deadline'], false, lf.Order.DESC);

      var todoDb;
      var item;

      schemaBuilder.connect().then(function(db) {
          todoDb = db;
          item = db.getSchema().table('Item');
          displayData();
      });

      // データの登録
      function addData() {
          var row = item.createRow({
              'description': 'Get a cup of coffee',
              'deadline': new Date(),
              'done': false
          });

          todoDb.insertOrReplace().into(item).values([row]).exec().then(() => {
              console.log("Data added to IndexedDB");
              displayData();
          }).catch(error => {
              console.error("Error adding data:", error);
          });
      }

      // データの参照と表示
      function displayData() {
          const dataList = document.getElementById("dataList");
          dataList.innerHTML = ""; // Clear previous data

          todoDb.select().from(item).exec().then(rows => {
              rows.forEach(row => {
                  const listItem = document.createElement("li");
                  listItem.textContent = `${row.description}, deadline: ${row.deadline}`;

                  // 削除ボタンの追加
                  const deleteButton = document.createElement("button");
                  deleteButton.textContent = "Delete";
                  deleteButton.onclick = () => deleteData(row.id);
                  listItem.appendChild(deleteButton);

                  // 更新ボタンの追加
                  const updateButton = document.createElement("button");
                  updateButton.textContent = "Update";
                  updateButton.onclick = () => updateData(row.id);
                  listItem.appendChild(updateButton);

                  dataList.appendChild(listItem);
              });
          }).catch(error => {
              console.error("Error displaying data:", error);
          });
      }

      // データの削除
      function deleteData(id) {
          todoDb.delete().from(item).where(item.id.eq(id)).exec().then(() => {
              console.log("Data deleted from IndexedDB");
              displayData(); // 再表示
          }).catch(error => {
              console.error("Error deleting data:", error);
          });
      }

      // データの更新
      function updateData(id) {
          const newDescription = prompt("Enter new description:");
          const newDeadline = new Date(prompt("Enter new deadline (YYYY-MM-DD):"));

          todoDb.update(item)
            .set(item.description, newDescription)
            .set(item.deadline, newDeadline)
            .where(item.id.eq(id))
            .exec().then(() => {
              console.log("Data updated in IndexedDB");
              displayData(); // 再表示
          }).catch(error => {
              console.error("Error updating data:", error);
          });
      }

      // ページの再読み込み
      function reloadPage() {
          location.reload();
      }

      // ページロード時にデータ表示
      window.onload = displayData;
  </script>
</body>
</html>
```

## フロントjsで使えないもの(使わないもの)

### LokiJS

IndexedDBのラッパー?
参考記事が少なく、検索しても他のLoki jsがヒットしてしまうので除外しました。

### AlaSQL

localStorage, Excelにアクセスできるみたいです。

localStorageは使うつもりがないのと、
Excelへのアクセスに関してはCORSエラーになります。
サーバーを立てれば解決しますが、フロントjsのみの場合は
サーバーを立てれないので除外しました。

Local Storageを使用しないこと
メモ帳作る程度なら気にしなくてよさそうだが
パスワードをメモするつもりならやめた方がいい
https://techracho.bpsinc.jp/hachi8833/2024_04_05/80851

CORSエラー
ローカルファイルを読み込もうとするとエラーになる
サーバを立てれば解決可能
https://ugo.tokyo/same-origin-policy/

### lowdb

json Serverのリソース更新で使用されるみたいです。
Node.jsが必要そうなので除外しました。

json Server
モックサーバを立てれる
https://majisemi.com/topics/oss/4925/#:~:text=JSON%20Server(%E3%82%B8%E3%82%A7%E3%82%A4%E3%82%BD%E3%83%B3%20%E3%82%B5%E3%83%BC%E3%83%90)%E3%81%A8,%E3%81%AA%E3%81%A9%E3%81%AB%E6%B4%BB%E7%94%A8%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82

### NeDB

組み込み型DB
Electronなどで使用されるみたいです。

たぶんNode.jsが必要です。(ちゃんと調べてない)
※Node.jsが不要でも組み込みDBならsqliteみたいにファイルに
　アクセスすることになるのでCORSエラーになると思います。
 
フロントjsのみで解決したいので除外しました。

Electron
フロント技術でパッケージ作成する
osに依存しない
https://www.electronjs.org/ja/docs/latest

### RxDB

オフラインで貯蓄したデータをオンライン時に別DBと同期できるそうです。
Node.jsが必要そうなので除外しました。