ElectronアプリをURLから起動する

Electronアプリをブラウザ上のURLから開き、URLに含まれた情報をレンダラープロセスで利用できるようにする実装方法をまとめます。（mac, windows用）

特にwindows用の実装の情報が少なく、苦戦したので、同じような状況の方の参考になりましたら幸いです。

# 環境

- electron v16（v12でも動作しました）
- electron-builder v22

# サンプルリポジトリ

https://github.com/K-kind/electron-custom-url-sample

# 完成イメージ

![キャプチャ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504515/8632eded-34d6-67da-4965-0b8e67b98e18.gif)


- `custom-url://` のようなURLからアプリを起動し、画面にそのURLを表示することができる
- アプリが既に起動している場合、していない場合も同じように動作する

# 実装ステップ
## 1. URLからアプリを起動する

URLの情報を受け取ることは考えず、とりあえずURLをクリックすることでアプリが起動するようにします。

### 1-1. electron-builder用の設定を追加

起動したいcustom schemeを決めて、 `package.json` もしくは `electron-builder.yml` に以下のように追記します。

今回の例では `custom-url://` から始まるURLから起動できるようにします。

```json:package.json
{
  ...
  "build": {
    ...
    "protocols": {
      "name": "Custom URL Sample",
      "schemes": [
        "custom-url"
      ]
    }
  }
}
```

### 1-2. OSのレジストリにプロトコルを登録するコードを追加（windowsのみ）

windowsでは、上記の設定だけではアプリが起動されないため、 `app.setAsDefaultProtocolClient` でプロトコルを登録する必要があります。

```js:main.js
const { app } = require('electron');
const CUSTOM_SCHEME = 'custom-url';
...

// 初回起動時のみ必要だが、既に登録された状態で呼び出しても問題ない
app.setAsDefaultProtocolClient(CUSTOM_SCHEME);
```

※ 初回起動時にはURLから開くことができなくても大丈夫な場合を想定しています。

## 2. メインプロセスでURLを取得する

起動時のURLを、まずメインプロセスで取得できるようにします。この実装はmacとwindowsで大きく異なります。

### mac

macでは、 `app` の `"open-url"` というイベントで簡単にURLを受け取ることができます。アプリが既に起動していても、していなくても同じように動作します。

```js:main.js
const { app } = require('electron');
...

app.on('open-url', (_event, url) => {
  // ここで第二引数にurlが渡ってくるため、次項でそれをレンダラープロセスに渡す
});
```

### windows

#### A. アプリが起動していなかった場合

URLクリックによりアプリが起動した場合は、 `process.argv` の配列の中にURLが含まれます。

```js:main.js
const url = process.argv.find((arg) => arg.startsWith(`${CUSTOM_SCHEME}://`));
```

#### B. アプリが起動していた場合

windowsでは、アプリが起動している状態でURLをクリックすると、2つ目のアプリが重複して開こうとします。

そこで、 `app.requestSingleInstanceLock()` を呼び出すことで、 `app` が2つ目のアプリかどうかを確認し、かつその時のURLを1つ目のアプリでlistenしておくようにします。

```js:main.js
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  // gotTheLockがfalseの場合、appは2つ目のアプリのインスタンスなので、即座に終了する
  app.quit();
} else {
  // 2つ目のアプリがrequestSingleInstanceLockを呼び出した時に、1つ目のアプリで発火される
  app.on('second-instance', async (_event, commandLineArgs) => {
    // 第二引数には、2つ目のアプリ起動時のprocess.argvと同じものが含まれる
    const url = commandLineArgs.find((arg) => arg.startsWith(`${CUSTOM_SCHEME}://`));
  });
}
```

A, Bをまとめると以下のようになります。

```js:main.js
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', async (_event, commandLineArgs) => {
    const url = commandLineArgs.find((arg) => arg.startsWith(`${CUSTOM_SCHEME}://`));
    // 次項でレンダラープロセスに渡す
  });

  const url = process.argv.find((arg) => arg.startsWith(`${CUSTOM_SCHEME}://`));
  // 次項でレンダラープロセスに渡す
}
```

## 3. レンダラープロセスにURLを渡す

あとはURLをレンダラープロセスに渡すだけです。

1つ注意点として、URLからアプリを起動した場合は、レンダラープロセスで受け取る準備ができていないため、それを待つ必要があります。

### レンダラープロセス

```js:index.js
const { ipcRenderer } = require('electron');

window.onload = () => {
  ipcRenderer.on('url-opened', (_event, url) => {
    document.getElementById('url').textContent = url;
  });

  ipcRenderer.send('window-ready', true);
};
```

※ 説明の簡略化のためレンダラープロセスで直接 `ipcRenderer` を利用していますが、本来はpreloadの利用が推奨されているようです。（参考: [ElectronでcontextBridgeによる安全なIPC通信](https://qiita.com/pochman/items/64b34e9827866664d436)）

### メインプロセス

簡略化のため、macの場合のみを例示します。

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

let mainWindow;
let windowReady = false;

const getMainWindowWhenReady = async () => {
  if (!windowReady) {
    await new Promise((resolve) => ipcMain.once('window-ready', resolve));
  }
  return mainWindow;
};

app.on('open-url', async (_event, url) => {
  const mainWindow = await getMainWindowWhenReady();
  mainWindow.send('url-opened', url);
});

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    ...
  });

  ipcMain.once('window-ready', () => {
    windowReady = true;
  });
});
```

※ ウィンドウの準備を待つ実装については、もっと良い方法があるかもしれません。

また、windowsの場合は、2つ目のウィンドウが一瞬表示されてしまうことを防ぐなど、やや工夫が必要でしたので、詳細は [サンプルリポジトリ](https://github.com/K-kind/electron-custom-url-sample) の方もご覧ください。

# 参考

https://www.electronjs.org/docs/v14-x-y/tutorial/launch-app-from-url-in-another-app