ElectronのDBにelectron-storeを使う。

# はじめに
ElectronでToDoアプリを作る際にデータを保存するためelectron-storeを使いました。
今後も使うかもしれないので得た知識をここに置いておきます。
electron-storeの機能紹介ではなく、完成したソースコードと関連するElectronの仕組みの説明です。

:::note info
今回は[electron-vite](https://electron-vite.org/)から`npm create @quick-start/electron my-app`で作成を開始しています。ファイル名など他の記事と違うところがあるかと思いますがご了承ください。
:::
# electron-storeとは
https://github.com/sindresorhus/electron-store
electronでデータを保存するためのモジュールです。

>Simple data persistence for your Electron app or module - Save and load user settings, app state, cache, etc
>
>Electron doesn't have a built-in way to persist user settings and other data. This module handles that for you, so you can focus on building your app. The data is saved in a JSON file named config.json in app.getPath('userData').

保存形式はjsonになります。
デフォルトだと`C:\Users\[user name]\AppData\Roaming\[app name]\config.json`にデータが保存されます(Windowsでの話です)

インストールは以下のコマンドを実行するだけ。
```
npm install electron-store
```
# 実装
とりあえず今回書いたソースコードは以下の通り。
この後解説します。

```preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

// 保存するデータの型定義
interface Task {
  task: string
  createdTime: Date
  status: number
}

// データの取得と保存の処理を定義
const api = {
  getList: (): Promise<Task[]> => ipcRenderer.invoke('getList'),
  setList: (data): Promise<void> => ipcRenderer.invoke('setList', data)
}

if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI)
    // windowにAPIを入れる
    // window.api.getListとwindow.api.setList
    contextBridge.exposeInMainWorld('api', api)
  } catch (error) {
    console.error(error)
  }
} else {
  // @ts-ignore (define in dts)
  window.electron = electronAPI
  // @ts-ignore (define in dts)
  window.api = api
}
```

```main/index.ts
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import Store from 'electron-store'

// storeの読み込み
const store = new Store({ taskList: [] })

function createWindow(): void {
  // webPreferences.preloadでpreloadを使用することを明示する
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.mjs'),
      sandbox: false
    }
  })

}

...

// getListの処理を追加
ipcMain.handle('getList', async (event, data) => {
  return store.get('taskList', [])
})

// setListの処理を追加
ipcMain.handle('setList', async (event, data) => {
  store.set('taskList', data)
})
```

これでウェブページからデータを読み書きする処理`getList`, `setList`を使用できます。
```javascript
// データの取得
const taskList = await window.api.getList();

// データの保存
await window.api.setList(taskList);
```

# ちょっと解説
勉強中の身なので大雑把な理解しかできていませんが、Electronの理解の助けになればと思います。
※間違いがあればすみません。
## メインプロセスとレンダラープロセス
ElectronにはNodeモジュールを扱うなどするメインプロセスとウェブページを実行するレンダラープロセスという2つのプロセスが存在します。ちなみにNode.jsを実行するのはメインプロセスで、レンダラープロセスはセキュリティの関係でNode.jsを実行しません。
この２つのプロセスをつないでくれるのがプリロードと呼ばれるスクリプトです。
今回でいう`preload/index.ts`がこれにあたります。

プリロードスクリプトはウェブページを読み込む前に実行されます。
ウェブページからメインプロセスの処理を実行できるようにするには`contextBridge API`を使用して処理内容を定義する必要があります。
```pleload/index.ts
contextBridge.exposeInMainWorld('api', api)
```
このスクリプトをレンダラープロセスにアタッチするためにメインプロセスの`BrowserWindow`の`webPreferences.preload`に`contextBridge`を記載したファイルのパスを追加します。
```main/index.ts
function createWindow(): void {
  // webPreferences.preloadでpreloadを使用することを明示する
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.mjs'),
      sandbox: false
    }
  })
  ...
}
```
これでレンダラープロセスが`api`という処理にアクセスできるようになりました。
ウェブページからは`window.api`でアクセスできます。
## プロセス間通信
メインプロセスとレンダラープロセスは互換性がないためメインプロセスはウェブページに、レンダラープロセスはNode.jsにアクセスすることができません。
この問題を解決してウェブページからjsonファイルのデータを取得する処理を行えるように`ipcMain`と`ipcRenderer`モジュールを使用します。
ウェブページからメインプロセスに通信するには`ipcMain.handle`でメインプロセスでの処理を設定し、`ipcRenderer.invoke`でプリロードからメインプロセスの処理を公開します。
```preload/index.ts
// データの取得と保存の処理を定義
const api = {
  getList: (): Promise<Task[]> => ipcRenderer.invoke('getList'),
  setList: (data): Promise<void> => ipcRenderer.invoke('setList', data)
}
```
```main/index.ts
// getListの処理を追加
ipcMain.handle('getList', async (event, data) => {
  return store.get('taskList', [])
})

// setListの処理を追加
ipcMain.handle('setList', async (event, data) => {
  store.set('taskList', data)
})
```
これにより、`ipcRenderer.invoke`で定義した`getList` `setList`というチャネルを通じてレンダラーからメインプロセスへ処理の実行をお願いすることができます。
公式が言うには`ipcRenderer`を直接`contextBridge`で公開するのはセキュリティ面(?)からよくないそうです。
# おわりに
わかりにくい箇所や間違い等ありましたら修正しますので、お声がけいただけると嬉しいです！