🧩

初めてのElectron vol.03 [簡易ToDoリストを作成(データ保存)]

2022/01/25に公開

記事はbookへ統合されます

https://zenn.dev/isana_citrus/books/90372c9fe6554d

vol.03 今回の内容

第三回です。前回の
"完成品"
これのデータを保存できるようにしていきます。

どうやって実現するか

ぶっちゃけWindow.localStorage使うのが定石な気がするのはおいておいて、
electronの機能を学ぶためにelectron-storeを使います。

electron-store

electronでデータを保存しておくためのモジュールです。
簡単にデータの読み書きができるようになります。

electron-store
const Store = require('electron-store');
const store = new Store();

store.set('key', 'data');       // 保存

const data = store.get('key');  // 取り出し

store.delete('key');            // 削除
store.clear();                  // データ全部削除

if( store.has('key') ){// 存在確認
  console.log('keyあった');
}

簡単でしょ
入れるのはnpmで一発

npm install electron-store

実際にコードを書く前にelectronの基本の話

electronは大きく分けるとメインプロセスレンダラープロセスで構成されています

メインプロセス

これはelectronを起動すると呼び出されるやつです。今作成しているモノだと./main.jsが該当します。このメインプロセスがレンダラープロセスを呼んでああだこうだする感じです
Electronの設定やOS由来の機能はメインプロセスで制御されてます

レンダラープロセス

こっちが実際に表示される部分を制御している部分です
今作っているモノだと./index.html(から呼び出されている[./todolist.js])とか表示しているウインドウが該当します、多分

通常のブラウザでできること以外のNode.jsやElectronの機能を直接呼び出すことはできません
正確には昔は使えました
この仕様変更のせいで、electronのチュートリアルにつまずく人が増える気がする

index.html
<body>
    <!--色々-->
    <script>
    const {ipcRenderer} = require('electron');//<!--ここがエラーになる-->
    //何らかの処理
    </script>
</body>

上記書き方をしている記事が多くそのままだとUncaught ReferenceError: require is not definedエラーが発生しちゃうんですよね
今も非推奨ですがちょっといじれば使えますがセキュリティ的によくないそうです。なので使いません

余談ですが

main.js
const mainWindow = new BrowserWindow({
    webPreferences: {
    nodeIntegration: true,
    contextIsolation: false,
    },
});

と定義すれば上記エラーとかでなくなりまっせ

レンダラーとメインの橋渡し

preload.jsを使う
小見出しの通り橋渡しをしてくれるやつ
レンダラーがNode Electron APIに自由にアクセスできないように、
あらかじめpreload.jsを作成し必要な機能のみを記載することで、安全に橋渡ししてくれる便利もの

イメージ図
[メインプロセス] < == > [preload.js] < == > [レンダラープロセス]

直接じゃない&定義されていないものを呼び出せなくすることで安全性向上してるっぽいですね

これを踏まえていくとちょっとわかりやすくなるかもしれません

実際にコード書いてく

では実際にコードを書いていきます

main.js書き換え

レンダラーからの通信に応答するためのやつを読み込むようにする

./main.js
 const electron = require("electron");   //electron読み込んで
-const { app, BrowserWindow } = electron;//electronからapp,BrowserWindowを取り出す
+const { app, BrowserWindow, ipcMain } = electron;
+const Store = require('electron-store');//storeも読み込んでおく

通信に応答するところは後で書きます

preload.jsを使うよってBrowserWindowに教えてやる

./main.js
 //省略
 //electron が準備終わったとき
app.on("ready", function () {
    //新しいウインドウを開く
    mainWindow = new BrowserWindow({
        width: 320,
        height: 500,
+       webPreferences: {
+           preload: path.join(__dirname, 'preload.js'),
+       }
    });
    //省略
});

このように教えてあげましょう
webpreferencesって書き忘れてはまったのはないしょ

preload.js作成

今日の本題preload.jsを書いていきます

./preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld(
    'dataapi', {
    getlist: () => ipcRenderer.invoke("getlist"),
    setlist: (data) => ipcRenderer.invoke("setlist", data),
}); 

レンダラー側からwindow.dataapi.getlist()を呼ぶとメイン側に"getlist"中継する
レンダラー側からwindow.dataapi.setlist()を呼ぶとメイン側に"setlist"とdataを中継する
みたいなことを書いてます

main.jsに処理を書いていく

下記のように追記します

./main.js
 //省略
+const Store_Data = new Store({ name: "data" });//Dataを格納しておくStore
 //electron が準備終わったとき
 app.on("ready", function () {
    //省略
 }

+// IPC通信(DataAPI関係)
+
+//getlist(data取得処理)
+ipcMain.handle('getlist', async (event, data) => {
+    return Store_Data.get('ToDoList', []);//ToDoListがあれば取り出し、なければからのリストを返す
+});
+
+//getlist(data保存処理)
+ipcMain.handle("setlist", async (event, data) => {
+    Store_Data.set('ToDoList', data);       // 保存
+});

todolist.jsを書いていく

./todolist.js
 //アイテム追加ボタンを押したときの処理
 function addToDo() {
      const item = document.querySelector("#ToDoItem").value;//formの文字列取得
      if (item === "") {//itemがからだったら何もしない
          return
      }
+     //preloadを介してmainjsでStoreのデータを取得 
+     const ToDoList = await window.dataapi.getlist();
+     ToDoList.push(item);//今回追加されたものをデータに追加
+     await window.dataapi.setlist(ToDoList);//preloadを介してmainjsでStoreのデータを保存
+     listview();//表示を更新
-     const ul = document.querySelector("#ToDolist");//id=ToDolistを取得する
-     const li = document.createElement("li");//li elementを作る
-     const todotext = document.createTextNode(item);//itemをテキストノードにする
-     li.appendChild(todotext);//liにtodotextを入れる
-     ul.appendChild(li);//ulに作ったliを入れる
}

+//Storeデータをmain.jsからとってきて表示する処理
+async function listview() {
+    const ul = document.querySelector("#ToDolist");//id=ToDolistを取得する
+    const clone = ul.cloneNode(false); //ul要素の中身以外を拝借
+    const ToDoList = await window.dataapi.getlist();
+    for (const item of ToDoList) {
+        const li = document.createElement("li");//li elementを作る
+        const todotext = document.createTextNode(item);//itemをテキストノードにする
+        li.appendChild(todotext);//liにtodotextを入れる
+        clone.appendChild(li);//ulに作ったliを入れる
+    }
+    ul.parentNode.replaceChild(clone, ul); //ulをcloneに入れ替える
+}
+//起動時初期化用
+listview();

準備はできた

ここまでいろいろファイルをいじったので、
現在のファイル内容を確認しよう
コメントの位置とかちょっとづつ違うのはご容赦

./main.js
const electron = require("electron");   //electron読み込んで
const { app, BrowserWindow, ipcMain } = electron;//electronからapp,BrowserWindowを取り出す
const path = require("path");           //pathも使う
const Store = require('electron-store');//storeも読み込んでおく
let mainWindow;
const Store_Data = new Store({ name: "data" });//Dataを格納しておくStore
//electron が準備終わったとき
app.on("ready", function () {
    //新しいウインドウを開く
    mainWindow = new BrowserWindow({
        width: 320,
        height: 500,
        webPreferences: {
            preload: path.join(__dirname, 'preload.js'),
        }
    });
    //mainWindowでhtmlファイルを開く
    //"file://" + path.join(__dirname, 'index.html');>> file://作業ディレクトリ/index.html
    mainWindow.loadURL("file://" + path.join(__dirname, 'index.html'));

    //メインウインドウが閉じたらアプリが終了する
    mainWindow.on("closed", function () {
        app.quit();
    });
});
// IPC通信(DataAPI関係)

//getlist(data取得処理)
ipcMain.handle('getlist', async (event, data) => {
    return Store_Data.get('ToDoList', []);//ToDoListがあれば取り出し、なければからのリストを返す
});

//getlist(data保存処理)
ipcMain.handle("setlist", async (event, data) => {
    Store_Data.set('ToDoList', data);       // 保存
});
./todolist.js
//アイテム追加ボタンを押したときの処理
async function addToDo() {
    const item = document.querySelector("#ToDoItem").value;//formの文字列取得
    if (item === "") {//itemがからだったら何もしない
        return
    }
    const ToDoList = await window.dataapi.getlist();//preloadを介してmainjsでStoreのデータを取得 
    ToDoList.push(item);//今回追加されたものを追加
    await window.dataapi.setlist(ToDoList);//preloadを介してmainjsでStoreのデータを保存
    listview();
}
//Storeデータをmain.jsからとってきて表示する処理
async function listview() {
    const ul = document.querySelector("#ToDolist");//id=ToDolistを取得する
    const clone = ul.cloneNode(false); //ul要素の中身以外を拝借
    const ToDoList = await window.dataapi.getlist();
    for (const item of ToDoList) {
        console.log(item);
        const li = document.createElement("li");//li elementを作る
        const todotext = document.createTextNode(item);//itemをテキストノードにする
        li.appendChild(todotext);//liにtodotextを入れる
        clone.appendChild(li);//ulに作ったliを入れる
    }
    ul.parentNode.replaceChild(clone, ul); //入れ替える


}
//起動時初期化用
listview();
./preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld(
    'dataapi', {
    getlist: () => ipcRenderer.invoke("getlist"),
    setlist: (data) => ipcRenderer.invoke("setlist", data),
}); 
./index.html
<html>

<head>
    <title>ToDoリスト</title>
</head>

<body>
    <h1>ToDoリスト</h1>
    <div class="form">
        <input type="text" id="ToDoItem">
        <button onclick="addToDo()">追加</button>
        <!-- ボタンが押されたらjsファイルの function addToDo()を呼ぶ-->
    </div>
    <ul id="ToDolist">
    </ul>
    <script src="./todolist.js"></script>
    <!-- jsファイルを読み込む-->
</body>

</html>

動作テスト

npm start


この状態でウィンドウを閉じて開きなおしても

この通り保存できた。

次回予告

  • 完了したものを消す
  • 全削除ボタンを付ける
  • 非アクティブでもキーボードショートカットで呼び出せるようにする

の全部かどれかです。

あとがき

book形式で記事書けばよかったとちょっと後悔中

Discussion