ElectronにURLクリックで直接操作する機能をつけてみる

Electronには、iTunesのitms-apps://をクリックすると、iTunesが起動して該当のアプリのページが開かれたり、Teamsのようにクリックひとつで該当チャンネルのチャット作成画面が出るような「URL Scheme」「ディープリンク」と呼ばれる機能が備わっています。

例えば、kanri://edit?junle=nyusya&recid=17といったようなURLをChrome等で開こうとすると、自作のElectronアプリが起動し、URLの引数を元に操作が可能になるわけです。今回これを装備してみたいと思います。

図:こんな感じの確認画面が出るようになる

今回使用する機能やライブラリ

今回の機能は、Electronの標準機能で実現可能なので、別途ライブラリのインストールは不要です。

ソースコード

コードはindex.jsの中に起動時に記述するだけです。但し、Windowsの場合とmacOSの場合とで少々異なる点があるのと、二重起動防止の措置を入れている場合にはちょっとテクニックが必要になります。

普通に使えるようにするパターン

//URL Schemeを設定
app.setAsDefaultProtocolClient('kanri', process.execPath);

//macOSの場合
app.on('open-url', (e, url) => {
    //URLスキーマクリック時のルーチン
    mainWindow.webContents.send('openscheme', url);
});



//Windowsの場合
var procman = process.argv;

//コマンドライン引数を取得してCustom URLがある場合、メインプロセスに渡す
procman.forEach(cmd => {
  // URIスキームのみを探して、レンダラプロセスに送る
  if (/kanri:\/\//.test(cmd)) {
    mainWindow.webContents.send('openscheme', cmd);
  }
});
  • macOSの場合はapp.onにてopen-urlをセットしておくだけで、URLを取得することが出来ます。
  • Windowsの場合、open-urlが使えない為、electronのコマンドライン引数から取得します。コマンドライン引数に自分が設定したURL Scheme文字(今回はkanri://)が含まれていたら、それを取り出してレンダラプロセスに送っています。
  • これだけで、基本はkanri://testといったURLをクリックすると、アプリが起動して引数をURLを取得可能。あとはURLを分解して引数からパラメータを取り出せばオッケー

二重起動防止を加えたパターン

アプリによっては多重起動をしてもらっては困るケースがあります。その場合、URL Schemeを使う場合には以下の配慮が必要です。

  • アプリが起動していない場合には、素直にURLを受け取って、起動時にURLに基づいて処理をさせる
  • アプリが起動している場合には、2回目に起動したアプリはapp.quitさせなければならない。しかし、パラメータは受け取って起動済みのappに渡したい

これを実現するには、以下のような処理で実現が可能です。

//二重起動の防止
const gotTheLock = app.requestSingleInstanceLock(
  (cmd, pwd) => {}
);

//app初期化
if (!gotTheLock) {
  console.log("二重起動防止");
  app.quit();
} else {
    app.on('second-instance', (event, cmdargs, workingDirectory) => {
        console.log("セカンドインスタンス")

        //コマンドラインからURL Schemeだけ取り出して送り込む
        cmdargs.forEach(cmd => {
            // URIスキームのみを探して、レンダラプロセスに送る
            if (/kanri:\/\//.test(cmd)) {
                //kanri://をhttps://に置き換える
                var ret = cmd.replace("kanri://","https://")

                //パラメータ取り出す
                var nameman = "junle";
                var junleman = getParam(nameman,cmd);

                //パラメータを取り出す
                nameman = "recid";
                var recidman = getParam(nameman,cmd)
                recidman = recidman.replace(/\//g, "")

                mainWindow.webContents.send('openUrlScheme', [junleman,recidman]);
            }
        });
        
        //起動済みのWindowにフォーカスを移動する
        if (mainWindow) {
          if (mainWindow.isMinimized()) mainWindow.restore()
          mainWindow.focus()
        }
    });

    app.on('ready', () => {
    
        //URL Schemeを設定
        app.setAsDefaultProtocolClient('kanri', process.execPath);
    
    });
}

  //初回起動完了後に実行
  mainWindow.webContents.on('did-finish-load', ()=>{
    mainWindow.show();
    var procman = process.argv;

    //コマンドライン引数を取得してCustom URLがある場合、メインプロセスに渡す
    procman.forEach(cmd => {
      // URIスキームのみを探して、レンダラプロセスに送る
      if (/kanri:\/\//.test(cmd)) {
        //kanri://をhttps://に置き換える
        var ret = cmd.replace("kanri://","https://")

        //パラメータ取り出す
        var nameman = "junle";
        var junleman = getParam(nameman,cmd);
        
        //パラメータを取り出す
        nameman = "recid";
        var recidman = getParam(nameman,cmd)
        recidman = recidman.replace(/\//g, "")

        mainWindow.webContents.send('openUrlScheme', [junleman,recidman]);
      }
    });
  });
  • gotTheLockでまず、インスタンスの状態を取得する
  • その値がfalseの場合(つまり、二重起動判定)には、app.quitで終わらせる
  • trueの場合(つまり一回目起動時)には、app.onのsecond-instanceにて二回目の起動時にパラメータだけ受け取るコードを実行しておく。これで、app.quitされても二重起動時にパラメータだけは受け取れるようになります。
  • その後既存のBrowserWindowをアクティブにしてあげる
  • また、app.setAsDefaultProtocolClientは、app.onのready時に実行しておきます。
  • 1回目起動時用のパラメータ受け取りはBrowserWindowのdid-finish-loadイベントで、開いた後にでも、コマンドライン引数で受け取るようにしておきます。

URLからパラメータを取り出す

kanri://edit?junle=nyu&recid=17といったURLから、junleの値とrecidの値をそれぞれ分解して取り出す必要があります。

//URLパラメータから各種値を取り出す
function getParam(name,url){
  // パラメータを格納する用の配列を用意
  var paramArray = [];

  // URLにパラメータが存在する場合
  name = name.replace(/[\[\]]/g, "\\$&");
  var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
      results = regex.exec(url);
  if (!results) return null;
  if (!results[2]) return '';

  //パラメータを返す
  return decodeURIComponent(results[2].replace(/\+/g, " "));
}

//kanri://のURLから実際に取り出す場合
//一旦kanri://をhttps://に置き換える
var ret = cmd.replace("kanri://","https://")

//パラメータ取り出す
var nameman = "junle";
var junleman = getParam(nameman,cmd);

//パラメータを取り出す
nameman = "recid";
var recidman = getParam(nameman,cmd)
recidman = recidman.replace(/\//g, "");  //パラメータ最後のスラッシュは削る

mainWindow.webContents.send('openscheme', [junleman,recidman]);
  • getParam関数を用意。もともとはhttps://から取り出す関数として使っていました。
  • kanri://は一旦、https://に変換しておきます。
  • 取り出したいパラメータ名をnameに、取り出す対象を第2引数にcmdやurlを入れると取り出せます。
  • パラメータによっては最後にスラッシュがついてるケースがあるので、これを除外する為にreplaceしています。
  • 取り出した値を配列にして、レンダラプロセス側に送り込んでいます

URLを短縮URLでラップする

前述の通り、URL SchemeのURLは、OutlookとかTeamsに投稿しても開けません。リンクが削除されたりリンクされていても特殊URLはオープンにしない仕様であるため。よって、このURLをux.nuを利用して一旦ラップしてからクリックして開かせる事は可能なので、kanri://なURLを短縮URLにして返すようにしてみます。これならば、URLをそのままOutlookやTeamsに投稿して開くことが可能なので、ワンクッションはありますが、便利にはなります。

//対象のレコードのURL Schemeをux.nuで短縮URLにして返す
ipcMain.on('sharerecord', function( event, table, recid){
  //URL SchemeのURLを構築
  var url = "kanri://edit?junle=" + table + "&recid=" + recid;
  
  //ux.nuへ投げて短縮URLに変換する
  geneShortUrl(url,function (ret){
    //valueを取得
    var result = ret.status;

    if(result =- "NG"){
      //エラーメッセージを出す
      //メッセージオプション
      var options ={
        type:'info',
        title:"エラー",
        button:['OK'],
        message:'共有用URLの生成ができませんでした。',
        detail:ret.value
      }

    }else{
      //クリップボードにコピー
      clipboardy.writeSync(ret.value);

      //終了メッセージを出す
      //メッセージオプション
      var options ={
        type:'info',
        title:"URLを生成",
        button:['OK'],
        message:'共有用のURLを生成しました。',
        detail:"共有するための短縮URLをクリップボードにコピーしました。"
      }
    }

    //表示する
    dialog.showMessageBox(null,options);
    return;
  });
});

//ux.nuへ短縮URL生成リクエスト
function geneShortUrl(args,callback){
  //引数データをurlencode
  var encode = encodeURIComponent(args);

  //エンドポイントURL
  var uxend = "http://ux.nu/api/short?url=" + encode;

  //返却用変数
  var retman = {};

  //オプション
  var options = {
    url: uxend,
    method: 'GET',
  }

  //requestにて新しいトークンを取得する
  request(options, function (error, response, body) {
    //ステータスコードを取得する
    if(error != null){
      //リクエストエラー
      console.log(error);
      retman.status = "NG";
      retman.value = error;
    }else{
      //URLを整形するためにreplace
      var cleaning = body.replace(/https\:/g , "" );
      cleaning = cleaning.replace(/\\/g, "");
      cleaning = cleaning.replace(/\//g,"");
      cleaning = cleaning.replace(/ux\.nu/g,"")

      //JSON Objectにする
      var json = JSON.parse(cleaning);
      var uxurl = "https://ux.nu/" + json.data.id;

      //短縮URLを返す
      retman.status = "OK";
      retman.value = uxurl;
    }

    //callbackする
    callback(retman);
  });
}
  • ipc通信でレンダラプロセス側からのリクエストにて、sharerecordで処理を行うように今回は作っています。
  • レンダラプロセス側からtableとrecidの2つの値をURLに繋げるようにしています。
  • geneShortUrl関数がメインの処理を行う関数になります。
  • URLはencodeURIComponentにてURLエンコードする必要があります。
  • 単純なrequestモジュールでGETにてux.nuのAPIに送り込みます。返り値はJSONです。
  • 送り込み先のエンドポイントURLは、http://ux.nu/api/short?url=ここにURLエンコードしたURLとなります
  • リクエストをなげたらデータが返ってきますが、返り値のURLの値ガオカシイので、このままだとJSON.parse出来ない。使うのは返り値のdata.idの部分なのですが正規表現でおかしな部分をreplaceしてクリーニングします。
  • 最後にuxurlとして整形して返します。ux.nuでラップした短縮URLが返ります。

ちなみに、ux.nuからの生の返り値は以下のような感じで返ってきます。

{
    "safebrowsing":{
        "safe":true,
        "THREAT_TYPE_UNSPECIFIED":false,
        "MALWARE":false,
        "SOCIAL_ENGINEERING":false,
        "UNWANTED_SOFTWARE":false,
        "POTENTIALLY_HARMFUL_APPLICATION":false,
        "is_success":true
    },
    "data":{
        "blacklist":"false",
        "malware":"false",
        "safe":"true",
        "id":"jvrRU",
        "url":"https:\/\/ux.nu\/jvrRU"
    },
    "new":"true",
    "status_code":"200"
}

注意点

クリックできない問題

URL Schemeは大変便利なのですが、例えばこのkanri://なURLをメールやTeamsで送ると、メールの場合Outlookだとリンクが削られたりします。Teamsの場合https://の場合と異なり、リンクがあってもクリックが出来なかったりします。

Chrome上で対象のリンクをクリックした場合に有効であるため、これらで送ってリンクをクリックできるようにするには、一段回挟む必要があります。それが短縮URLサービス。しかし、殆どの短縮URLサービスが独自のURL Schemeに対応していない中、ux.nuだけは対応していたので、現在はこれを自分はアプリ内で使っています。ux.nuはREST APIがあり、独自のURLを投げて短縮URLを取得することが可能であるので、Node.jsのRequestモジュールなどでHTTPリクエストして取得した短縮URLを、メールやTeamsなどに送るようにしています。

ウェブアプリケーションの場合はダイレクトにURL Schemeのリンクを張っておけば良いだけなので特に問題ないのですが、Electronなどで使う場合には注意が必要です。

一回起動しないと使えない問題

PCを起動して、対象のアプリを起動していない状態でURL Schemeをクリックしても、普通にGoogle検索されるだけで、確認ダイアログが出ません。一旦起動すると以降は起動していない場合でも、Custom URL Schemeは作動するのですが、一旦PCをシャットダウン後は再度同じことがおきます。

これは上記のソースコードだけだと、インスタントに使えるだけでシステムに登録されているわけではない為に起こります。WindowsであってもmacOSであっても同様に登録作業が必要です。これを解消し全くアプリが起動していない状態でも、iTunes同様にURL Schemeを利用可能にする為には、通常はelectron-builderを利用します。プロジェクトのpackage.jsonに以下のコードを追加します。

macOSの場合

//macOSの場合
"build": {
    "protocols": {
      "name": "Management Software for Jinji",
      "schemes": [
        "kanri"
      ]
    },
}
  • schemesに利用するスキーマ(今回はkanri://なのでkanri)を指定しておく

Windowsの場合

Windowsの場合には同じような処理がnsisでのマクロで指定はできるのですが、ややこしい。Custom URL Schemeはレジストリの登録値に基づいて処理をしているので、レジストリにエントリが追加できれば良いので、もっとも手軽な方法は

  • 別のインストーラでインストール後にレジストリエントリ登録の作業をやらせる
  • 対象のレジストリをエクスポートしておいて、Electron初回起動時にshellでも使って、コマンドプロンプトから登録する
  • Electronで直接レジストリの読み書きを初回起動時にでもやらせる

といった方法で良いかと思います。ちなみに、プロトコルと関連付けられたアプリの一覧は、設定⇒アプリ⇒既定のアプリ⇒プロトコルごとに既定のアプリを選ぶにて、確認することが可能です。ちなみに、以降のレジストリの作業自体は、app.setAsDefaultProtocolClient('kanri', process.execPath);が行ってるものと同じなので、手動で行う必要はありません。

図:プロトコルと関連付けられたアプリ一覧

図:mailto:の場合の登録事例

レジストリへの登録手順は以下の通りとなります。通常は、登録先となるのは、"HKEY_CLASSES_ROOT"や"HKEY_LOCAL_MACHINE\Software\Classes"の中に作成します。但し、こちらの場合、管理者権限を必要とするため、会社で制限を掛けている場合には登録が出来ません。その場合には、"HKEY_CURRENT_USER\Software\Classes\"のほうであれば、該当のユーザのみになりますが、登録することが可能になります。

今回は後者のHKCUのほうで作業を行います。

  1. レジストリエディタを起動して、HKEY_CURRENT_USER\Software\Classes\を開く
  2. この上で右クリック⇒「新規」⇒「キー」でまずキーを作成します。名前はurlschemeのkanriとしました。
  3. その下に「shell」のキーを作成、さらにshellの下に「open」のキーを作成、さらにその下に「command」のキーを作成する
  4. 根っこであるkanriをクリックして選び、右パネルの既定を編集。値には「url:kanri protocol」といったものを入れておく
  5. 同じく、新たに文字列値を作って、名前はURL Protocolとする。値は空でOK
  6. 3.のcommandを開いて、既定を編集します
  7. 通常は"C:\Users\ユーザ名\Documents\kanri-win32-x64\kanri.exe" "%1" といったようなフルパスに %1を加えたようなスタイルで記述を追加します。
  8. 7.の場合、exeにはURL Scheme全体が渡るので、「kanri://edit?・・・」のようなフルアドレスが引き渡されます

これをエクスポートすると以下のような感じになります。毎回このレジストリがクリアされるような環境の場合は、起動時に自動的にレジストリ登録するような仕掛けをしておくと良いでしょう。

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Classes\kanri]
"URL Protocol"=""
@="url:kanri protocol"

[HKEY_CURRENT_USER\Software\Classes\kanri\shell]

[HKEY_CURRENT_USER\Software\Classes\kanri\shell\open]

[HKEY_CURRENT_USER\Software\Classes\kanri\shell\open\command]
@="\"C:\\Users\\ユーザ名\\Documents\\kanri-win32-x64\\kanri.exe\" \"%1\""

関連リンク

ElectronにURLクリックで直接操作する機能をつけてみる” に対して2件のコメントがあります。

  1. 通りすがり より:

    macOSのURLスキームの実装が必要になり、検討もつかなくて困っていたので
    めちゃくちゃ助かりました!ありがとうございます。

    1. akanemaru2017 より:

      macOSとWindowsとで、書き方が違うケースがあるのが、Electronのちょっと面倒な所ですね。
      概ねクロスプラットフォームですが。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)