まえおき
世の中には、この令和の時代になってもなおChromeでは動かないWebサービスがあります。
勤怠システムや資産管理システムなど、業務ユーザしか使わないようなアプリケーションだと多いのではないでしょうか。
いまどき、Chromeでまともに動かないWebサービスを提供するくらいなら、REST APIを提供してほしいものです。でも世の中そんなに甘くはありませんね。
そこで今回は、Chromeで動かなくてイライラするようなWebシステムを、dartを活用して勝手にCLIを作る方法を紹介します。
ベースにする要素技術
dart2native
DartはGoのようにシングルバイナリを作れます。
去年のアドベントカレンダーでサラッと書いてた記事があるので、それを見てください。
→ dart2nativeで簡易的なコマンドラインツールを作る
puppeteer-dart
これも去年のアドベントカレンダーで、dart2nativeを使ってググレカスするCLIアプリケーションの作り方を紹介していました。
ぐぐれかすをするためには、ブラウザを動かす必要がありますが、その自動操作をするためのライブラリが puppeteer-dart です。(puppeteerをDartに移植された非公式のライブラリです)
文字入力、マウス操作はもちろんのこと、「特定のDOM要素が現れるまで待つ」 「画面遷移するまで待つ」 などができます。
puppeteer-dart のFirefox対応版
ここからが今年の話です。
今年のテーマは Chromeでまともに動かない(けどFirefoxだとかろうじて動く) Webサービスに戦うことでした。
なんか条件が増えてるけど気にしないw
さて、puppeteer-dartは2020/12/05現在、Firefoxの自動操作には対応していません。
・・・じゃあどうする?
Firefox対応させるしかないですね。Firefox対応させました。
https://github.com/xvrh/puppeteer-dart/pull/125
ただ、まだマージされてないんで、pubspec.yamlにはGitのURLを指定する必要があります。
dependencies:
puppeteer:
git:
url: git@github.com:YusukeIwaki/puppeteer-dart
ref: feature/firefox
Null safety "ではない" Dart SDK
2020年のDartの大きな変化といえば、なんといってもSound null safetyでしょう。
コードを書くときは圧倒的に Null safetyが書きやすいです。
しかし!
dart2nativeはどうもNull safetyじゃない依存ライブラリが1こでもあるとコンパイルが通らないみたいなんです。
$ dart2native main.dart -o awesome_app
Error: This project cannot run with sound null safety, because one or more project dependencies do not
support null safety:
- package:cli_dialog
- package:puppeteer
- package:meta
- package:async
- package:pool
- package:logging
- package:archive
- package:http
- package:path
- package:collection
- package:dart_console
- package:stack_trace
- package:petitparser
- package:crypto
- package:http_parser
- package:ffi
- package:win32
- package:convert
- package:string_scanner
- package:typed_data
- package:source_span
- package:charcode
- package:term_glyph
Run 'pub outdated --mode=null-safety' to determine if versions of your
dependencies supporting null safety are available.
Failed to generate native files:
Generating AOT kernel dill failed!
--no-sound-null-safety
オプションも今のところありませんし、いまのところ Dart SDK 2.10 (Null safety非対応)でdart2nativeするしか方法はなさそうです。
(実は方法があるよ!って知ってる人は教えて下さい)
cli_dialog
CLI化する場合には、いまのところ cli_dialogがシンプルかなと思います。
- テキストで入力をもらう
- 選択肢から選んでもらう
- yes/no を回答してもらう
が、簡単にできます。
DartはGoにくらべると、コンソールアプリケーションの便利ライブラリは少なくて、
cli_dialogも、正直かゆくて手が届かないところが多いです。OSSコントリビューションチャンスがいっぱいですね(白目)
組み合わせる
まず自動操作側を作ってしまう
class KusoServiceTop {
Page page;
KusoServiceTop(this.page);
browseToLoginPage() async {
await page.click("#sub_menu");
await Future.wait([
page.waitForNavigation(),
page.click("li.login_page"),
]);
}
}
class KusoServiceLogin {
Page page;
KusoServiceLogin(this.page);
submitCredential(String username, String password) async {
await page.waitForSelector("input[name='username']");
await page.click("input[name='username']");
await page.keyboard.type(username);
await page.keyboard.press(Key.tab);
await page.keyboard.type(password);
await page.keyboard.press(Key.tab);
await Future.wait([
page.waitForNavigation(),
page.keyboard.press(Key.enter),
]);
}
}
class KusoServiceDataList {
Page page;
KusoServiceDataList(this.page);
list() async {
final cells = await page.$$(".cell_item");
// 省略
}
}
きれいなページオブジェクトになっていなくても、とりあえず画面単位でクラスを分けるとよいです。
メインアプリケーション側のコードは、一旦CLIのプロンプト操作などを一切含まず、上から下に流れるだけのものにします。
import 'package:puppeteer/puppeteer.dart';
doWithBrowser(Browser browser) async {
final page = await browser.newPage();
await page.setViewport(DeviceViewport());
await page.goto("http://kusoservice.example.com/");
final top = KusoServiceTop(page);
await top.browseToLoginPage()
final login = KusoServiceTop(page);
await login.submitCredential("YusukeIwaki", "secretsecret");
final dataList = KusoServiceDataList(page);
for(DataItem item in await dataList.list()) {
print(item);
}
}
main() async {
final browser = await puppeteerFirefox.launch();
try {
await doWithBrowser(browser);
} finally {
await browser.close();
}
}
CLI部分を作り込む
ID/パスワードを問う
cli_dialogのサンプルほぼそのままですが、以下のように書けば ask()
のところで処理がブロッキングされて、ユーザ入力を受け付けます。ユーザ入力が済むと、処理が再開され、ログイン操作が行われます。
final loginDialog = CLI_Dialog(questions: [
["User Name:", "username"],
["Password:", "password"],
]);
final loginDialogAnswer = loginDialog.ask();
await login.submitCredential(
loginDialogAnswer['username'],
loginDialogAnswer['password'],
);
ちなみに、少し話は逸れますが、Goのviperみたいなライブラリは、今のところDartにそんな便利なライブラリはありません。
データを取得してきたものから、1つを選択してもらう
// itemListが選択肢
while (true) {
final selectDataDialog = CLI_Dialog(listQuestions: [
[
{
'question': 'Select data',
'options':
["Exit"] + itemList.map((item) => item.toString()).toList(),
},
'data'
],
]);
final String selectDataAnswer = selectDateDialog.ask()['data'];
if (selectDataAnswer == 'Exit') break;
// selectDataAnswerが選択されたもの
}
cli_dialog の仕様上どうしても、選択肢は文字列のリストで、選択されたものも選択肢の文字列そのままで返されてしまいます。
たとえば
12/1: xxxx
12/2: yyyy
12/3: zzzz
12/4: aaaa
12/7: bbbb
のような選択肢を出すには
["12/1: xxxx", "12/2: yyyy", "12/3: zzzz", ..... ]
を渡して、かりに12/7が選ばれると "12/7: bbbb"
が文字列として返ってくる、という感じです。
仕様としては非常にシンプルではありますが、「文字列」であることがとてもネックで、
たとえば勤怠データとかだと、「勤怠データから文字列への変換」「文字列から勤怠データへの変換」を用意しないといけません。
このデータのシリアライズ/デシリアライズ処理が増えるのが結構めんどくさいです。
HTMLのselect/optionみたく、表示テキストとvalueが別に指定できればいいんですけどね・・・。
あとは、DartはPythonのようにCtrl+Cをカジュアルに処理しづらいので、選択ダイアログをを抜けるための選択肢(上のサンプルだとExit)を1つ追加しておいたほうが無難です。
完成
(私が作ったのは会社の勤怠システム用のCLIなので、残念ながらスクショ貼れません・・・ そのうち気が向いたらサンプル作って貼ります)
今回は cli_dialogを活用したCLIを作る話でしたが、Dart標準のHttpServerを使えばJSON API化することもできるでしょう。
まとめ
令和にもなってChromeに対応していないWebサービスは、おそらく今後もずっとChromeには対応しないでしょう。
そんなWebサービスをわざわざFirefoxに切り替えて使わないといけないストレスから開放されるための、CLIツールの作り方を紹介してみました。(本当の敵はIEじゃないと倒せないかもしれませんがw)
Dartはシングルバイナリにビルドできるし、Goに比べるとゆるふわなコマンドラインアプリケーションを作るのに向いています。
みなさんの周りに、見たくもないWebサービスがもしもあれば、Dartの勉強がてらCLIを作ってみてはいかがでしょうか。