Electronでテキストファイルの読み書きを行う


あらすじ

「Electronを使用すれば自分好みのローカルノートアプリが作れるのでは?🤔」と思ったため、最低限必要そうな「ウィンドウの表示」と「ファイルの読込と書込」を行ってみることにした。

環境

electron 10.1.5

ウィンドウの表示

公式ドキュメントのQuick Start Guideを参考に実装を行う。

ディレクトリの作成

mkdir my-electron-app
cd my-electron-app

パッケージの追加

yarn init -y
yarn add --dev electron

main.jsの作成

const { app, BrowserWindow } = require('electron')

function createWindow () {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  win.loadFile('index.html')
  win.webContents.openDevTools()
}

app.whenReady().then(createWindow)

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

開発者ツールを表示したくない場合はwin.webContents.openDevTools()をコメントアウトする。

ウィンドウに表示するファイル(index.html)の作成

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-inline';"
    />
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

package.jsonの編集

{
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  }
}

mainの修正

mainの初期値がindex.jsなので、main.jsに書き換える。

scriptsの追加

electron .を実行するコマンドを追加する(実行時にmainのスクリプトが読み込まれる)。

アプリケーションの起動

yarn startを実行すると、index.htmlを読み込んだウィンドウが表示される。

メインプロセスとレンダラープロセス

Main and Renderer Processes

Electronにはメインプロセスとレンダラープロセスの、2種類のプロセスが存在する。ざっくりいうとレンダラープロセスはそれぞれ担当のWebページを管理しており、メインプロセスはそれらの親玉🙄

レンダラープロセスからメインプロセスのAPIを使用する

Electronで使用できるAPIには、どちらかのプロセスのみでしか使用できないものも存在する。
メインプロセスのみで使えるAPIをレンダラープロセスで使用するためには、何かしらの方法を取る必要がある。

remoteを使用する

レンダラープロセスからメインプロセスのみで使用できるAPIを使用するために次のようなコードをよく見かけるが、Electron 10.0.0からはデフォルトで無効になっている(パフォーマンスやセキュリティの問題のためらしい🤔)。

// dialogはメインプロセスのAPIであり、レンダラープロセスで使用するときはremoteを経由する
const { dialog } = require('electron').remote

enableRemoteModuletrueにするとremote moduleが有効になるが、将来的に素のElectronからは使えなくなりそうなので注意🙄

ipcMainとipcRendererの使用

レンダラープロセスからメインプロセスへメッセージを送り、その結果をレンダラープロセスに戻す。

main.js(メインプロセス)

const { ipcMain } = require('electron')

ipcMain.handle('open', (event) => {
  // ...
})

index.js(レンダラープロセス)

const { ipcRenderer } = require('electron')

(async () => {
  await ipcRenderer.invoke('open')
})()

今回はこちらの方法を使用する。

ファイルの読込

ファイル選択ダイアログを表示し、選択したファイルを読み込む機能を作成する。ダイアログではパスの取得のみを行い、ファイルの読込自体はfsモジュールを使用する。

画面の作成(レンダラープロセス)

index.html

ファイル選択ダイアログを表示するボタンと、読み込んだファイルを表示するためのテキストエリアを配置している。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-inline';"
    />
  </head>
  <body>
    <h1>Hello World!</h1>
    <div>
      <button id="open" type="button">Open</button>
    </div>
    <div>
      <textarea id="text"></textarea>
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js

「ファイル選択ダイアログを表示するボタン」に対してクリックイベントを登録している。この後の工程で「ファイル選択ダイアログを表示して、選択したファイルの内容を取得して表示する処理」を追加する。

document.querySelector('#open').addEventListener('click', () => {
  // document.querySelector('#text').value = 'テキストエリアに表示したいテキスト'
})

ファイルを読み込む処理の実装(メインプロセス)

main.js

レンダラープロセスからメッセージを受け取ったとき、ファイル選択ダイアログを表示する。ファイルの選択がキャンセルされた場合は空の配列を返し、そうでない場合は選択したファイル(のパス)の中身を読み込み、配列にして返す。

const fs = require('fs')
const { app, BrowserWindow, ipcMain, dialog } = require('electron')

// ウィンドウ周りの処理(中略)

ipcMain.handle('open', async (event) => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    filters: [
      { name: 'Documents', extensions: ['txt'] }
    ]
  })

  if (canceled) return { canceled, data: [] }

  const data = filePaths.map((filePath) =>
    fs.readFileSync(filePath, { encoding: 'utf8' })
  )

  return { canceled, data }
})

index.js(レンダラープロセス)

メインプロセスへメッセージを送信し、配列の先頭の内容をテキストエリアに表示する。

const { ipcRenderer } = require('electron')

document.querySelector('#open').addEventListener('click', async () => {
  const { canceled, data } = await ipcRenderer.invoke('open')
  if (canceled) return
  document.querySelector('#text').value = data[0] || ''
})

ファイルの書込

ファイル読込のソースに追加または修正を行う。

メインプロセス

main.js

ファイルを書き込む処理を追加する。

// 追加部分のみ抜粋
ipcMain.handle('save', async (event, data) => {
  const { canceled, filePath } = await dialog.showSaveDialog({
    filters: [
      { name: 'Documents', extensions: ['txt'] }
    ]
  })

  if (canceled) return

  fs.writeFileSync(filePath, data)
})

dialog.showOpenDialogとは異なり、dialog.showSaveDialogで取得できるパスは一つ(文字列)。

レンダラープロセス

index.html

保存ボタンを追加する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'unsafe-inline';"
    />
  </head>
  <body>
    <h1>Hello World!</h1>
    <div>
      <button id="open" type="button">Open</button>
      <button id="save" type="button">Save</button>
    </div>
    <div>
      <textarea id="text"></textarea>
    </div>
    <script src="index.js"></script>
  </body>
</html>

index.js

メインプロセスへファイルの保存を要求するメッセージを送信する処理を追加する。

// 追加部分のみ抜粋
document.querySelector('#save').addEventListener('click', async () => {
  const data =  document.querySelector('#text').value
  await ipcRenderer.invoke('save', data)
})

参考