FlutterでWebView(flutter_webview_plugin)を使ってページ内のJavaScriptを実行する

「横浜市立利用状況」アプリでは、予約や予約取消、貸出延長、再予約の補助として、横浜市立図書館検索ページをWebViewで開いて必要なページまで遷移、設定する機能があります。

WebViewは、flutter_webview_pluginパッケージを使っています。
その使いっぷりを一度整理しておこうと思います。

事前準備

・ios/Runner/Info.plist

Webコンテンツのローディングを許可するセキュリティ設定を追加します。

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
    <key>NSAllowsArbitraryLoadsInWebContent</key>
    <true/>
</dict>

・pubspec.yaml

依存パッケージを追加します。

dependencies:
  flutter:
    sdk: flutter
  flutter_webview_plugin: ^0.4.0 // これを追加

組み込み

・初期化と後処理

_flutterWebViewPlugin.close()をお忘れなく。

class MyWebViewCtrl extends StatefulWidget {
  MyWebViewCtrl({Key key}) : super(key: key);

  @override
  _MyWebViewCtrlState createState() => _MyWebViewCtrlState();
}

class _MyWebViewCtrlState extends State<MyWebViewCtrl> {
  final _flutterWebViewPlugin = FlutterWebviewPlugin(); // 初期化

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _flutterWebViewPlugin.close();
    super.dispose(); // 後処理(開放)
  }

  @override
  Widget build(BuildContext context) {
〜

・画面作成

WebviewScaffoldを使います。
「Accept-Language」は指定した方がよいと思います

@override
Widget build(BuildContext context) {
 〜
    return WebviewScaffold(
      url:"https://opac.lib.city.yokohama.lg.jp/opac/", // 最初に開くページURL。ここでは横浜市立図書館蔵書検索ページ
      withJavascript: true,
      headers: {'Accept-Language': 'ja-JP'}, // 英語対応サイトの場合、この指定がないと英語ページを取得してしまうのでこの指定はあった方がよい。
      appBar: new AppBar(
        title: Text([ // タイトルバー文字列
          '予約',
          '予約取消',
          '貸出延長',
          '再予約',
        ][args.mode.index]),
      ),
    );
}

・ページロード完了時イベントの判定

@override
  Widget build(BuildContext context) {
    〜
    // ページ読み込みステータスが変わったときのイベント処理の設定
    _flutterWebViewPlugin.onStateChanged
        .listen((WebViewStateChanged state) async {
      String url = state.url; // 読み込んだページ
      WebViewState type = state.type; // 読み込みステータス
      
      // ページ読み込み完了ステータス
      if (type == WebViewState.finishLoad) {
        switch (url) {[
          case 'https://opac.lib.city.yokohama.lg.jp/opac/':
           // ここで読み込んだページに対する処理を行う。(次項)
            break;
          default:
            break;
        }
      }
    });
   〜
}

・ページ内のJavaScriptを実行

JavaScriptの実行は非同期処理なるため、awaitで待ちます。
JavaScriptの話にはなりますが、参考としていくつかの実行例を紹介します。

// ページ内のinputフォームにvalueをセット
String cardNo = await _flutterWebViewPlugin.evalJavascript(
    'document.getElementsByName("USERID")[0].value="' +
        cardNo(図書カード番号) +
        '";');

// ページ内のbuttonをclick
await _flutterWebViewPlugin.evalJavascript(
    'document.getElementById("loginbtn").click();');

// ページ内のcheckboxにcheck
await _flutterWebViewPlugin.evalJavascript(
    "document.querySelector(\"body > article > table > tbody > tr > td > div > div > div.panel-body > form:nth-child(3) > div.visible-xs > div > input[type=CHECKBOX][value='" +
        ssno(書架番号) +
        "']\").click();");

より詳しい使い方はパッケージのページをご覧ください。

flutter_webview_plugin | Flutter Package
Plugin that allow Flutter to communicate with a native Webview.

まとめ

このような感じで、WebViewを使って、ページ読み込みとJavaScriptによるデータセット、画面遷移を繰り返せば自動実行でもスクレイピングでも大体なんでも出来そうです。

以上、ご参考になれば幸いです。

また、この記事の元ネタとなった「横浜市立図書館利用状況」アプリもぜひインストールしてお試しください。

最後にソース全文です。
上で紹介したソースは説明用に元ソースから変更しているので多少の違いがあることに注意してください。

import 'package:flutter/foundation.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'define.dart';
import 'package:flutter/material.dart';

enum MyWebViewCtrlMode {
  WillReserve,
  CancelReserved,
  ExtendOnLoan,
  WillReserveAgain,
}

class MyWebViewCtrlArguments {
  MyWebViewCtrlArguments({
    @required this.mode,
    @required this.cardNo,
    @required this.password,
    this.ssno,
    this.id,
    this.title,
  });

  final MyWebViewCtrlMode mode;
  final String cardNo;
  final String password;
  final String ssno;
  final String id;
  final String title;
}

class MyWebViewCtrl extends StatelessWidget {
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  @override
  Widget build(BuildContext context) {
    final MyWebViewCtrlArguments args =
        ModalRoute.of(context).settings.arguments;

    debugPrint(args.mode.index.toString());
    debugPrint(args.cardNo);
    debugPrint(args.password);
    debugPrint(args.ssno);

    flutterWebViewPlugin.onUrlChanged.listen((
      String url,
    ) async {
      debugPrint('onUrlChanged:' + url);
    });

    flutterWebViewPlugin.onStateChanged
        .listen((WebViewStateChanged state) async {
      //debugPrint('onStateChanged:');
      String url = state.url;
      WebViewState type = state.type;

      if (type == WebViewState.finishLoad) {
        debugPrint('finishLoad url:$url type:$type');
        switch (url) {
          // ログイン入力画面 or ログイン後画面
          case YokohamaCityLibraryUrl:
            // 図書館カード番号セットを試みる
            String cardNo = await flutterWebViewPlugin.evalJavascript(
                'document.getElementsByName("USERID")[0].value="' +
                    args.cardNo +
                    '";');
            cardNo = cardNo.replaceAll('"', '');

            // パスワードセットを試みる
            String password = await flutterWebViewPlugin.evalJavascript(
                'document.getElementsByName("PASSWORD")[0].value="' +
                    args.password +
                    '";');
            password = password.replaceAll('"', '');

            debugPrint('set form result1:' + cardNo + ',' + password);
            debugPrint(
                'set form compare:' + args.cardNo + ', ' + args.password);

            // 両方ともセットできたらログイン入力画面で確定
            if (cardNo == args.cardNo && password == args.password) {
              // ログインボタンをクリック
              debugPrint('loginbtn click.');
              await flutterWebViewPlugin.evalJavascript(
                  'document.getElementById("loginbtn").click();');
            } else {
              // ログイン後画面で確定
              // ログアウトボタンをクリック
              debugPrint('logoutbtn click.');
              await flutterWebViewPlugin.evalJavascript(
                  'document.getElementById("logoutbtn").click();');
            }

            break;
          case 'https://opac.lib.city.yokohama.lg.jp/opac/OPP0100':
            // 検索
            if (args.mode == MyWebViewCtrlMode.WillReserve ||
                args.mode == MyWebViewCtrlMode.WillReserveAgain) {
              await flutterWebViewPlugin.evalJavascript(
                  'document.getElementById("collapsedButton").click();');
            }

            /* 再予約なので検索キーワード入れて検索ボタンを押す */
            if (args.mode == MyWebViewCtrlMode.WillReserveAgain) {
              await flutterWebViewPlugin.evalJavascript(
                  "document.querySelector(\"#nav_target > div.input-group.custom-search-form > input.form-control\").value='" +
                      args.title +
                      "';");

              // タイトルをそのまま入れも検索出来ない場合もあるので検索ワード入れるにとどめて検索ボタンまで押さない。
              // await flutterWebViewPlugin.evalJavascript(
              //     'document.querySelector("#nav_target > div.input-group.custom-search-form > span > button").click();');
            }

            // 予約キャンセル
            // 貸出延長
            if (args.mode == MyWebViewCtrlMode.CancelReserved ||
                args.mode == MyWebViewCtrlMode.ExtendOnLoan) {
              await flutterWebViewPlugin.evalJavascript(
                  'document.querySelector("#nav_target > div.list-group > a:nth-child(6)").click();');
            }
            break;
          case 'https://opac.lib.city.yokohama.lg.jp/opac/OPP1000':
            // 予約キャンセル
            if (args.mode == MyWebViewCtrlMode.CancelReserved) {
              await flutterWebViewPlugin.evalJavascript(
                  "document.querySelector(\"body > article > table > tbody > tr > td > div > div > div.panel-body > form:nth-child(3) > div.visible-xs > div > input[type=CHECKBOX][value='" +
                      args.ssno +
                      "']\").click();");
            }

            // 貸出延長
            if (args.mode == MyWebViewCtrlMode.ExtendOnLoan) {
              var result = await flutterWebViewPlugin.evalJavascript(
                  "document.querySelector(\"body > article > table > tbody > tr > td > div > div > div.panel-body > form:nth-child(2) > div > div > label > input[type=CHECKBOX][value^='" +
                      args.id +
                      "']\").click();");
              debugPrint('result:' + result);
            }

            break;
          default:
            debugPrint('unknown url.');

            break;
        }
      }
    });

    return WebviewScaffold(
      url: YokohamaCityLibraryUrl, // ログイン画面
      withLocalStorage: true,
      withJavascript: true,
      headers: {'Accept-Language': 'ja-JP'},
      hidden: true,
      appBar: new AppBar(
        title: Text([
          '予約',
          '予約取消',
          '貸出延長',
          '再予約',
        ][args.mode.index]),
      ),
    );
  }
}

コメント

タイトルとURLをコピーしました