開発編 |
Pubのサイトには膨大なライブラリが蓄積されている。このライブラリ内を検索すれば、自分のアプリケーション開発に有用なライブラリを見つけることが可能である。本章では読者のPubパッケージの活用の為の参考として、幾つかのパッケージを具体的に紹介してみたい。
なおtimeago、sqljocky5、及びloggingのサンプルはIDEの章で取り込んだdart2_code_samplesの中のC:\dart2_code_samples\utilizing_packagesというフォルダの中に含まれている。
|
timeago、sqljocky5、及びloggingの各アプリにはpubspe.yamlというファイルが含まれている。このファイルは外部パッケージを取り込むか、及びどの開発環境を使うかなどを指定するもので、Dart 2のアプリには必須となっている。従ってこれらのファイルを選択してPub getコマンドを実行しないと、上図のように赤のアンダラインでこのままでは実行不能と表示される。
例えばlogging_testでは下図のようにC:\dart2_code_samples\utilizing_packages\logging_test\pubspec.yamlをコード・ビュー上で開き、Get dependenciesをクリックする。
|
これには少々時間がかかるが、logging及びtestのパッケージが取り込まれ、赤のアンダーラインの警告がが消える。またlogging_test\.dart_toolというフォルダ、及びlogging_test\.packagesとlogging_test\pubspec.lockという2つのファイルが追加されることに注意する。同様の操作をtimeago及びsqljocky5に対しても行っておく。
mime_typeは僭越ながら筆者がDart言語開発初期の2013年にパブリッシュしたもので、HTTPサーバがファイルをクライアントに返す際に必要な'Content-Type'をそのファイルパスをもとに返すシンプルな関数である。その後Dartチームがこのライブラリを拡張たmimeをパブリッシュし、MIMEマルチパート要求にも対応できるようにしている。従って、このライブラリは歴史的な意味があること、及び極めて簡単なもので誰もがそのコードを見て理解できるという点で価値が未だ存在しているのかもしれない。
筆者がしばらくこの世界から離れていた間に、DartはDart 2になり、このライブラリはGoogleの Robert Beyer氏が自分のライブラリのroute_providerの依存物として使ってくれている。この際彼はこのライブラリをDart 2に対応するよう手を加えてくれ、筆者がそれを再パブリッシュしたものである。
従ってこのライブラリのページを開くとaboutの枠の中は次のように表示されている:
|
このように自分が作ったライブラリが、思わぬ人たちによって成熟して行くのが経験できるのはこのシステムの大きな特徴である。それらの経過はgithubのissues及びpull requestsのタブの終了したもの(closed)を開くと知ることができる。
このパッケージの使い方は「サーバ編」で解説する。
timeago v2はそのページをアップロードされた時刻等のイベント時刻を与えることで「何日前」とか「何か月前」とか表示するためのものである。几帳面な日本人にはしっくり来ないが、海外ではアバウトなほうが良いらしく、よく目にする。
このライブラリの構成は次のようになっている:
timeago/ example/ index.html main.dart lib/ src/ messages/ lookupmessages.dart da_messages.dart … ja_messages.dart … zh_messages.dart timeago.dart timeago.dart test/ CHANGELOG.md LICENSE README.md pubspec.yaml …. |
これは「ライブラリ・パッケージを構成する」で説明されているlib/src/内に.dartファイルを置く方式をとっている。これらのファイルはlib/timeago.dartによりエクスポートされている(lib/src/内にもtimeago.dartがあるので混同しないこと)。なおlib/src/messages/loolkupmessages.dartは抽象クラスで、これを各国向けのmessages.dart(例えば日本語ではja_messages.dart)のなかで具体化している方式になっている。
このライブラリを使ったtimeago_testというウェブ・アプリを紹介する。このアプリをブラウザで開いた画面をまずお見せする:
|
左半分がブラウザ画面で、右半分がデベロッパー・ツールのコンソール表示である。3個のセレクション・メニューに自分のエポック(イベントの時刻等)を選択して「現時刻との差を表示」のボタンをクリックすると、以下のものが表示される:
ISO 8601形式での現在の時刻
ISO 8601形式でのエポックの時刻
両者の間の差の日数での表示(マイナスは現時刻以後)
両者の間の差の時間での表示(マイナスは現時刻以後)
このプログラムはブラウザのタブを消さない限り走り続けるので、いろいろその動作を試してみることが可能である。
このプログラムは既に読者のIDE上で展開されている筈である。これはウェブ・アプリであるので、Buildコマンドを実行してbuildというフォルダに配布用の以下のファイルを用意しなければならない。
|
timeago_test\build\timeago_test.dart.js:webフォルダ名にあるtimeago_test.dartをJavaScriptに変換したファイル
timeago_test\build\timeago_test.html:webフォルダ名にあるtimeago_test.htmlと同じ物
この2つが組になってクライアント側に渡されることになる。従ってtimeago_test\build\timeago_test.htmlを最初にクライアントに渡すと、クライアントのブラウザはそれに従ってtimeago_test.dartを要求してくる。
コードは次のようになっているので、各自自分のIDE上で自分なりに加工してみると良い:
pubspec.yaml
|
name: timeago_test description: > A sample web application using Pub timeago library version: 0.0.1 author: homepage: environment: sdk: '>=2.0.0-dev <3.0.0' #dependencies: dependencies: timeago: ^2.0.13 dev_dependencies: build_runner: '>=0.8.10 <2.0.0' build_test: ^0.10.2 build_web_compilers: '>=0.3.6 <2.0.0' |
web/timeago_test.dart
|
import 'package:timeago/timeago.dart' as timeago; import 'dart:html'; main() { // Add a new locale messages timeago.setLocaleMessages('ja', timeago.JaMessages()); // See if Dart is running var mesArea = document.getElementById('title'); mesArea.innerHtml = 'dart code started'; // Go button listening forever var goButton = document.getElementById('goTimeago'); // On click event handler goButton.onClick.listen((e) { var el, p; el = document.getElementById('epochYear') as SelectElement; var epochYear = el.value; el = document.getElementById('epochMonth') as SelectElement; p = ('0' + el.value); var epochMonth = p.substring(p.length - 2); el = document.getElementById('epochDay') as SelectElement; p = ('0' + el.value); var epochDay = p.substring(p.length - 2); var tstr = '$epochYear-$epochMonth-${epochDay}T12:00'; var epochDateTime = DateTime.parse(tstr); var currentDateTime = DateTime.now(); var agoDateTime = currentDateTime.difference(epochDateTime); print(' epoch time is : ${epochDateTime.toIso8601String()}'); print('current time is : ${currentDateTime.toIso8601String()}'); print(' duration is : ${agoDateTime}'); print('duration_inDays : ${agoDateTime.inDays}'); print('duration_inHours: ${agoDateTime.inHours}'); document.getElementById('timeAgo1').innerHtml = "<br>" "<br>current time is : ${currentDateTime.toIso8601String()}" "<br>epoch time is : ${epochDateTime.toIso8601String()}" "<br>inDays : ${agoDateTime.inDays}" "<br>inHours : ${agoDateTime.inHours}"; var timeagoResult = timeago.format(epochDateTime, locale: 'ja', allowFromNow: true); print('datetime_output : $timeagoResult'); document.getElementById('timeAgo2').innerHtml = "<br> $timeagoResult"; }); } |
web/timeago_test.html
|
長いので省略(リンクを開いてその画面を右クリックし、「ページのソースを表示」を選択し、コピーする) |
読者がこの3つのファイルを使ってIDE上で試されるにあたって、幾つかの注意点を示すと:
これはウェブ・アプリであるので、timeago_test.dartとtimeago_test.html(それにもしあればCSS)のファイルは必ずweb/の下に置くようにする
timeago_test.dartでは
el = document.getElementById('epochYear') as SelectElement; var epochYear = el.value; |
のように、getElementByIDメソッドはElementのオブジェクトと見做しているが、Element→HtmlElement→SelectElementという実装ツリーになっており、実際は孫のクラスを取り出しているので、asを使って型キャストしなければならない。でないとvalue属性は取得できない。
このパッケージではまず日本語のロケールを以下のようにセットする:
timeago.setLocaleMessages('ja', timeago.JaMessages());
|
次にformatメソッドを呼ぶに際しては:
timeago.format(epochDateTime, locale: 'ja', allowFromNow: true);
|
のように、allowFromNow:をtrueにしないと現時点以降のエポックが「今から」といった表示がされない。
なお従来は「2 月 前」といった具合に月表示が不自然なので、「か月」表示にするよう作者にバグ報告し、早速対応してくれ、2.0.13にバージョン・アップされている。
通常大規模アプリではデータベースを使う場合が殆どであろう。その中でもMySQLは世界的に広く使われているRDBMSである。
SQLJockyはMySQL用のDart言語用のドライバで、長い歴史を持つ。このコネクタの著者のひとりのJames Otsによれば、sqljockeyという名前は著名なダーツの選手のJocky Wilsonが由来だという。
注意:このパッケージは2019年3月の時点で最新の2.2.1版にまだ問題が残っているので、この資料ではひとつ前の2.2.0版を使用している。新しい2.2.1版ではAPIに一部変更(preparedAllメソッドなどで)があるので、将来新しい版を採用する場合は注意が必要である。その場合のサンプル・コードはここにある。
読者のコンピュータ上にMySQLがインストールされている必要がある。ダウンロードとインストールの日本語の手順はネット上で検索(例えばここ)して頂きたい。
MySQLが初めてのユーザは:
Windowsの場合、MicrosoftのVisualC++がインストールされていないときは、これの最新の再配布版(redistributable package)をインストールしておく。
Shellコマンドを使うよりもワークベンチを使うほうが便利であろう。
Dartのsqljocky5ドライバは未だMySQL8.0で追加されたSHA256認証に対応していない。しかし2019年3月現在作業中で、近い将来対応可能となる予定である。従ってそれまでの間はMySQLのcaching_sha2_passwordプラグインからmysql_native_passwordに変更しなければならない。近い将来このドライバがSHA256認証に対応するようになればこの作業は不要となる。
これにはMySQLの設定ファイルに以下のテキストを追加する:
[mysqld] default-authentication-plugin=mysql_native_password |
このファイルのパスはWindowsでは C:\Program Data\MySQL\MySQL Server 8.0\my.ini である。(Program Dataというディレクトリは通常隠しファイルであるので、エクスプローラの表示から隠しファイルのチェックボックスを入れる)
このファイルをメモ帳で開くと、[mysqlid]の区間はSERVER SECTIONの区間の中にある。この区間の中にある
# The default authentication plugin to be used when connecting to the server default_authentication_plugin=caching_sha2_password |
を次のように変更し、上書き保存する:
# The default authentication plugin to be used when connecting to the server # default_authentication_plugin=caching_sha2_password default-authentication-plugin=mysql_native_password |
MySQLサーバを再起動する。
MySQLサーバの再起動はタスク・マネージャから行ったほうが手っ取り早い:
|
タスクマネージャ→サービスでMySQL80を選択し、右クリックで停止させ、すこし時間をおいて開始させる。
ワークベンチ上でsqljocky5の為の新しい接続を以下のように用意する。MySQL Workbench Community では、root コネクションで接続してから操作を行う。最初にウェルカム画面からMySQL ConnectionsのLocal instanse MySQL 80の表示個所をクリックしてこの接続の詳細を表示させる:
|
そうすると下図のようなインスタンス表示画面が表示される:
|
次にこのインスタンス表示画面の左側のAdministraionタブにあるUsers and Privilegesを選択し、Add Accountボタンをクリックして以下のように設定してやる:
user : sqljocky5
Login Name : sqljocky5
Authentication Type : Standard
Limit to Hosta Matching : localhost
Password : dart_jaguar
終了したらApplyボタンをクリックすると、下図のようになる:
|
次にアカウントにデータベースのアクセス権を設定する。ユーザ・アカウントのsqljocky5を選択して、Add Entry...ボタンを押して、Select “ALL”を選択したのちApplyボタンをクリックする:
|
まずワークベンチのLocal instance MYSQL80をクリックすると次のようなクエリのエディタ画面がでてくる:
|
左上にあるデータベースのアイコンをクリックすると、新規のスキーマ作成画面が表示されるので、以下のように指定する:
Name : sqljocky5
Caraset : utf8
Collation : utf8_general_ci
入力したらApplyボタンをクリックする
|
Revew画面が出てくるので、そこでもApplyボタンをクリックする。同様にレビュー画面が出るので、確認の上Applyボタンをクリックする。
これでsqljocky5がMySQLに接続可能となった。
サンプル・プログラムがPubにあるので、これを一部加工したものを示す。このアプリは既に読者のIDE上にC:\dart2_code_samples\utilizing_packages\sqljocky_test\bin\sqljocky_test.dartとして存在している筈である:
sqljocky_test\pubspec.yaml
|
name: sqljocky5_test description: > A sample code utilising Pub sqljocky5 library version: 0.0.1 author: homepage: environment: sdk: '>=2.0.0-dev <3.0.0' #dependencies: dependencies: sqljocky5: 2.2.0 test: ^1.3.0 |
sqljocky_test\bin\sqljocky_test.dart
|
import 'package:sqljocky5/sqljocky.dart'; import 'dart:async'; /// Drops the tables if they already exist Future<void> dropTables(MySqlConnection conn) async { print("Dropping tables ..."); await conn.execute("DROP TABLE IF EXISTS pets, people"); print("Dropped tables!"); } Future<void> createTables(MySqlConnection conn) async { print("Creating tables ..."); await conn.execute('CREATE TABLE people (id INTEGER NOT NULL auto_increment, ' 'name VARCHAR(255), ' 'age INTEGER, ' 'PRIMARY KEY (id))'); await conn.execute('CREATE TABLE pets (id INTEGER NOT NULL auto_increment, ' 'name VARCHAR(255), ' 'species TEXT, ' 'owner_id INTEGER, ' 'PRIMARY KEY (id),' 'FOREIGN KEY (owner_id) REFERENCES people (id))'); print("Created table!"); } Future<void> insertRows(MySqlConnection conn) async { print("Inserting rows ..."); List<Results> r1 = await conn.preparedMulti("INSERT INTO people (name, age) VALUES (?, ?)", [ ["Dave", 15], ["John", 16], ["Mavis", 93], ]); print("People table insert ids: " + r1.map((r) => r.insertId).toString()); List<Results> r2 = await conn.preparedMulti( "INSERT INTO pets (name, species, owner_id) VALUES (?, ?, ?)", [ ["Rover", "Dog", 1], ["Daisy", "Cow", 2], ["Spot", "Dog", 2] ]); print("Pet table insert ids: " + r2.map((r) => r.insertId).toString()); print("Rows inserted!"); } Future<void> readData(MySqlConnection conn) async { Results result = await conn.execute('SELECT p.id, p.name, p.age, t.name AS pet, t.species ' 'FROM people p ' 'LEFT JOIN pets t ON t.owner_id = p.id'); print(result); print(result.map((r) => r.byName('name'))); } main() async { var s = ConnectionSettings( user: "sqljocky5", password: "dart_jaguar", host: "localhost", port: 3306, db:"sqljocky5" // Schema ); // create a connection print("Opening connection ..."); var conn = await MySqlConnection.connect(s); print("Opened connection!"); await dropTables(conn); await createTables(conn); await insertRows(conn); await readData(conn); await conn.close(); } |
このコードはSQLを理解しているユーザは、このコードを追って行けばDartコードでどのようにテーブルの作成、挿入、変更、検索等を行うかが容易に理解されよう。このパッケージのAPIには必要な情報が記されているので、それも参考にされたい。
これを各自のIDE上に置いて動作させると、次のように表示される:
Opening connection ... Opened connection! Dropping tables ... Dropped tables! Creating tables ... Created table! Inserting rows ... People table insert ids: (1, 2, 3) Pet table insert ids: (1, 2, 3) Rows inserted! ([1, Dave, 15, Rover, Dog], [2, John, 16, Daisy, Cow], [2, John, 16, Spot, Dog], [3, Mavis, 93, null, null]) (Dave, John, John, Mavis) Process finished with exit code 0 |
このプログラムを実行したあとはpeopleとpetsの二つのテーブルが残される。例えばワークベンチでSELECT * FROM people;というコマンドを実行するとpeopleの内容を知ることができる:
|
アプリケーションの開発から運用に至る各段階でログをとることが欠かせない。DartにおいてもDartチームは早い段階からこのloggingパッケージを用意してきた。このパッケージはClosure JS Logger及びjava.util.logging.Logger等の言語との互換性をなるべく持たせているので、すでに他の言語でロギングを使った経験がある読者にとっては馴染みやすい。
各レベル(キー)には整数が割り当てられている。独自のレベルとその値を設定することも可能であるが、あらかじめ定義されているレベルの使用が推奨されている。どの事象に対しどのレベルを割り振るかはユーザの判断による。
キー |
値 |
意味 |
ALL |
0 |
総てのレベルに対しログをとるという特別の意味を持つ |
FINEST |
300 |
極詳細なトレース |
FINER |
400 |
かなり詳細なトレース |
FINE |
500 |
詳細なトレース情報 |
CONFIG |
700 |
staticな設定メッセージ |
INFO |
800 |
情報メッセージ |
WARNING |
900 |
潜在的問題 |
SEVERE |
1000 |
致命的障害 |
SHOUT |
1200 |
付加的なデバッグ用 |
OFF |
2000 |
総てのレベルに対しログを取るのを停止するという特別の意味を持つ |
各ロガーにはドットで分離した階層化を持たすことができる(従って各ロガーの名前はドットで始まってはいけない)。Loggerにはparentとchildlenの2つの階層化のプロパティがある:
import 'dart:async'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; main() { test('logger naming is hierarchical', () { var c = Logger('a.b.c'); expect(c.name, equals('c')); expect(c.parent.name, equals('b')); expect(c.parent.parent.name, equals('a')); expect(c.parent.parent.parent.name, equals('')); expect(c.parent.parent.parent.parent, isNull); }); } |
このコードはtestというパッケージを使用しているが、このコードを見ればLogger('a.b.c');で作ったcという名前のロガーの親はb、またその親はaという階層になっていることが判る。
このパッケージをインポートしただけでは、デフォルト状態ではログ・メッセージに関してはなにもしない。従ってログ・メッセージに対するログするレベルを設定し、それに対するハンドラを付加しなければならない。次のコードは総てのログ・メッセージをprintを介してログする単純な設定の例である:
Logger.root.level = Level.ALL; Logger.root.onRecord.listen((LogRecord rec) { print('${rec.level.name}: ${rec.time}: ${rec.message}'); }); |
最初にLogger.rootをセットしている。Loggerクラスのrootというプロパティはトップ・レベルに置かれるロガーであることを示す。Level.ALLはこのレベルまたはそれ以上のレベルの総てのメッセージはonRecordのストリームに送られる。
次にLogRecordのイベントに対するonRecordのストリームをリスンする。LogRecordクラスは message、error、logger name、その他多くのプロパティが存在する。
到来したLogRecordのイベントに対するハンドラは、Dartのトップ・レベルにあるprintメソッドでレベル名:到来時刻:到来メッセージの形式でコンソールに出力している。
ログ・メッセージの到来元が容易に識別できるようなユニークな名前を持ったLoggerを生成する:
final log = Logger('MyClass');
|
以下はデバッグ・メッセージとエラーをログする一例である。この場合はある非同期処理doSomethingAsync()を監視しログを取っている:
var future = doSomethingAsync().then((result) { log.fine('Got the result: $result'); processResult(result); }).catchError((e, stackTrace) => log.severe('Oh noes!', e, stackTrace)); |
もっと複雑なメッセージのログをとるには、該メッセージが実際にログされたときにのみ処理されるようなクロージャを渡すことも可能である:
log.fine(() => [1, 2, 3, 4, 5].map((e) => e * 4).join("-"));
|
前項にあった非同期関数doSomethingAsyncとしてテキスト・ファイルを非同期で読み出す関数を用意してみる。このサンプルは既に読者のIDE上にdart2_code_samples\utilizing_packages\logging_test\bin\logging_test.dartとして存在している筈である:
bin/logger_test.dart
|
//import 'dart:async'; import 'dart:io'; import 'package:logging/logging.dart'; //import 'package:test/test.dart'; // create logger final log = Logger('MyClass'); createLogger() { Logger.root.level = Level.ALL; Logger.root.onRecord.listen((LogRecord rec) { print( '${rec.level.name}: ${rec.time.toString().substring(11)} : ${rec.message}'); }); } // doSomethingAsync doSomethingAsync() async { var file = new File('bin/logging_test.txt'); return file.readAsStringSync(); } // main() main() async { createLogger(); log.fine('started processes'); var future = doSomethingAsync().then((result) { log.fine('Got the result: $result'); }).catchError((e, stackTrace) => log.severe('Oh noes!', e, stackTrace)); } |
bin/logger_rest.txt
|
Pub 'logging' provides APIs for debugging and error logging. This library introduces abstractions similar to those used in other languages, such as the Closure JS Logger and java.util.logging.Logger. |
Pubspec.yaml
|
name: logging_test description: > A sample code utilising Pub logging library version: 0.0.1 author: homepage: environment: sdk: '>=2.0.0-dev <3.0.0' #dependencies: dependencies: logging: any test: any |
ここではdoSomethingAsync()は非同期関数であるasyncが付され、中には非同期メソッドのfile.readAsStringSync()が使われている。これを実行すれば次のようなログ・メッセージが記録される:
FINE: 19:40:01.241961 : started processes FINE: 19:40:01.274963 : Got the result: Pub 'logging' provides APIs for debugging and error logging. This library introduces abstractions similar to those used in other languages, such as the Closure JS Logger and java.util.logging.Logger. |
ここで存在しないファイル名を指定すると
FINE: 19:47:18.575732 : started processes SEVERE: 19:47:18.601729 : Oh noes! |
というログ・メッセージが書き込まれるが、残念ながらエラーの内容とスタック・トレースは残されない。
Pubにはこれ以外にも多くの有用なライブラリが登録されている:
test:Dartコードの高度なテスト
shelf:サーバ・ミドルエア
http:HTTPサーバとクライアント記述の簡単化
dart_style:Dartコードのフォーマッタ
json_annotation:JSONコードの作成用ヘルパー
google_maps:ブラウザ用のGoogle Maps用API
modern_charts:ブラウザ上でのチャート作成
特に最近はスマホ等のモバイル機器向けのFlutter用のライブラリが急速に増えている。
従って、Dart言語によりアプリの開発に際しては、Pubを検索して活用することが有効であろう。