3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DartAdvent Calendar 2020

Day 5

いまだにChromeに対応していないWebサービス、puppeteer-dartとdart2nativeで勝手にCLIを作ってやる!

Last updated at Posted at 2020-12-04

まえおき

世の中には、この令和の時代になってもなおChromeでは動かないWebサービスがあります。
勤怠システムや資産管理システムなど、業務ユーザしか使わないようなアプリケーションだと多いのではないでしょうか。

いまどき、Chromeでまともに動かないWebサービスを提供するくらいなら、REST APIを提供してほしいものです。でも世の中そんなに甘くはありませんね。

そこで今回は、Chromeで動かなくてイライラするようなWebシステムを、dartを活用して勝手にCLIを作る方法を紹介します。

ベースにする要素技術

dart2native

DartはGoのようにシングルバイナリを作れます。

去年のアドベントカレンダーでサラッと書いてた記事があるので、それを見てください。
dart2nativeで簡易的なコマンドラインツールを作る

puppeteer-dart

これも去年のアドベントカレンダーで、dart2nativeを使ってググレカスするCLIアプリケーションの作り方を紹介していました。

dart2nativeを使ってggrksコマンドを作る

image

ぐぐれかすをするためには、ブラウザを動かす必要がありますが、その自動操作をするためのライブラリが 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を指定する必要があります。

pubspec.yaml
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がシンプルかなと思います。

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のプロンプト操作などを一切含まず、上から下に流れるだけのものにします。

main.dart
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なので、残念ながらスクショ貼れません・・・ :cry:  そのうち気が向いたらサンプル作って貼ります)

 

今回は cli_dialogを活用したCLIを作る話でしたが、Dart標準のHttpServerを使えばJSON API化することもできるでしょう。

まとめ

令和にもなってChromeに対応していないWebサービスは、おそらく今後もずっとChromeには対応しないでしょう。
そんなWebサービスをわざわざFirefoxに切り替えて使わないといけないストレスから開放されるための、CLIツールの作り方を紹介してみました。(本当の敵はIEじゃないと倒せないかもしれませんがw)

Dartはシングルバイナリにビルドできるし、Goに比べるとゆるふわなコマンドラインアプリケーションを作るのに向いています。

みなさんの周りに、見たくもないWebサービスがもしもあれば、Dartの勉強がてらCLIを作ってみてはいかがでしょうか。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?