はじめに
ElectronでToDoアプリを作る際にデータを保存するためelectron-storeを使いました。
今後も使うかもしれないので得た知識をここに置いておきます。
electron-storeの機能紹介ではなく、完成したソースコードと関連するElectronの仕組みの説明です。
今回はelectron-viteから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
実装
とりあえず今回書いたソースコードは以下の通り。
この後解説します。
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
}
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
を使用できます。
// データの取得
const taskList = await window.api.getList();
// データの保存
await window.api.setList(taskList);
ちょっと解説
勉強中の身なので大雑把な理解しかできていませんが、Electronの理解の助けになればと思います。
※間違いがあればすみません。
メインプロセスとレンダラープロセス
ElectronにはNodeモジュールを扱うなどするメインプロセスとウェブページを実行するレンダラープロセスという2つのプロセスが存在します。ちなみにNode.jsを実行するのはメインプロセスで、レンダラープロセスはセキュリティの関係でNode.jsを実行しません。
この2つのプロセスをつないでくれるのがプリロードと呼ばれるスクリプトです。
今回でいうpreload/index.ts
がこれにあたります。
プリロードスクリプトはウェブページを読み込む前に実行されます。
ウェブページからメインプロセスの処理を実行できるようにするにはcontextBridge API
を使用して処理内容を定義する必要があります。
contextBridge.exposeInMainWorld('api', api)
このスクリプトをレンダラープロセスにアタッチするためにメインプロセスのBrowserWindow
のwebPreferences.preload
にcontextBridge
を記載したファイルのパスを追加します。
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
でプリロードからメインプロセスの処理を公開します。
// データの取得と保存の処理を定義
const api = {
getList: (): Promise<Task[]> => ipcRenderer.invoke('getList'),
setList: (data): Promise<void> => ipcRenderer.invoke('setList', data)
}
// 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
で公開するのはセキュリティ面(?)からよくないそうです。
おわりに
わかりにくい箇所や間違い等ありましたら修正しますので、お声がけいただけると嬉しいです!