Electronでパス取得にハマった話
Electronをパッケージ化したときに詰まった
nodeでパスを取得するとき、よく使うのは__dirname
かなと思います。
Electronでアプリケーションを作成するときに外部ファイルを__dirname
で取得していましたが
ビルドした瞬間上手く動作しなくて数時間ハマりました…。
解決方法を検証を含めて紹介します。
結論
先に結論から
-
__dirname
だとモジュールからみたディレクトリが起点となる。 -
path.resolve()
だとアプリケーションを起動したディレクトリが起点となる。
外部ファイルを扱う場合は、path.resolve()
で絶対パスを取得するのをおすすめします。
以下検証になります。
検証環境
Windows環境になります。
使用ライブラリ
electron
electron-builder
ソース全容
Electronのプロセス間通信のソースを基本にし、ボタンを押すとパスが返却されるハンドラーを作成しました。
それぞれ__dirname
とpath.resolve()
が返却されるようになっています。
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()
})
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
dirPath: () => ipcRenderer.invoke('dir'),
resolvePath: () => ipcRenderer.invoke('resolve'),
})
// __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
})
<!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>
今回使用したソース一式はこちらに保管しています。
検証
ビルド前
パッケージ化する前で検証します。
electron .
ボタンを押してパスを確認
どちらも同じパスが表示されました。
ビルド後
electron-builder
でパッケージ化します。
electron-builder --win --x64 --dir
dist/win-unpacked/{file}.exe
を起動。
ボタンを押してパスを確認します。
-
path.resolve()
はアプリケーションの起点ディレクトリ -
__dirname
はパッケージ化されたresources/app.asar
が表示されました。
両者の違い
両者の違いはどこのディレクトリを参照しているかです。
- dirnameは現在のモジュールのディレクトリパス
- path.resolveは引数指定がない場合、現在の作業ディレクトリパス
__dirname
だとモジュールからみたディレクトリパスを返却します。
electron-builderはソースをdist/win-unpacked/resources/app.asar/
の中に格納しているため
返却されるのはapp.asarまでのディレクトリを返却します。
ソースについてはこちらの記事が参考になります。
一方、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