読者です 読者をやめる 読者になる 読者になる

リズムのじかん

javascript、typescriptなど中心に書きます。

ElectronアプリをカスタムURIで起動する

はじめに

webブラウザからリンクをクリックして、KindleiTunesを起動したことありますよね。

このときリンク先アドレスは以下のようになっています。

  • kindle://home/?action=refresh
  • itmss://itunes.apple.com/jp/album/liang-cheng-bai/id1065727732

カスタムURIですね。

自分で作ったElectronアプリもカスタムURIで起動したいと思います。 完全なソースはGitHubにあります。

github.com

こんな感じで動きます。

f:id:chords:20160221020141g:plain

カスタムURIの登録

MacWindowsに分けて書きます。

MacでのカスタムURIの登録

Macの場合は、アプリのinfo.plistにCFBundleURLNameを登録することで、カスタムURIでアプリを起動できるようになります。 https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html

具体的にElectronアプリにどう組み込むかは簡単で、electron-packagerのオプション(protocol-name, protocol)にカスタムURIを指定するだけです。

// package.json
{
  ...
  "scripts": {
    ...
    "pack": "npm run pack:osx && npm run pack:win32 && npm run pack:win64",
    "pack:osx": "electron-packager ./dist \"MyApp\" --out=dist/osx --platform=darwin --arch=x64 --version=0.36.8 --protocol-name=\"myapp-protocol\" --protocol=\"myapp\"",
    "pack:win32": "electron-packager ./dist \"MyApp\" --out=dist/win32 --platform=win32 --arch=ia32 --version=0.36.8,
    "pack:win64": "electron-packager ./dist \"MyApp\" --out=dist/win64 --platform=win32 --arch=x64 --version=0.36.8,
    ...
  },
  ...

パッケージ後の出力は以下のようになります。

// MyApp.app/Contents/info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    ...
    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleURLName</key>
        <string>myapp-protocol</string>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>myapp</string>
        </array>
      </dict>
    </array>
  </dict>
</plist>

WindowsでのカスタムURIの登録

Windowsの場合はレジストリにカスタムURIを登録する必要があります。 https://msdn.microsoft.com/ja-jp/library/aa767914(v=vs.85).aspx

レジストリ登録(削除)のタイミングは以下がいいですね。

インストールスクリプト作成

まずNSISでインストールスクリプトを作成します。 テンプレートはhttps://github.com/loopline-systems/electron-builder/blob/master/templates/win/installer.nsi.tpl にあるので、カスタマイズしましょう。

// installer.nsi.tpl
!define APP_NAME "<%= name %>"
...
# default section start
Section
  SetShellVarContext all
  ...
  DetailPrint "Register MyApp URI Handler"
  DeleteRegKey HKCR "myapp"
  WriteRegStr HKCR "myapp" "" "URL:myapp"
  WriteRegStr HKCR "myapp" "URL Protocol" ""
  WriteRegStr HKCR "myapp\DefaultIcon" "" "$INSTDIR\${APP_NAME}.exe"
  WriteRegStr HKCR "myapp\shell" "" ""
  WriteRegStr HKCR "myapp\shell\Open" "" ""
  WriteRegStr HKCR "myapp\shell\Open\command" "" "$INSTDIR\${APP_NAME}.exe %1"
  ...
SectionEnd

# create a section to define what the uninstaller does
Section "Uninstall"
  ...
  DetailPrint "delete MyApp URI Handler"
  DeleteRegKey HKCR "myapp"
SectionEnd

"myapp\shell\Open\command"の行を見てもらうとわかると思いますが、 カスタムURIで起動した場合、アプリには起動引数としてURIを渡します。

electron-builderの設定

次にelectron-builderのconfigファイルを用意します。nsiTemplateに先ほど作成したinstaller.nsi.tplを指定します。

// builder.json
{
  "osx" : { ... },
  "win" : {
    "title" : "MyApp",
    "icon" : "resources/win/icon.ico",
    "nsiTemplate" : "resources/win/installer.nsi.tpl"
}

あとはelectron-builder実行時にbuilder.jsonを指定するだけです。

// package.json
{
  ...
  "scripts": {
    ...
    "installer": "npm run installer:osx && npm run installer:win32 && npm run installer:win64",
    "installer:osx": "electron-builder \"dist/osx/MyApp-darwin-x64/MyApp.app\" --platform=osx --out=\"dist/osx\" --config=builder.json",
    "installer:win32": "electron-builder \"dist/win32/MyApp-win32-ia32\" --platform=win --out=\"dist/win32\" --config=builder.json",
    "installer:win64": "electron-builder \"dist/win64/MyApp-win32-x64\" --platform=win --out=\"dist/win64\" --config=builder.json",
    ...
  },
  ...

インストール後のレジストリは以下のようになります。

f:id:chords:20160221102147p:plain

アプリ側で起動URIをハンドル

起動URIのハンドルの仕方もMacWindowsで少し違います。

イベントリスナー登録

  • Macの場合は、Electron.appの"open-url"イベントで起動URLを取得できます。
  • Windows(とLinux)の場合は、起動引数で起動URLを取得できます。
// main.ts or main.js
import {app} from 'electron';
import {ready, handleOpenUri} from './mainWindow';

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

app.on('ready', ready);

app.on('will-finish-launching', () => {

  // For OSX
  app.on('open-url', (e, url) => {
    e.preventDefault();
    handleOpenUri(url);
  })

  // For Windows
  process.argv.forEach(arg => {
    if (/myapp:\/\//.test(arg)) {
      handleOpenUri(arg);
    }
  })
});

ポイントは https://github.com/atom/electron/blob/master/docs/api/app.md#user-content-event-will-finish-launching に記載されている通り、"will-finish-launching"イベントで"open-url"のイベントリスナーを登録することです。

ここまでは特に難しくないですね。

注意すること

ここで少し注意が必要です。

前提

Electron.appのイベントの発生順序は以下となります。(Macの場合)

  1. will-finish-launching
  2. open-url
  3. ready

BrowserWindow(≒画面、Rendererプロセス)のインスタンス作成はreadyイベントで行います。 ちなみにreadyイベントより前ではBrowserWindowのインスタンスを作成できません。

問題

open-urlイベントで起動URIを取得したときに、画面の表示を変えようとMainプロセスからRendererプロセスに通信すると、 BrowserWindowのインスタンスがまだできてないので、大抵ぬるぽで落ちます。

Windowsの場合も、will-finish-launchingイベントの中で同期的に起動URIを取得するので、同じことが起きます。

対策

サンプルでは、"BrowserWindowの準備完了"と"起動URIの取得"をそれぞれPromise化し、Promise.allで同期をとって(両方の処理が完了して)から通信するようにしています。

https://github.com/masahirompp/electron-open-url-sample/blob/master/appsrc/main/main.ts (コードが少し冗長な感じがする)

現実的には、 起動URIを取得したあとに(Mainプロセスで)サーバから必要なデータを取得している間に、 Rendererプロセスの準備ができていることがほとんどだと思います。 なので、どこまでやるかは状況に合わせて決めてください。

その他

AutoUpdaterでレジストリを書き換える方法は検証していません。

まとめ

Electronはwebと親和性の高いフレームワークです。 そのうちwebとシームレスに使えるアプリとか出てくる気がしています(・∀・)

Enjoy Electron!!