UiPath (ja) Advent Calendar 2度目の登場です。すみませんすみません。
2日目では、UiPathでJavaScriptを活用する方法を長々と書かせていただきました。
もし、UiPathでJavaScriptを活用するイメージにまだピンと来ていないようでしたらそちらからお読みください。
TL;DR
- いつ処理が終わるかわからないことを非同期処理と呼ぶ(正確な説明は割愛します)
-
Inject JS Script
アクティビティで実行したJavaScriptの非同期処理をUiPath側で認識するために、Find Element
(要素を探す)アクティビティなどを利用する - UiPath側に「処理が終わったことを認識させるための要素」をJavaScriptで作り出してDOMに出力する
- UiPath側が要素を発見できれば、それは非同期処理の終了を検知したことと同義である
はじめに
- UiPathにJavaScriptの非同期処理終了を認識させる時のお話です
- Webサイトの中に登場するUIのことを、本記事では要素と呼称します
-
Inject JS Script
(JS スクリプトを挿入)アクティビティを利用します - 2日目の記事同様、この記事はJavaScriptメインの記事ではなくUiPathで難しいことをJavaScriptで解決しよう、という記事です
- RPAビギナーによる記事のため、誤りやより高速で安全な方法がありまいたらご指摘ください
- 対象読者は、UiPathでJavaScriptを活用しているRPAエンジニアの皆さんです
非同期処理とは
通常JavaScriptコードは、書かれた順に必ず動作します。
console.log(1); // > 1
console.log(2); // > 2
console.log(3); // > 3
何回実行しても、「1, 2, 3」の順番でコンソールに出力されるはずです。
この時「undefined」というメッセージも出力されるかもしれませんが、それは今回関係ないので無視しておきましょう。数字の出力順序だけに注目してください。
次のコードも試して見ましょう。
var hoge = function () {
console.log(2); // > 2
};
console.log(1); // > 1
hoge(); // コンソールに「2」と出力する関数の呼び出し
console.log(3); // > 3
こちらも先ほど同様、何回実行しても「1, 2, 3」の順番でコンソールに出力されます。
では次のコードはどうでしょうか?
var hoge = function () {
setTimeout(function () {
// 100ミリ秒経った!
console.log(2); // > 2
}, 100);
};
console.log(1); // > 1
hoge(); // コンソールに「2」と出力する関数の呼び出し
console.log(3); // > 3
先ほどとは異なり、「1, 3, 2」という順に数字が出力されたのではないでしょうか?
このように呼び出した順番通りに処理が行われるものを「同期」、そうではないものを「非同期」と呼びます(※これはざっくり解釈です)。
例で使っているsetTimeout()
という関数は、delay
(遅延処理)アクティビティと同じ役割を持ち、指定した時間分(今回は100ミリ秒)待ってから受け取った関数を実行するものです。
そういったケースでは、もともとInject JS Script
アクティビティの仕様にある「return
文を利用してUiPathに結果を文字列で返す」という処理を実現することができません。return
をするタイミングではまだ100ミリ秒経っていないためです。
単純に別々のJavaScriptに分けて実行タイミングをdelay
アクティビティで100ミリ秒まってあげれば良さそうですが、非同期処理の多くはこの例とは異なりいつ終わるかがはっきりしません。
JavaScriptによるファイルの取得などの終了タイミングはネットワーク環境に依存しますから、厳密に何秒待てばいいのかは誰にもわからないのです。
そこでUiPathでもJavaScriptの非同期処理が「どれくらいで終わるだろう」という予想による不安定な遅延ではなく、より安全に「非同期処理が終わっているのか」を知る方法が必要になってきます。
UiPathでJavaScriptの非同期処理を待ってみよう
例として、開いたページの画像でリンク切れが発生していないかをフロントエンド側から検査するRPAを組んでみましょう。(サーバ側のエラーログ?なんですかそれは)
簡易的な例として、フローは次のようなものを想定します。
- ページ内の全ての画像を表示している
img
要素を取得 - 1つ1つの
img
要素のsrc
属性に書かれているパスへJavaScriptでアクセスしてみる - UiPath側に「読めない画像の有無」を伝えて制御を戻す
まずは、読み込めていない画像がいるかどうかを判定するコードを書いてみます。
※ 本記事ではPromise
やfetch
に関する説明は割愛します。
var iterable = []; // Array<Promise>
// 画像の数だけ fetch (非同期的に画像ファイルにアクセスする) する
document.querySelectorAll('img').forEach(function (img) {
var src = img.src;
iterable.push(fetch(src).then(function(res) {
if (!res.ok) {
throw new Error(); // 読めていなかった場合エラー判定にする
}
}));
});
Promise
.all(iterable) // iterable の中の [object Promise](fetchが終わるまで待機する約束) が全部終わっているか
.then(function () {
window.alert('全部読めた'); // 問題なく終わった
})
.catch(function () {
window.alert('読めていない画像がある'); // 少なくとも1つ以上のエラーが吐かれた
});
全ての画像が読めていれば、「全部読めた」と表示されます。
意図的に、画像をリンク切れさせて走らせてみる
インスペクタ(ブラウザの開発者ツールにあるDOMツリーを操作できるパネル)から、適当なimg
要素を探し出し、src
属性の値を存在しないパスに書き換えて先述のコードを実行して見ましょう。
このように、読めていない画像がページ内に存在していると「読めていない画像がある」とダイアログが出ると思います。
UiPathが、JavaScriptの非同期処理の終了を検知できるように手がかりを作ってあげる
ページ内の全ての画像を表示しているimg
要素を取得1つ1つのimg
要素のsrc
属性に書かれているパスへJavaScriptでアクセスしてみる- UiPath側に「読めない画像の有無」を伝えて制御を戻す
ここまでの間で3ステップのうち2つまでは解決できました。いよいよ非同期処理の完了をUiPathに伝えましょう。
UiPathにはFind Element
(要素を探す)アクティビティやGet Text
(テキストを取得する)アクティビティなどの、Webページ内の要素を探すアクティビティが用意されています。
これまでの例のように、JavaScript側では「非同期処理が終わったタイミングで何かをする」というのが容易なので、非同期処理終了時に1つの要素を作り出して、DOMツリーに出力します。
その要素をUiPathに探させて、見つけることができたら、UiPathは非同期処理の終了を理解することができます。
先ほどのJavaScriptコードの「window.alert
」の行を、要素の出力に書き換えるだけですね。
どちらも同じようなコードになりますから、一箇所に関数をまとめて書いておきます(exportResult()
)。
前回同様、UiPathがInject JS Script
アクティビティで読み込めるように無名関数にしておきましょう。
// 最終形態
function () {
var iterable = []; // Array<Promise>
var exportResult = function (message) {
// input要素を作る。まだページには追加されてない
var input = document.createElement('input');
// UiPathのセレクタで見つけやすいIDを付与する
input.id = 'RPA_VALUE';
// 引数で受け取った文字列を値に入れる
input.value = message;
// 目で見てわかりやすいようにスタイリングする(任意)
input.style.cssText = 'font-size: 20px; padding: 10px; position: relative; z-index: 9999; background: #fff; color: #333; box-sizing: border-box; width: 100%;';
// ページの先頭に追加する
document.body.prepend(input);
};
// 画像の数だけ fetch (非同期的に画像ファイルにアクセスする) する
document.querySelectorAll('img').forEach(function (img) {
var src = img.src;
iterable.push(fetch(src).then(function(res) {
if (!res.ok) {
throw new Error(); // 読めていなかった場合エラー判定にする
}
}));
});
Promise
.all(iterable) // iterable の中の [object Promise](fetchが終わるまで待機する約束) が全部終わっているか
.then(function () {
exportResult('全部読めた'); // 問題なく終わった
})
.catch(function () {
exportResult('読めていない画像がある'); // 少なくとも1つ以上のエラーが吐かれた
});
}
}
あとは、このJavaScriptをInject JS Script
アクティビティで実行後、次ようなセレクタを用いて、任意のアクティビティでUiPathにDOM探索させるだけです。
<webctrl tag='INPUT' id='RPA_VALUE' />
3ステップ目は『UiPath側に「読めない画像の有無」を伝えて制御を戻す』としていましたが、「伝えて」というより「結果を置いて読んでもらう」イメージですね。
このコードをカスタムしていけば、読めていない画像のパスをカンマ区切りの文字列にまとめてinput
要素に乗せて、UiPath側で拾ってからsplit
してエクセルに投入してレポートを生成する、なんてこともできそうですね。
※ 例ではJavaScriptでinput
要素を作っていますが、コンテンツを持つことができる要素ならid
属性さえ付与しておけばなんでもよさそうです(要素の種類によってコンテンツの入れ方は異なります)。
最後に
ここまでお付き合いいただきありがとうございます。
通常は、1つの関数内でreturn
文で文字列を返せばそれで事足りるのですが、非同期処理が必要な場合に、要素を作り出してUiPathに見つけさせる方法は非常に有効です。
ただし、UiPathが要素を見つけるまでのタイムアウト時間が短いと意味がないですから、見つけるまで待たせるように設定しておかなければならないことに注意が必要です(TimeoutMS
を長時間に設定するか、TryCatch
(トライキャッチ)アクティビティを利用して見つけられるまでループさせるなど)。
// ポイントまとめ
var input = document.createElement('input'); // 要素を作る
input.id = 'RPA_VALUE'; // UiPathのセレクタで見つけやすいIDを付与する
input.value = '好きな文字列'; // 引数で受け取った文字列を値に入れる。div要素を作ったりした場合は innerText で値を入れる
input.style.cssText = 'font-size: 20px; padding: 10px; position: relative; z-index: 9999; background: #fff; color: #333; box-sizing: border-box; width: 100%;'; // 目で見てわかりやすいようにスタイリングする(任意)
document.body.prepend(input); // ページの先頭に追加する
画像やCSSなどのリンク切れを検証する方法は他にも方法があったりしますが、Promise.all
とXMLHttpRequest
の組みわせ方法やfetch
との違いなどを解説するにはちょっとアドベントカレンダー畑が違いすぎると思い、コードも短くて済みますから、今回はfetch
を採用しました。JavaScriptの基本がわかった上で、JavaScriptに興味がある方はPromise
やfetch
のベストプラクティスを調べてみてください。
本業がWeb屋なのでUiPathの活用方法がフロントエンド寄りな発想ばかりですが、またなにかご紹介できるものがあれば書こうかと思います。
12月2日の記事と合わせて読んでくださった方、重ねてありがとうございました。何かありましたら編集リクエストお待ちしております。