[Electron] 設定情報をローカルファイルに簡単保存 - electron-store

設定情報やちょっとしたデータの管理にレンダラープロセスの場合はWebStorageやIndexedDBが利用できますが、メインプロセスでは自力でファイルに保存する処理が必要でちょっと面倒。そんな時に利用するのがelectron-storeです。手軽にデータの永続化ができます。

今回はこのelectron-storeを利用しウィンドウの位置とサイズを記録、次回起動する際に復元するサンプルを作成します。

ソースコード

実際に稼働するソースはGitHubからも確認できます。
github.com

準備

npmで一発で入ります。

$ npm install electron-store

基本的な原理

利用方法

一言で言うならKVSですね。データを保存する際にはstore.set(key, value)、保存したデータを取得するにはstore.get(key)を利用します。

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

// 保存
store.set('aisatsu', 'Helloworld')

// 取り出し
const value = store.get('aisatsu')

// 削除
store.delete('aisatsu')
store.clear()   // すべて削除

// 存在確認
if( store.has('foo') ){
  console.log('foo is exists');
}

store.set(key, value)valueで渡す値は、最終的にJSON.stringify()を通して保存され、取り出すときはJSON.parse()を通し保存前の状態に戻されるためJSで扱える大抵のデータ型は扱うことができます。

どこに記録されるの?

Electronから提供されるユーザーデータを保存するのに適したフォルダの下にconfig.jsonという名称で保存されます。

app.getPath("userData")

具体的には以下の場所になります。

OS パス
Windows. C:\Users(ユーザー名)\AppData\Roaming(アプリ名)
macOS ~/Library/Application Support/(アプリ名)

中身は拡張子からも分かる通りJSON形式(テキスト形式)で保存されます。

$ cat "${HOME}/Library/Application Support/electron-sample-config/config.json"
{
  "aisatsu": "Helloworld"
}

サンプル

ウィンドウを閉じる際に最終的なウィンドウの位置とサイズをelectron-storeで保存し、次回起動する際に復元するサンプルになります。

デモ

実際に動かした物が以下になります。

メインプロセス

/**
 * ウィンドウの位置とサイズを復元するサンプル
 *
 */

//------------------------------------
// モジュール
//------------------------------------
const { app, BrowserWindow, ipcMain, screen } = require('electron')
const Store = require('electron-store')
const store = new Store()

//------------------------------------
// 定数
//------------------------------------
// ウィンドウのデフォルトサイズ
const DEFAULT_SIZE = {
  width: 800,
  height: 600
}

//------------------------------------
// グローバル変数
//------------------------------------
// ウィンドウ管理用
let mainWin


/**
 * ウィンドウを作成する
 */
function createWindow () {
  const pos  = store.get('window.pos')  || getCenterPosition();
  const size = store.get('window.size') || [DEFAULT_SIZE.width, DEFAULT_SIZE.height];

  // ウィンドウを新たに開く
  mainWin = new BrowserWindow({
    show: false,
    width: size[0],
    height: size[1],
    x: pos[0],
    y: pos[1],
    webPreferences: {
      nodeIntegration: true
    }
  })

  // ウィンドウ内に指定HTMLを表示
  mainWin.loadFile('public/index.html')

  // 準備が整ったら表示
  mainWin.once('ready-to-show', () => {
    mainWin.show()
  })

  // ウィンドウが閉じられる直前に実行
  mainWin.on('close', ()=>{
    store.set('window.pos', mainWin.getPosition())  // ウィンドウの座標を記録
    store.set('window.size', mainWin.getSize())     // ウィンドウのサイズを記録
  })
}

//------------------------------------
// [app] イベント処理
//------------------------------------
// 初期化が終了したらウィンドウを新規に作成する
app.whenReady().then(()=>{
  createWindow();
})


// すべてのウィンドウが閉じられたときの処理
app.on('window-all-closed', () => {
  // macOS以外はアプリを終了する
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// アプリがアクティブになった時の処理
// (macOSはDocのアイコンがクリックされたとき)
app.on('activate', () => {
  // ウィンドウがすべて閉じられている場合は新しく開く
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

//------------------------------------
// ipc通信
//------------------------------------
// 「リセット」ボタン押下
ipcMain.handle('window-reset', async (event, data) => {
  const pos = getCenterPosition();
  mainWin.setSize(DEFAULT_SIZE.width, DEFAULT_SIZE.height);
  mainWin.setPosition(pos[0], pos[1]);

  return(true);
});


/**
 * ウィンドウの中央の座標を返却
 *
 * @return {array}
 */
function getCenterPosition(){
  const { width, height } = screen.getPrimaryDisplay().workAreaSize
  const x = Math.floor( (width - DEFAULT_SIZE.width) / 2)
  const y = Math.floor( (height - DEFAULT_SIZE.height) / 2)
  return([x, y]);
}

レンダラープロセス

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
  <title>Electron Smaple - config</title>
  <style>
    body{ background-color:white; }
  </style>
</head>
<body>

<h1>ウィンドウの状態を記録</h1>
<p>アプリが終了した瞬間のウィンドウの位置とサイズを記録し、次回起動時に復元します。</p>

<button type="button" id="btn-reset">リセット</button>

<script>
  const {ipcRenderer} = require('electron');

  document.querySelector('#btn-reset').addEventListener('click', () => {
    // メインプロセスを呼び出し
    ipcRenderer.invoke('window-reset')
      .then((result) => {
        if(!result){
          alert('リセットに失敗しました');
        }
      })
  });
</script>
</body>
</html>

おまけ

保存先やファイル名を変更する

データファイルの保存先ディレクトリやファイル名はインスタンス生成時に変更することができます。

const Store = require('electron-store')
const store = new Store({
    cwd: app.getPath('userData')  // 保存先のディレクトリ
    name: 'config',               // ファイル名
    fileExtension: 'json'         // 拡張子
})

データファイルを暗号化する

config.jsonはただのJSON形式のファイルのため、ユーザーがテキストエディタなどで閲覧したり編集することが可能です。何らかの事情でこれらを防ぎたい場合、簡易的な暗号化の仕組みが用意されています。

インスタンスを作成する際にencryptionKeyに適当な文字列(暗号化キー)を渡すだけでAES256 CBC形式で暗号化してくれます。

const Store = require('electron-store')
const store = new Store({encryptionKey: 'foobar'})

通常、config.jsonodコマンドでバイナリとして開くと以下のようにASCIIな文字が記録されているのがわかります。

$ od -tc "${HOME}/Library/Application Support/electron-sample-config/config.json"
0000000    {  \n  \t   "   w   i   n   d   o   w   "   :       {  \n  \t
0000020   \t   "   p   o   s   "   :       [  \n  \t  \t  \t   3   2   0
0000040    ,  \n  \t  \t  \t   1   3   8  \n  \t  \t   ]   ,  \n  \t  \t
0000060    "   s   i   z   e   "   :       [  \n  \t  \t  \t   8   0   0
0000100    ,  \n  \t  \t  \t   6   0   0  \n  \t  \t   ]  \n  \t   }  \n
0000120    }                                                            
0000121

しかし先ほどのencryptionKeyを指定して同じコマンドを叩くと以下のようにすぐには解読できなくなってしまいました。

$ od -tc "${HOME}/Library/Application Support/electron-sample-config/config.json"
0000000  212   8 005 343 242   ? 240   i   V   ˁ  ** 261 307   \ 214 072
0000020    :   e 304 001   £  ** 220 345  \v 334 377 027 247 260 254   8
0000040  207   v 031 261 265 241 347   % 311 300 367 004 025   j   o 032
0000060  375   | 237 276 313 367   i 270 036   R   $   !   μ  ** 306 115
0000100    M 217 252   c   = 365 023   ` 261   ȵ  ** 030   v 241   ' 056
0000120    . 364   u 235 202   I 254   r 376   X 201   4   % 025   8   Q
0000140  221 006 035 350 225   ީ  ** 004   7   + 216 355 246   4   a 247
0000160  247                                                            
0000161

もちろんソースコードを覗かれると暗号方式も暗号化キーもバレるため、完全に秘匿することは難しくあくまで簡易的な物です。何もしないよりはマシですかね。あと暗号化キーを忘れると復号化できなくなりますのでご注意ください。

Validationを行う

config.jsonに意図しない値が混入しないようにチェックを行うことが可能です。

インスタンス作成時にschemaを渡してあげます。

const Store = require('electron-store')
const store = new Store({
  schema: {
    'aisatsu': {.      // キー
      type: 'string',  // データ型 (文字列)
      minLength: 5,    // 文字数の下限(5文字)
      maxLength: 30.   // 文字数の上限 (30文字)
    },
    'foo': {           // キー
      type: 'number',  // データ型(整数)
      minimum: 1       // 最小値(1)
    }
  }
})

先ほどfooは整数型であると定義しましたが、以下のようにschemaに違反したデータをsetしようとすると例外が発生します。

store.set('foo', 'bar');

// ↓以下の例外が発生する
// App threw an error during load
// Error: Config schema violation: `foo` should be number

例外が発生するとそこでプログラムの実行が終了しちゃいますので、実際に利用する際にはtry〜catchで囲う必要がありますね。

try{
  store.set('foo', 'bar');
}
catch(e){
  console.error(e.message)
}

この仕組はelectron-storeの本体であるconfモジュール内で、ajvモジュールを利用して実装されています。先ほどのサンプル以外に利用できるValidationのキーワードについては以下のページを参照してください。 github.com

参考ページ