electron-vue で dialog.showOpenDialog(...) しようとした時に出る厄介なエラーと対処法

# はじめに

electron-vueでWindows/macOS向けのアプリを作成することがあると思います

デスクトップ向けOSのアプリで欲しい機能といえば、おそらく 「ファイル(ディレクトリ)を開くダイアログ」 ではないでしょうか？ (少なくとも私はそうでした)

そんなときに、dialogを使用しようとすると出る厄介なエラーとその解決方法を記載します

※前提として、Typescriptで一部のコードが記載されています


## 元のコードと発生するエラー

### 元のコード

```MyComponent.vue
<template>
  <div class="mycomponent">
    <button @click="OpenDialog" />
  </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { remote } from 'electron

@Component
export default class AppCard extends Vue {
  private OpenDialog() {
    remote.dialog.showOpenDialog({defaultPath: '/', properties: ['openFile']})
  }
}
</script>
```

```src/background.ts
// :
// :
async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      enableRemoteModule: true,
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
    if (!process.env.IS_TEST) win.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    win.loadURL('app://./index.html')
  }
}
// :
// :
```

### 発生するエラー

`Uncaught ReferenceError: __dirname is not defined`

![スクリーンショット 2021-02-11 19.02.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/dca2fda5-f813-d7bd-144b-b4f499a21b67.png)

### 解決方法

```diff_typescript
// src/background.ts
// :
// :
 async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
       enableRemoteModule: true,
+      nodeIntegration: true
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
    if (!process.env.IS_TEST) win.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    win.loadURL('app://./index.html')
  }
}
// :
// :
```

### 解決した結果

一難去ってまた一難とはこのことだと感じます

![スクリーンショット 2021-02-11 19.02.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/7708c57b-9a64-7c68-9eb0-bb6f7f131086.png)

### さらに修正します

1. preloadの項目をwebPreferenceに追加し
2. preload.jsを対象のプロジェクトに追加し
3. preload.jsを記述し
4. 実際に `showOpenDialog` を呼び出す箇所を変更します

#### 1. preloadの項目をwebPreferenceに追加

```diff_typescript
// src/background.ts
// :
// :
async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      enableRemoteModule: true,
+      preload: path.join(__dirname, '..', 'src', 'preload.js'),
      nodeIntegration: true
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
    if (!process.env.IS_TEST) win.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    win.loadURL('app://./index.html')
  }
}
// :
// :
```

#### 2. preload.js をプロジェクトに追加 (パスは先ほど追加したコードの位置と合わせる必要があります)

![スクリーンショット 2021-02-11 19.20.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/e99b5281-066b-79ad-ada7-24bddfeba576.png)

#### 3. preload.jsを記述

```preload.js
const remote = require("electron").remote;
window.remote = remote;
```

#### 4. 実際に `showOpenDialog` を呼び出す箇所を変更

```diff_vue
<!-- MyComponent.vue -->
<template>
  <div class="mycomponent">
    <button @click="OpenDialog" />
  </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { remote } from 'electron

@Component
export default class AppCard extends Vue {
  private OpenDialog() {
-        remote.dialog.showOpenDialog({defaultPath: '/', properties: ['openFile']})
+        window.remote.dialog.showOpenDialog({defaultPath: '/', properties: ['openFile']})
  }
}
</script>
```

## 最終コード

```src/background.ts
// :
// :
async function createWindow() {
  // Create the browser window.
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      enableRemoteModule: true,
      preload: path.join(__dirname, '..', 'src', 'preload.js'),
      nodeIntegration: true
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string)
    if (!process.env.IS_TEST) win.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    win.loadURL('app://./index.html')
  }
}
// :
// :
```

```preload.js
const remote = require("electron").remote;
window.remote = remote;
```

```MyComponent.vue
<template>
  <div class="mycomponent">
    <button @click="OpenDialog" />
  </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { remote } from 'electron

@Component
export default class AppCard extends Vue {
  private OpenDialog() {
    window.remote.dialog.showOpenDialog({defaultPath: '/', properties: ['openFile']})
  }
}
</script>
```

## ただし

以下のように、tslintによって赤波線が立ちます
ので、windowに依存させない方法をとった方が良いかもしれません

![スクリーンショット 2021-02-11 19.27.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/10bdbf1c-bc87-177c-8509-fece6b11ff75.png)