最新のNode.jsとElectron環境でkeytarを動かしてみるテスト

社内だけでしか使わないツールであるため、現在の環境のままでも問題ないといえば問題ないのですが、色々新しいAPIが追加されてる最新のNode.jsやElectronを使いたい。けれど、その特定のモジュールのビルドが最新版に対応していないので、アップデートできずにいるものの1つがKeytar。以前も以下のエントリーでとりあえずElectron 5.0.0 + Keytar 4.6.0で動くようになったのですが、今一度、最新環境で動作するのかテストしてみようと思いました。

SQLite3は最新版はElectron対応したので、特にリビルドせずにnpm installで動くようになったのですが、果たして・・・Keytarのgithubのページを見てみると、Electronに関する記述が追加されてたりするので、ちょっと期待はしているのですが。

electron@5.0.0でkeytar@4.6.0をWindowsで使う2020年版

今回使用する環境

環境整備

環境はWindows10 64bit Build 1909。コマンドプロンプトで作業が必要です。

Node.jsをインストール

インストーラが配布されているので、叩いてそのままインストールするだけ。とっても簡単。但し、社内利用の場合には、環境変数の設定をしないと社内プロキシを超えられないなどの問題が出るので、そのあたりのセットアップが一番難儀するかもしれません。プロキシ関係の設定は前述の2020年度版のページに記載してあります。

旧環境からのアプデだったからなのか、「npm WARN npm npm does not support Node.js v14.16.0」とエラーが出てしまったので、一旦その状態のままでnpm i -g npmを実行し、最新版にしました。プロジェクトフォルダを用意し、npm init -yでpackage.jsonを作成時にエラーは出なくなりました。

図:node -vでバージョン確認

Pythonをインストール

続いて、これまでネイティブビルドに必要であったPython最新版をダウンロードして、インストール。Add Python 3.9 to PATHにチェックを入れてインストールしています。python --versionでチェックした所、きちんと最新版になっていました。

※結果的には今回はPythonは不要でした。

図:もう2.xはサポート終了済みです

Electronをインストール

npm i -g electronにて、リリースされたばかりのv12をインストールしてみます。しかしエラーが。Electron 7.x以降はNode.jsの為に記述した環境変数のプロキシ設定をElectronの場合読みに行ってくれないようで、以下の環境変数を追加設定する必要がありました。プロキシ環境でなければこの問題にはぶつからないと思います。

追加する設定は以下の通り

ELECTRON_GET_USE_PROXY  - 値はtrueを指定
GLOBAL_AGENT_HTTP_PROXY - 値はhttpプロキシのアドレス(例:http://proxy.test.com:8080)
GLOBAL_AGENT_HTTPS_PROXY - 値はhttpsプロキシのアドレス(例:https://proxy.test.com:8080)

httpとhttpsに別れてない場合は、どちらも同じプロキシアドレスで通過できると思います。

図:3つの設定の追加が必要

図:無事にインストールできた

Keytarをインストール

インストール実行

さて、これで下準備が整ったので、プロジェクトフォルダに移動して、npm i keytarでインストールしてみます。インストール自体は簡単です。最新版のv7.4.0がインストールされて、package.jsonにも記載が追加されています。

図:インストール自体は簡単

図:package.jsonでバージョン確認

呼び出しテストをしてみる

本来はElectronで使う為にはここでnode-gypやelectron-rebuildを使ってのリビルド作業が必要です。しないままにindex.jsとhtmlを作って簡単なelectronアプリを作り、keytarの呼び出しをしてみたいと思います。

ソースコード

'use strict';

//標準モジュールの宣言
const electron = require('electron');
const { app, Tray, Menu, dialog, BrowserView, shell } = require('electron');
const BrowserWindow = electron.BrowserWindow;

//追加モジュールの宣言
const keytar = require('keytar');
var servicename = "notetest";

// メインウィンドウはグローバル宣言
let mainWindow = null;

//app初期化
app.on('ready', () => {
    //キャッシュを捨てる
    electron.session.defaultSession.clearCache(() => {});
    
    //keytarでセーブを実行
    keytarman();

    //メインウィンドを開く
    mainwindowopen();
});

//メイン画面を開くコマンド
function mainwindowopen(){
    // メイン画面の表示。ウィンドウの幅、高さを指定できる
    mainWindow = new BrowserWindow({
        'width': 1280,
        'height': 800,
        'autoHideMenuBar':true,
        'resizable':true,
        'fullscreenable':true,
        'fullscreen':false,
    });

    //初期ページの表示
    mainWindow.loadURL('file://' + __dirname + '/index.html');
}

//keytarで値を資格情報マネージャに書き込み
function keytarman(){
    //ID指定
    let uid = "tomato";
    
    //キーワードを保存する
    keytar.setPassword(servicename,uid,"goodtaste");
    
    //キーワードを取得してconsole出力
    let secret = keytar.getPassword(servicename,uid);
    secret.then((result) => {
        //console出力
        console.log(result);
    });
}
  • BrowserWindowを一個表示するアプリですが、index.html側は特に重要では有りません。
  • app.on の readyにてBrowserWindow表示とkeytarman関数を呼び出しています。
  • keytarmanでは特定のservice名とIDにてキーワードを保存し、直後にそのキーワードを探索して呼び出し、console表示させています。

注意点

keytarは非常に便利なのですが、情報がそれほど多くなく、嵌るポイントが結構あります。そのうちの一つに「Account is Required」というエラーがあり、最近嵌った事例ですと資格情報マネージャに登録するにあたって、サービス名とキー名はあるものの、格納する値がNullや空っぽの場合に発生するということがありました。

回避策は言うまでもなく、保存する前に変数の値がNullだったりundefined、空であるならばsetPasswordで保存しないようにしなければなりません。このあたりのハンドリングは自分で行わなければならないので、要注意です。

実行結果

今回、一切リビルド作業を行っていませんが、何事もなく動作しました。非常に素晴らしいことです。node-gypとelectron-rebuildの為にVisualC++やら、ビルドの為の変数設定等しなければならなかっただけでなく、このリビルドが最新環境では概ね失敗する(Node.js + ElectronのABI問題)。インストールするだけで安全に資格情報マネージャを利用することが出来るので、心置きなく最新版に移植できそうです。但し、Electron 5.x以降は、XSSの問題から、nodeintegrationが廃止されて、レンダラプロセス側でrequireができないため、jQueryなどを使う場合に問題が出ます。

preloadを使った手法に切り替えていく必要はあります。preloadを使った手法は以下のエントリーをご覧ください。

electronでkintoneのフォームブリッジを呼び出し操作する

図:無事に値を取り出せた

図:資格情報マネージャに値が入ってる

SQLite3も試してみた

これも自分がよく使う、SQLiteデータベースを操作する為のモジュールですが、以前はリビルドが必要でした。しかし、5.x系以降はリビルドせずに動作するようになったので、改めてインストールしてみたいと思います。

インストール実行

コマンドプロンプトから、npm i sqlite3を実行するだけです。一部ライブラリのWarningが出ましたが問題ないです。v5.0.2は問題なく稼働しています。

図:こちらもリビルド不要

ソースコード

前述のコードに以下のコードを追記し、db.sqlite3ファイルとテーブルを用意しています。

//SQLite3ファイル
const sqlite3 = require('sqlite3').verbose();
var dbfile = __dirname + '/db.sqlite3';
var db = new sqlite3.Database(dbfile);

//SQLiteファイルからOAuth情報を取得する
db.serialize(function(){
  var selectman = new Promise(function(resolve, reject){
    //SELECT文を発行する
    db.all('SELECT * FROM oauth where ID=1',function(err,rows){
      if(err){
        //メッセージを表示
        //メッセージオプション
        var options ={
          type:'info',
          title:"エラーメッセージ",
          button:['OK'],
          message:'OAuth認証情報取得エラー',
          detail:err.message
        }

        //表示する
        dialog.showMessageBox(null,options);
        return;
      }else{
        //取得したデータを変数に格納しておく
        var record = rows[0];

        //console表示
        console.log(record);
      }
    });
  });
});
  • 単純にdb.sqlite3のデータベース内にあるoauthテーブルから情報を取り出して、console.logで常時させてるだけのプログラムです。

実行結果

なんなく動きました。KeytarやSQLite3といったモジュールのように、今後ユーザ側でリビルドがすごく大変といったものについては、あらかじめ用意してくれてあると助かりますね。ハードルが非常に下がるのと、心理的にも楽です(バージョン依存でElectronのバージョン上げられないといったことがなくなるので)。

図:問題なくSQLiteファイルから読み出せた

Electronがv12になったことで

コレまでも段階的にいろいろな機能が廃止されて置き換えられていますが、大きな変更点はremoteが廃止された点と、BrowserWindowのwebPreferencesに於けるcontextIsolationがデフォルトでtrue、結果として、IPC通信の方法が旧方式ではなくcontextBridgeにしてくださいという変更があります(そのため、nodeIntegrationはデフォルトでfalse)。

そこで、IPC通信やレンダラ側でのrequireの現在をテストしてみました。

旧方式

Electron v5.0時代のような旧方式が現在でも使えるのかテストしてみました。しかしこの方法はセキュリティ面で現在は非推奨なので、社内オンリーで使うなどの「書き直しメンドイなぁ」というケース以外のプロダクトでは使うべきではないでしょう。

index.js側

//Node.js側とHTML側で通信をするモジュール
const ipcMain = require('electron').ipcMain;

//メイン画面を開くコマンド
function mainwindowopen(){
    // メイン画面の表示。ウィンドウの幅、高さを指定できる
    mainWindow = new BrowserWindow({
        'width': 1280,
        'height': 800,
        'autoHideMenuBar':true,
        webPreferences: {
            contextIsolation: false,
            nodeIntegration: true, 
        },
        'resizable':true,
        'fullscreenable':true,
        'fullscreen':false,
        'icon': __dirname + '/images/box.png',
    });

    //初期ページの表示
    mainWindow.loadURL('file://' + __dirname + '/index.html');
}

//テストIPC通信
ipcMain.on('testipc', function( event ){
    //処理
    console.log("test test");

    //レンダラ側に送る
    event.sender.send('testalert', "てすと");

    //windowを指定してレンダラ側に送る
    mainWindow.webContents.send('testalert', "テストですよ");
});
  • webPreferencesのcontextIsolationはfalseにしなければレンダラ側でrequireはできません。
  • webPreferencesのnodeIntegrationはtrueにしなければレンダラ側でrequireはできません。
  • 他、IPC通信としてevent.sender.sendwindow.webContents.sendも使えます。

index.html側

<!DOCTYPE html>
<html>
    <head>
        <title>管理アプリ</title>
        <script>
                var $ = jQuery = require("jquery")
        </script>
        <link rel="stylesheet" href="css/jquery-ui.css" />
        <script src="js/jquery-ui.min.js"></script>

        <script>
            //メインプロセスとIPC通信を行う
            const { ipcRenderer } = require('electron');

            //テスト
            ipcRenderer.send('testipc');

            //受信テスト
            ipcRenderer.on('testalert', function(event,arg) {
                  alert(arg);
                  return;
            });
        </script>
    </head>
    <body>

    </body>
</html>
  • jQueryもメインプロセス側でnpm installしたものをrequireして使うことになります。
  • これまで通り、ipcRendererはrequireして使えます。sendおよびonでそれぞれ送信・受信となります。

新方式

Electron v5.0以降は、旧方式をunsafeという事でpreload.jsを間に挟んで通信する方式をデフォルトとしました。そのためレンダラ側とメイン側で直接通信するのではなく、このpreload.jsを間に挟むようになったのですが、さらにElectron v12ではcontextBridgeという仕組みがデフォルトとなり、より安全な通信を推奨するようになりました。そのため、コードの書き方が少々異なります。

index.js側

//Node.js側とHTML側で通信をするモジュール
const ipcMain = require('electron').ipcMain;

//メイン画面を開くコマンド
function mainwindowopen(){
    // メイン画面の表示。ウィンドウの幅、高さを指定できる
    mainWindow = new BrowserWindow({
        'width': 1280,
        'height': 800,
        'autoHideMenuBar':true,
        webPreferences: {
            contextIsolation: true,
            nodeIntegration: false,
            worldSafeExecuteJavaScript: true,
            preload: __dirname + '/preload.js'
        },
        'resizable':true,
        'fullscreenable':true,
        'fullscreen':false,
        'icon': __dirname + '/images/box.png',
    });

    //初期ページの表示
    mainWindow.loadURL('file://' + __dirname + '/index.html');
}

//テストIPC通信
ipcMain.on('testipc', function( event, args ){
    //処理
    console.log(args);

    //レンダラ側に送る
    event.sender.send('testalert', args);

    //windowを指定してレンダラ側に送る
    mainWindow.webContents.send('testalert', args);

});
  • メインプロセス側での変更点は、webPreferencesの中。contextIsolationはtrue, nodeIntegrationはfalse, worldSafeExecuteJavaScriptをtrue, preloadにpreload.jsを追加しています。
  • これまでと異なり、preload.jsを別途用意します。
  • ipc通信周りは旧方式と変わりありません。

preload.js側

const { contextBridge, ipcRenderer} = require("electron");

contextBridge.exposeInMainWorld(
    "api", {
        //引数で受け取ってメインプロセスに投げる
        send: (channel, data) => {
            ipcRenderer.send(channel, data);
        },

        //メインプロセスから受け取りレンダラ側へ投げる
        on: (channel, func) => {
            ipcRenderer.on(channel, (event, ...args) => func(...args));
        }
    }
);
  • 間に挟まれてるpreload.jsはレンダラ、メイン双方の中間で橋渡しをするスクリプトになります。
  • sendがレンダラプロセス側からメインプロセス側へ送るのを担当、onがメインプロセスからレンダラプロセス側へ送るのを担当します。
  • 但し、公式ドキュメントによると、特にsendのような「汎用的な書き方」はunsafe扱いになっています(channelを引数で受け取って、メイン側を叩くのはよろしくないということ)
  • よって、本来はIPCの1処理に1個の関数を対応するように、preload.jsに記述するようにするのがより安全な書き方となります(処理の数だけ用意することになります)

index.html側

<!DOCTYPE html>
<html>
    <head>
        <title>管理アプリ</title>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
        <link rel="stylesheet" href="css/jquery-ui.css" />
        <script src="js/jquery-ui.min.js"></script>

        <script>
            //IPC通信用
            var ipc = window.api;

            //送信テスト
            ipc.send("testipc","テストです");

            //受信テスト
            ipc.on("testalert", (arg)=>{
                alert(arg);
                return;
            });
        </script>
    </head>
    <body>

    </body>
</html>
  • jQueryはメインプロセス側のを使用せず、通常のCDNの読み込みで利用可能です。
  • ipcRendererなどをrequireする事はできません。
  • 代わりに、preload.jsで規定した「api」をwindow.apiで参照可能なので、window.api.sendにてpreload.js内のsend関数を呼び出して、メインプロセス側にipcRenderer.sendさせます。
  • 逆にメインプロセス側からは通常通り送信されてきたものは、preload.jsが受け取るので、window.api.onで受け取る事が可能です。

新方式で複数の引数を渡したい場合

旧方式で、ipcRenderer.send("hostman",args1,args2)といったような複数の引数を渡していたようなケースの場合、新方式にそのままipc.sendで直しても引数を渡すことが出来ません。この場合、preload.jsおよびメインプロセス側は以下のように書き直します。ただし、メインプロセスからレンダラプロセスにevent.sender.sendするようなケースは引数を複数であってもそのまま渡せます。

index.html側

<!DOCTYPE html>
<html>
    <head>
        <title>管理アプリ</title>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
        <link rel="stylesheet" href="css/jquery-ui.css" />
        <script src="js/jquery-ui.min.js"></script>

        <script>
            //IPC通信用
            var ipc = window.api;

            //送信テスト
            var array = ["test",1200, "tomato"]
            var argman = "kinoko";

            //複数の引数を配列に組み直す
            var temparr = {
                   args: array,
                   args2:argman
            }
              
            //IPCでメインプロセスに送る
            ipc.send("testipc",JSON.Stringify(temparr));
        </script>
    </head>
    <body>

    </body>
</html>
  • 複数の引数は{}の連想配列にして、ipc.sendの第二引数にてJSON.stringifyで送る

preload.js側

const { contextBridge, ipcRenderer} = require("electron");

contextBridge.exposeInMainWorld(
    "api", {
        //引数で受け取ってメインプロセスに投げる
        send: (channel, ...args) => {
            ipcRenderer.send(channel, ...args);
        },

        //メインプロセスから受け取りレンダラ側へ投げる
        on: (channel, func) => {
            ipcRenderer.on(channel, (event, ...args) => func(...args));
        }
    }
);
  • sendにて、第二引数は可変長変数のレストパラメータにしておく

index.js側

//テストIPC通信
ipcMain.on('testipc', function( event, ...args ){
    //引数を分解
    var temp = JSON.parse(args[0]);

    //引数を取得
    var temparg1 = temp.args;  //1つ目の引数
    var temparg2 = temp.args2; //2つ目の引数
});
  • 引数はレストパラメータの...argsで引き受ける
  • 配列になってるので、あとはargs[0]でindex.htmlにてtemparrにしたarrayの値が取れ、args[1]でargmanの値が取れる。

dialogのmessageboxの押されたボタンを取得する

これまで、メインプロセス側からレンダラプロセス側にメッセージを表示する場合、dialog.showMessageBoxを使ってきましたが、最新のNode.jsの場合これだと押されたボタンを取得する前に次の処理に移ってしまい、処理が進んでしまいます。そこで使うのがdialog.showMessageBoxSyncを利用します。これでこれまでと同じように押されたボタンを判定してから次の処理に移るようになります。

//表示する
var ret = dialog.showMessageBoxSync(mainWindow,options);
if(ret == 0){
    //処理を続行する
}else{
    //処理を中断する
    event.sender.send('toast', "削除処理はキャンセルされました。");
    return;
}

関連リンク

コメントを残す

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

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