electronでpuppeteerを使ってみることにした
すでに社内で、electronで作成した座席表アプリを配布済みです。この環境下でpuppeteer + nexeでバイナリ配布をするのも良いのですが、すでにある座席表アプリでpuppeteerを動かせないかな?と思い挑戦しました。
ただし、keytarも使わなければ行けない環境であるため、これまでのelectron 3.0.0 + keytar 4.2.1の環境では古すぎてpuppeteerが動きません(Error : Protocol errorが出て、getBrowserContexts wasn't foundと出ます)。そこで、今回electronを5.0にし、keytarを4.6.0でビルドし直した所うまく動いたので、これを元に進めたいと思います。
目次
今回使用するモジュール等
ちなみに、あえてpuppeteer-in-electronを使わずとも、puppeteer-coreのみでインストール済みのChromeを起動させて、Chromeを操縦することは出来るので、puppeteer-in-electronを使わなくとも問題なく構築は可能です(実際自分は、puppeteer-coreのみでelectronからChromeを操縦させてます。
事前準備
Keytarのインストール
Node.jsはv10.15.1を利用しています。リビルド環境の構築はWindows向けで構築しています。electron-rebuildは最新版に入れ直しています。keytarのインストールは以下のような手順です。
npm i keytar@4.6.0 cd node_modules cd keytar node-gyp configure cd ..¥..¥ electron-rebuild -w -keytar -p -f
Puppeteerをインストール
electron自体がChromiumなので、別途Chromiumは必要ありません。ということで、今回もpuppeteer-coreを利用します。また、electronのBrowserWindow内で使いたいので、puppeteer-in-electronもインストールします。
npm i puppeteer-core npm i puppeteer-in-electron
electron v5.0.0はpuppeteerから操作が可能なので、今回のようなアプリを作る場合には、electronのバージョンは5以上推奨です(ただし、keytarを使うとなると簡単に最新版という事にはいかないのが残念)。
ソースコード
Puppeteer単体での場合
puppeteer + node.jsとは異なりpuppeteer-in-electronの場合冒頭の部分が少々異なります。その部分以降はこれまでのawaitから始まる命令文をつなげていけば良いので、そこだけ要注意です。また今回は、keytarとの併用ができるかの確認の為に、main()につなげて無駄にパスワードの保存と取り出しを記述しています。
//モジュールの読み込み
const {BrowserWindow, app} = require("electron");
const pie = require("puppeteer-in-electron")
const puppeteer = require("puppeteer-core");
const keytar = require('keytar');
//puppeteerで操作するメインの処理
const main = async () => {
//puppeteer-in-electronに接続
const browser = await pie.connect(app, puppeteer);
//URLを指定してBrowserWindowに対してページの移動をさせる
const window = new BrowserWindow();
const url = "https://officeforest.org/wp/";
await window.loadURL(url);
//viewportのサイズの指定
const viewportHeight = 800;
const viewportWidth = 1280;
//ページを表示する
const page = await pie.getPage(browser, window);
//ここから通常のpuppeteerと同じような記法になります。
await page.setViewport({
width: viewportWidth,
height: viewportHeight
});
//スクリーンショットを撮って保存する
await page.screenshot({ path: './result.png', fullPage: true})
//BrowserWindowを閉じる
await window.destroy();
};
//サービス名を構築
var servicename = "zaseki_sakura";
//puppeteerを動かす
main()
.then(function(){
//パスワードを保存する
keytar.setPassword(servicename,"sakura","password");
console.log("OK")
return "sakura";
})
.then(function(data){
const secret = keytar.getPassword(servicename,data);
secret.then((result) => {
console.log(result);
return;
});
})
- main関数がpuppeteerで自動操縦させる関数になります。
- main関数冒頭のpuppeteerを起動する部分が異なります。pie.connect(app, puppeteer);にてbrowserを宣言
- executablePathなどは不要です。BrowserWindowがそれを担います。
- page.gotoは冒頭は不要です。BrowserWindowに対してloadUrlを指定でロードさせればOK
- pie.getPage以降は通常のpuppeteerでの記法と同じになります。
- Windowを閉じる場合には、electronなのでwindow.destroy()で閉じればOKです。
- main関数につづけて、promiseにてkeytarによる資格情報マネージャへのパスワードの格納/取得を追加して動作確認しています。
- electron 5.0.0 + keytar 4.6.0での動作確認が取れましたので、今後はこのバージョンを基準に開発していきたいと思います。
他のアプリ内に取り込んで使う場合
puppeteer-in-electronを使う上でちょっと厄介だったのが、既存のアプリに組み込んで使いたい場合。単体と違い、他のコードが存在し、それが原因でエラーが出たりします。特に厄介なのは、Must be called at startup before the electron app is ready.というエラー。app.onのreadyよりも前に、browserについて初期化しておいて、puppeteerに接続させておかなければなりません。よって、以下のような形で記述します。
//puppeteerモジュール読み込み
const pie = require("puppeteer-in-electron")
const puppeteer = require("puppeteer-core");
var browser = null;
var page = null;
//app.onより前にpuppeteerに接続させる
const main = async () => {
browser = await pie.connect(app, puppeteer);
};
main();
//appの初期化
app.on('ready', () => {
});
//puppeteerで操作する
async function openform(tempid,frmname,frmmail){
const window = new BrowserWindow({
'width': 950,
'height': 750,
'autoHideMenuBar':true,
webPreferences: {
nodeIntegration: false,
contextIsolation: false,
webSecurity: false
},
'resizable':true,
'fullscreenable':false,
'fullscreen':false,
'alwaysOnTop':false,
'modal':true,
'parent': mainWindow //モーダル表示にしておく
});
const url = "コントロールするサイトのURL";
await window.loadURL(url);
page = await pie.getPage(browser, window);
//viewportをフルサイズにする
await page._client.send('Emulation.clearDeviceMetricsOverride');
//自動入力
await page.waitForSelector('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner')
await page.click('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner')
await page.type('.ui > .row:nth-child(1) > .column > .el-input > .el-input__inner', tempid)
await page.waitForSelector('.ui > .ui > .ui > .row:nth-child(1) > .column')
await page.click('.ui > .ui > .ui > .row:nth-child(1) > .column')
await page.waitForSelector('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner')
await page.click('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner')
await page.type('.ui > .row:nth-child(2) > .column > .el-input > .el-input__inner', frmname)
await page.type('.ui > .row:nth-child(3) > .column > .el-input > .el-input__inner', frmmail)
}
- app.onのready前にbrowserとpageを宣言しておいて、browserに対してconnectさせて置かなければならない。
- フォームに入力させる場合は、自動化よりも前に予め入力予定のワードはどこかで取得させておくとスムーズです。
- kintoneのフォームブリッジは、ちょっと変な仕様で、直接機械的にinputに対して値をねじ込むと、入っていても送信時に「入っていない」と言われるので、このようにpuppeteerにて入力させ、keydownイベントを発火させる必要があります。
- viewportを常にウィンドウサイズに合わせるには、await page._client.send('Emulation.clearDeviceMetricsOverride');にて実現可能です。
- tempid, frmname, frmmailは予め自分の場合、MySQLから取得したものを格納してあります。
- BrowserWindowが開かれるとPuppeteerが発動し、社員番号と名前、メールアドレスがフォームに自動入力されます。
- webviewタグ表示の対象に対して、javascriptでキーコードを送っても反応しないので、Puppeteerでページ表示させて操作する方法がもっともベターです。
jQueryを使えるようにする
Puppeteerの非常に優れている点は、既存のフォームなどに対して後から、jQueryなどのモジュールを読み込ませて、機能を付け足すことが出来る点です。例えば、formブリッジのテキストボックスに対して、jQueryのAutocompleteを付け足すことで、フォームでの入力補助機能をアップさせたりなどなど。
今回、GoogleのCDNで配布されているライブラリを用いて、formブリッジのテキストボックスにautocompleteを設定してみました。
※Autocompleteは文字列でなければいけないので、xlsxモジュールなどでデータを生成する場合、String()で型変換しておくことが必要です。
・・・・前略・・・・
//外部ライブラリ挿入
await page.addScriptTag({url: 'https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js'});
await page.addScriptTag({url: 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'});
await page.addStyleTag({url: 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css'});
await page.addStyleTag({path: 'css/autocomp.css'});
//autocomplate
const _tmp = await page.evaluate(() => {
var data = [
{label:'しいたけ', value:'2012'},
{label:'しめじ', value:'2013'},
{label:'まつたけ', value:'2014'},
{label:'なめこ', value:'2015'},
{label:'えのきだけ', value:'2016'},
{label:'たもぎたけ', value:'2017'},
];
//formに対してNAME属性を加える
var forms = document.forms[0].setAttribute("name","kinoko");
//4つ目の項目に対してid属性を加える
document.kinoko.elements[3].setAttribute("id","salad");
//autocomplateを加える
$('#salad').autocomplete({
source: data,
autoFocus: true,
delay: 500,
minLength: 2
});
});
- addScriptTagにてURL指定でスクリプトライブラリを挿入可能です
- addStyleTagにてURL指定でスタイルシートを挿入可能です。
- page.evaluateにて、任意のスクリプトを対象のページに対して実行させることが可能です。
- あとは、通常のHTML上で記述するようにJavaScriptを記述すればOK.
- evaluate内に外側の変数を渡したい場合には、以下のように引数で渡すと利用すことが可能になります。
- xlsxモジュールなどで、固定場所にあるexcelデータからautocomplete用のデータを生成しておき、引数でevaluateに渡せば最新のリストをもってautocompleteが反映可能です。
var data = [
{label:'しいたけ', value:'2012'},
{label:'しめじ', value:'2013'},
{label:'まつたけ', value:'2014'},
{label:'なめこ', value:'2015'},
{label:'えのきだけ', value:'2016'},
{label:'たもぎたけ', value:'2017'},
];
//data変数を引数として渡し、evaluate内で利用する
const _tmp = await page.evaluate((data) => {
//formに対してNAME属性を加える
var forms = document.forms[0].setAttribute("name","kinoko");
//4つ目の項目に対してid属性を加える
document.kinoko.elements[3].setAttribute("id","salad");
//autocomplateを加える
$('#salad').autocomplete({
source: data,
autoFocus: true,
delay: 500,
minLength: 2
});
},data);
図:formブリッジに対してautocompleteが反映出来た
単一実行ファイルを作成する
Node.js 18よりSingle executable applicationsという機能が装備され、標準で単独実行ファイルが作成できるようになりました。結果pkgはプロジェクト終了となっています。よって、以下のエントリーの単一実行ファイルを作成するを参考に、Node18以降はexeファイルを作成することが可能です。
関連リンク
- build problems with electron 5 beta #173
- 【node.js入門】Promiseによる非同期処理の使い方まとめ!
- Electron Vue + Puppeteerで作る便利デスクトップApp
- Electron x Puppeteer
- Electronとpuppeteerを用いてRadikoをMP3化するソフトを製作しました
- CarloでWeb制作者が手軽にデスクトップアプリを作る
- jQueryのAutoCompleteを使って自動補完される検索ボックスを作る方法
- JavaScriptの「match」について
- jQuery UI Autocompleteでかなを入力した際に漢字を表示
- jQueryUI の Autocomplete でサジェスト表示と選択時の表示を変える方法


