🪐

Electronでパス取得にハマった話

2023/10/30に公開

Electronをパッケージ化したときに詰まった

nodeでパスを取得するとき、よく使うのは__dirnameかなと思います。
Electronでアプリケーションを作成するときに外部ファイルを__dirnameで取得していましたが
ビルドした瞬間上手く動作しなくて数時間ハマりました…。

解決方法を検証を含めて紹介します。

結論

先に結論から

  • __dirnameだとモジュールからみたディレクトリが起点となる。
  • path.resolve()だとアプリケーションを起動したディレクトリが起点となる。

外部ファイルを扱う場合は、path.resolve()で絶対パスを取得するのをおすすめします。
以下検証になります。

検証環境

Windows環境になります。

使用ライブラリ

  • electron
  • electron-builder

ソース全容

Electronのプロセス間通信のソースを基本にし、ボタンを押すとパスが返却されるハンドラーを作成しました。
それぞれ__dirnamepath.resolve()が返却されるようになっています。

main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

// __dirnameを返却するハンドラー
async function handleDirPath() {
  return __dirname
}

// path.resolve()を返却するハンドラー
async function handleResolvePath() {
  return path.resolve('.')
}

function createWindow() {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
    },
  })
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  ipcMain.handle('dir', handleDirPath)
  ipcMain.handle('resolve', handleResolvePath)
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

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

preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  dirPath: () => ipcRenderer.invoke('dir'),
  resolvePath: () => ipcRenderer.invoke('resolve'),
})
renderer.js
// __dirname
const dirBtn = document.getElementById('dirBtn')
const dirElement = document.getElementById('dirPath')
dirBtn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.dirPath()
  dirElement.innerText = filePath
})

// path.resolve
const resolveBtn = document.getElementById('resolveBtn')
const resolveElement = document.getElementById('resolvePath')
resolveBtn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.resolvePath()
  resolveElement.innerText = filePath
})
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
    <title>Path</title>
  </head>
  <body>
    <!-- __dirname -->
    <div>
      <button type="button" id="dirBtn">__dirname</button>
      Path: <strong id="dirPath"></strong>
    </div>
    <!-- path.resolve() -->
    <div>
      <button type="button" id="resolveBtn">resolve</button>
      Path: <strong id="resolvePath"></strong>
    </div>
    <script src='./renderer.js'></script>
  </body>
</html>

今回使用したソース一式はこちらに保管しています。
https://github.com/nowa0402/electron_path

検証

ビルド前

パッケージ化する前で検証します。

electron .

ボタンを押してパスを確認
ボタンの画像

どちらも同じパスが表示されました。

ビルド後

electron-builderでパッケージ化します。

electron-builder --win --x64 --dir

dist/win-unpacked/{file}.exeを起動。
ボタンを押してパスを確認します。

ボタンの画像2

  • path.resolve()はアプリケーションの起点ディレクトリ
  • __dirnameはパッケージ化されたresources/app.asar

が表示されました。

両者の違い

両者の違いはどこのディレクトリを参照しているかです。

  • dirnameは現在のモジュールのディレクトリパス
  • path.resolveは引数指定がない場合、現在の作業ディレクトリパス

__dirnameだとモジュールからみたディレクトリパスを返却します。
electron-builderはソースをdist/win-unpacked/resources/app.asar/の中に格納しているため
返却されるのはapp.asarまでのディレクトリを返却します。

ソースについてはこちらの記事が参考になります。
https://blog.katsubemakito.net/nodejs/electron/hack-insidecode

一方、path.resolve()は現在の作業ディレクトリパスを返却します。
ビルド前はnodeを起動したディレクトリ。
ビルド後はexeを起動したディレクトリが起点となります。

違いによって起こる問題

上記仕様によって、私が詰まったパターンを紹介します。
それは外部ファイルを取得する処理です。

例えば下記構成のアプリケーションがあったとします。

.
├── MyApp.exe
├── locales
├── resources
│   └── app.asar
└── conf
    └── setting.csv

設定ファイルをパッキングせず、外部から呼び出すパターンですね。
開発時はpath.join(__dirname, 'conf/setting.csv')で取得できますが
ビルドするとresources/app.asar/が起点になるため、外部ファイルを読み込めません。

この場合はpath.join(path.resolve(), 'conf/setting.csv')にしてあげると読み込むことができます。

まとめ

まとめです。

  • __dirnameだとモジュールからみたディレクトリが起点となる。
  • path.resolve()だとアプリケーションを起動したディレクトリが起点となる。

外部ファイルを操作するときはpath.resolve()でアプリケーションディレクトリ起点にするのがおすすめです。

参考文献リスト

Discussion