Dartのサーバサイドフレームワークを色々と試してみようと考え、何がいいのかなぁ〜と呟いたらrikuloがいいと言われたのでとりあえず触ってみています。
rikuloはUIフレームワークやサーバサイドフレームワークなどを提供しており、サーバサイドフレームワークはrikulo streamといいます。
特徴としては、
- シンプルなルーティング機能
- シンプルなテンプレート機能
で、サンプルを見た感じだと相当シンプルです。 まだ実際に何かを作ったわけではないのですが、シンプルすぎて辛さを感じました。
rikulo streamのどこが辛そうか?
サーバサイドをDartで書けるという点は、実はまだそこまで実感できてないのですが、ざっと見た感じで。
- ホットリロードがない。(修正するたびに再起動しないといけない)
- 普通のブラウザでDartが動かない(package browserを入れていても…)
他にはデフォルトでSassが使えないとかありますが、まぁそれはrikuloのせいではないので割愛します。
ホットリロードがない
今やホットリロードくらいは流石にあってほしいんですが、なさそうです。ファイルを変更するたびに、Ctrl + C
で止めて、また実行しています。まぁDartは起動が速いのでそこまで気にならないかもですが、どちらにしても面倒です。
フロントエンドのDartが動かない
通常のフロントエンド開発では、多分、pub serve
を使って開発するみたいなのですが、pub serve
はあくまでもフロントエンドの開発を行うためのWebサーバーを提供するという役割だと思います(勘違いしていたらすみません)。pub serve
だとpubspec.yamlに書いたtransformersとかも反映されるのですが、rikuloで作ったプログラムだとそういうのは全く考慮されないので、自分で実装する必要がありそうです。
ちなみに、Dartiumを使えばそんなことはありません。Dartiumは自身がDartをそのまま理解できるので、なんの問題もありませんが、DartiumはDart 2.0で消える運命にあるみたいなので、もう使いたくはありません…。
とりあえずこれらを解決したい!
これらを解決しないことには、なんかやる気が出ない!ということで、フロントエンド開発のお供であるgulpみたいなタスクランナーがあればいいんじゃない?と思いました。調べたらすぐgrinderというgoogleが作っているタスクランナーが見つかりました。今回はこれを使います。(前置き長くてすみません…)
grinderとは?
grinderはGoogleが作っているDart用のタスクランナーです。
様々なタスクを自分で定義することができます。
grinderのインストール
pubspec.yamlに追加します。今回は、ファイルの監視にwatcherを、sassのコンパイルにsassパッケージを入れました。
dev_dependencies: dart_to_js_script_rewriter: ^1.0.1 grinder: "^0.8.0+3" watcher: "^0.9.7+4" sass: "^1.0.0-alpha"
そして、pub get
でインストールします。その後、以下を実行してタスクを定義するためのファイルを作ります。
pub run grinder:init
これを実行すると、tools/grinder.dart
というファイルが作られます。
あとはgrind
でタスクを実行できるようにするために、grinderをグローバルにインストールしておくと便利かなと思います。
pub global activate grinder
grinderを実行してみる
デフォルトの状態
デフォルトだと、こんな感じのシンプルな構成になっています。コマンドラインでgrind
と打って実行すると、@DefaultTask
のアノテーションが書かれているタスクが実行されます。ここでは、buildです。buildタスクは、@Depends(test)
と書かれていて、テストが通ったら実行されるようになっています。
import 'package:grinder/grinder.dart'; main(args) => grind(args); @Task() test() => new TestRunner().testAsync(); @DefaultTask() @Depends(test) build() { Pub.build(); } @Task() clean() => defaultClean();
今回はbuildタスクは使わないのでコメントアウトしておきました。
タスクを定義する
以下のようなタスクを定義しました。compile
タスクを定義し、serve
というデフォルトタスクを定義しました。
import 'dart:io'; import 'dart:async'; import 'package:grinder/grinder.dart'; import 'package:path/path.dart' as path; import 'package:watcher/watcher.dart'; import 'package:stream/rspc.dart' as rspc; import 'package:sass/sass.dart' as sass; // ...省略 @Task() compile() { dart2js(new Directory(path.absolute('web/client/js'))); sass2css(new Directory(path.absolute('web/client/css'))); compileRsp(new Directory(path.absolute('web/client'))); } @DefaultTask() @Depends(compile) serve() async { Process process = await getStreamProcess(); stdout.addStream(process.stdout); stderr.addStream(process.stderr); // rspファイルの変更を検知してコンパイルする var clientWatcher = new DirectoryWatcher(path.absolute('web/client')); clientWatcher.events.listen((event) async { if (event.path.endsWith('.rsp.html')) { compileRsp(new File(event.path)); } if (event.path.endsWith('.dart')) { dart2js(new File(event.path)); } if (event.path.endsWith('.scss') || event.path.endsWith('.sass')) { sass2css(new File(event.path)); } }); var serverWatcher = new DirectoryWatcher(path.absolute('web/webapp')); serverWatcher.events.listen((event) async { if (event.path.endsWith('.dart')) { log("Server: file change detected."); // ファイルのコンパイル後にrekulo streamを再起動させる // TODO: 標準出力とかのあたりがまともに動いてないかも… log("Server: kill process..."); if (process.kill(ProcessSignal.SIGTERM)) { process = await getStreamProcess(); log("Server: restart done."); } } }); } getStreamProcess() async => Process.start('dart', ['web/webapp/main.dart']); // コンパイル用の関数は省略
rikulo streamを使用したアプリケーションサーバが起動する前にコンパイルを実行し、その後起動させています(getStreamProcess関数で非同期にアプリケーションサーバを起動)。その後watcherパッケージを使い、ファイルの変更をクライアントサイド、サーバサイドに分けて監視させました。変更が発生したファイルだけを検知して単体で非同期でコンパイルを実行させているので、速度は速いです。そして、サーバ側のファイル変更を検知したら、アプリケーションサーバのプロセスを停止させ、直後にまた起動させることでホットリロードに対応させました。
これで、コマンドラインでgrind
、またはgrind serve
を実行するだけで、rikulo streamが起動してホットリロード対応、フロントエンドはdart,sassが使えるという環境が整いました!(たぶん)
編集後記
まだ何もアプリは作ってないので、あとは開発するだけですが、とりあえずこれで疲れたので一旦ブログを書いている次第です。
サンプルリポジトリは以下に置いてあります。
GitHub - patorash/rikulo-sample: rikuloを試してみる
コンパイル用の関数を定義するときに、Zonesを使う学びがあったので、また別の記事で書こうと思います。