Electronアプリをブラウザ上のURLから開き、URLに含まれた情報をレンダラープロセスで利用できるようにする実装方法をまとめます。(mac, windows用)
特にwindows用の実装の情報が少なく、苦戦したので、同じような状況の方の参考になりましたら幸いです。
環境
- electron v16(v12でも動作しました)
- electron-builder v22
サンプルリポジトリ
完成イメージ
-
custom-url://
のようなURLからアプリを起動し、画面にそのURLを表示することができる - アプリが既に起動している場合、していない場合も同じように動作する
実装ステップ
1. URLからアプリを起動する
URLの情報を受け取ることは考えず、とりあえずURLをクリックすることでアプリが起動するようにします。
1-1. electron-builder用の設定を追加
起動したいcustom schemeを決めて、 package.json
もしくは electron-builder.yml
に以下のように追記します。
今回の例では custom-url://
から始まるURLから起動できるようにします。
{
...
"build": {
...
"protocols": {
"name": "Custom URL Sample",
"schemes": [
"custom-url"
]
}
}
}
1-2. OSのレジストリにプロトコルを登録するコードを追加(windowsのみ)
windowsでは、上記の設定だけではアプリが起動されないため、 app.setAsDefaultProtocolClient
でプロトコルを登録する必要があります。
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を受け取ることができます。アプリが既に起動していても、していなくても同じように動作します。
const { app } = require('electron');
...
app.on('open-url', (_event, url) => {
// ここで第二引数にurlが渡ってくるため、次項でそれをレンダラープロセスに渡す
});
windows
A. アプリが起動していなかった場合
URLクリックによりアプリが起動した場合は、 process.argv
の配列の中にURLが含まれます。
const url = process.argv.find((arg) => arg.startsWith(`${CUSTOM_SCHEME}://`));
B. アプリが起動していた場合
windowsでは、アプリが起動している状態でURLをクリックすると、2つ目のアプリが重複して開こうとします。
そこで、 app.requestSingleInstanceLock()
を呼び出すことで、 app
が2つ目のアプリかどうかを確認し、かつその時のURLを1つ目のアプリでlistenしておくようにします。
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をまとめると以下のようになります。
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からアプリを起動した場合は、レンダラープロセスで受け取る準備ができていないため、それを待つ必要があります。
レンダラープロセス
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通信)
メインプロセス
簡略化のため、macの場合のみを例示します。
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つ目のウィンドウが一瞬表示されてしまうことを防ぐなど、やや工夫が必要でしたので、詳細は サンプルリポジトリ の方もご覧ください。
参考