pub serveに対してproxyを経由してアクセスする
Dartでサーバサイドの開発とクライアントサイドの開発を行いたい場合、サーバサイドはともかくとして、クライアントサイドの開発はpub serve
を使わないと全然うまくいかないように思います。pubspec.yamlにtransformersを定義するのですが、pub serve
で起動したWebサーバでしか、それが有効ではないので、それ以外の方法では、AngularDartとかは使えないんじゃないかと思います(私が詳しくないだけかもしれないけれど)。
サーバサイドはサーバサイドで起動して、クライアントサイドはpub serve
で起動して、それぞれをやりとりするようにすればよさそうなのですが、ポート番号も違うし、どうすりゃいいんだろうか?と色々調べてみました。
proxyを使えばいいことに気づいた
AngelというDartのフレームワークがあるのですが、それでは、angel_proxyという機能を使ってサーバサイドのAngelとクライアントサイドのAngularDartのやりとりを行っているようでした。
ざっくりいうと、Angelにアクセスすると、pub serve
で起動したサーバに対してリクエストを飛ばして、その結果を返すようでした。この発想は思いつきませんでした。いや、よくある構成とかなんですが、なんかこういうパターンの解決策をど忘れしていて、パッと思いつくことができません…。くやしい…。
pubで検索してみた
proxyというキーワードがわかればこちらのものだと思い、pubでキーワード「proxy」で検索したら、shelf用のproxyなどもあるようでした。
shelfに則ったWebフレームワークなら、これを使うと良さそう。ですが、今はrikulo streamを使おうとしています。rikuloはshelfに対応してないので、他の方法を探してみました。
http_request_proxyはすごくシンプルなproxyです。手始めにこれを使ってみることにしました。
http_request_proxyはダメだった
結論から書くと、これは使えませんでした。ちゃんとproxyの役割はしてくれていたのですが、pub serve
が返すレスポンスのステータスが304のときに、content typeがtext/plain
になってしまい、cssファイルやjsファイルにアクセスできても「content typeがtext/plainだから何もしないよー」というログがコンソールに表示されていました。悲しい…。
http_request_proxyを参考にして作り直す
とはいえ、http_request_proxyはファイルを返すことには成功していたので、これを参考にcontent typeを任意のものに書き換えるように修正したproxyを作りました。libディレクトリ以下に定義しました。http_request_proxyとの違いは、pub serve
から返ってきたcontent typeをそのまま設定するのではなく、request.uri.path
の拡張子を元にcontent typeを設定している点です(それ以外はほぼ同じ)。
library stream_sample; import 'dart:io'; import 'dart:async'; import "package:stream/stream.dart"; import "package:rikulo_commons/io.dart" show getContentType; import 'package:path/path.dart' as path; class PubServeProxy { String _host; int _port; PubServeProxy(this._host, this._port); Future forward(HttpConnect connect) async { HttpRequest request = connect.request; final HttpClient client = new HttpClient(); final HttpClientRequest proxyRequest = await client.open( request.method, _host, _port, request.uri.path); await proxyRequest.addStream(request); final HttpClientResponse proxyResponse = await proxyRequest.close(); final HttpResponse response = request.response; response.statusCode = proxyResponse.statusCode; response.headers.contentType = getContentType(path.extension(request.uri.path)); await response.addStream(proxyResponse); await response.flush(); await response.close(); client.close(); return null; } }
これを、rikuloのurlマッピングに使います。
PubServeProxyを使う
では、実際に使ってみましょう。
rikulo streamはport 8080で、pub serveはport 8000で起動させました。コードはrikulo streamのサンプルを実行した名残があるのですが、重要なのは_mapping
のところです。/assets/以下と、/packages/以下にアクセスがあった場合、pub serve
に対して取得しにいっています。
library stream_sample; import 'dart:io'; import 'dart:async'; import "dart:convert" show JSON; import "package:stream/stream.dart"; import "package:rikulo_commons/io.dart" show getContentType; import 'package:stream_sample/pub_serve_proxy.dart'; part "client/helloView.rsp.dart"; var proxy = new PubServeProxy('localhost', 8000); //URI mapping var _mapping = { "/server-info": serverInfo, "/hello": helloView, "/assets/.*": proxy.forward, "/packages/.*": proxy.forward }; void serverInfo(HttpConnect connect) { final info = {"name": "Rikulo Stream", "version": connect.server.version}; connect.response ..headers.contentType = getContentType("json") ..write(JSON.encode(info)); } void main() { new StreamServer( homeDir: 'client', uriMapping: _mapping, ).start(); }
これで、http://localhost:8080のみにアクセスしつつ、フロントエンドもDartで快適に開発できるようになりました!
注意点
現時点では、単純にpub serve
ファイルが取得できるようになっただけなので、buildして実際にファイルが存在する場合などのルーティングは全く考慮できていません。ファイルが存在する場合はproxyを使わないような処理を書かないといけませんね。
Dartの非同期処理のエラー制御にはZoneを使う
前回はgrinderを使ってタスクを実行するという記事を書きました。
その時に、compile
タスクを定義しました。dartをjsに変換、sassをcssに変換、rspを変換などです(rspはrikuloのテンプレート機能。JSPみたいなやつ)。
@Task() void compile() { dart2js(new Directory(path.absolute('web/client/js'))); sass2css(new Directory(path.absolute('web/client/css'))); compileRsp(new Directory(path.absolute('web/client'))); }
実はこれらを定義してちゃんとコンパイルが通るようになって、いい気になっていたのですが、欠陥がありました。例えば、sassファイルが正しくない状態でコンパイルされたらコンパイルエラーが発生して、grinderで実行中のタスクごと落ちてしまうという不具合です(しかもrikulo streamのプロセスは生きたまま…)。
つまりはエラー制御をしっかりやれって話です、はい。
エラーになったコード
エラーになったコードを書いておきます。 やっていることはシンプルで、非同期でsassをcssにコンパイルしています。
/// sassをcssにコンパイルする /// /// [file]がDirectoryだった場合は、再帰的に呼び出す。 /// Fileだったら非同期でコンパイルする。 void sass2css(FileSystemEntity file) { file.exists().then((bool exists) { if (file is Directory) { Directory dir = file; dir.list().listen((FileSystemEntity fse) { sass2css(fse); }); } else if (file is File && (file.path.endsWith('.scss') || file.path.endsWith('.sass'))) { // この処理でコンパイルに失敗したら本体を巻き込んで落ちる! new File("${path.withoutExtension(file.path)}.css").writeAsString( sass.compile(file.path), mode: FileMode.WRITE_ONLY ); } }); }
エラー制御を追加したコード
Dartの非同期処理のエラーハンドリングにはZoneという概念があり、エラー制御はそのZoneの中で行うことができます。今回はそれをしていなかったために、全体を巻き込んで落ちていました。シンプルなZoneの例だと、非同期処理をrunZoned
でくくります。
runZoned(() { // 非同期処理 }, onError: (e) { // 非同期処理でエラー発生したら実行されるブロック // なんらかのエラー制御を行う });
これを元に、エラーになっていたコードを直します。
/// sassをcssにコンパイルする /// /// [file]がDirectoryだった場合は、再帰的に呼び出す。 /// Fileだったら非同期でコンパイルする。 void sass2css(FileSystemEntity file) { file.exists().then((bool exists) { if (file is Directory) { Directory dir = file; dir.list().listen((FileSystemEntity fse) { sass2css(fse); }); } else if (file is File && (file.path.endsWith('.scss') || file.path.endsWith('.sass'))) { runZoned(() => new File("${path.withoutExtension(file.path)}.css").writeAsString( sass.compile(file.path), mode: FileMode.WRITE_ONLY ), onError: (e) { log(e.toString()); }); } }); }
エラーが起きたらその原因をgrinderのログに出力するように変更しました。 これで、コンパイルエラーが起きてもgrinderのタスクを巻き込んで落ちることはなくなりました。
編集後記
非同期処理のエラー制御まで頭が働いていませんでした。たしかにそれでアプリケーション自体を巻き込んで落ちてしまったら、ウェブサーバーとかの場合は洒落になりません…。しかしrunZoned
を使えば見通しよくエラー制御できるのでいいですね。
Dartのタスクランナーのgrinderを使う
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を使う学びがあったので、また別の記事で書こうと思います。
Packtという洋書サイトがハロウィンセールで全ての本が$10になってた
今週限定でPacktという洋書サイトでハロウィンセールをしている模様です。1冊10ドル、3冊だと25ドルになるというすごいセールです。大体の本が30〜130ドルとかなので、100ドル越えの本とかで欲しい本がある場合はマジで最高なんじゃないだろうか?と思います。
たまたま、洋書でもいいからDartの本を探してみるかーという軽い気持ちでググったら、見つけました。洋書だとDartの本は結構あります。
Dart本の最新のやつで、約900ページもある大作です。
タイトル通り、Dartのサンプルコードが載ってそうなので買ってみました。
Rubyの内容を網羅的に捉えてそうな本だったので、買ってみました。3冊買うと25ドルになるから、Rubyの本はマジでとりあえず買っただけです。
Paypal払いができたので、それで買いました。25ドルは、まぁ大体3,000円くらいでした。
洋書はあんまり読んだことないんですが、技術本だしなんとかなるでしょう!と思って頑張って読んでみようと思います。
Dart良さそう
昨日の社内勉強会で、「Dartやってみたい」というLTをしました。 DartはGoogleが作ったプログラミング言語で、JavaScriptを置き換えるようなポジションでデビューしたのですが、今は方向転換してフロントエンドの扱い的には、JavaScriptにクロスコンパイルするのをメインとするようになりました。また、Dart VMがあるので、サーバサイドやCLIツールを作るのにも適していると思います。
なんで「Dartやってみたい」という発表かというと、まだちゃんとDart使ったことがないから…。
Dartはオワコンになったのか?
社内勉強会のときにも「オワコンになったんじゃないの?」という反応がありましたが、むしろGoogleでは積極的に使われていることや(Adwords, AdsenseとかはDartらしい)、FlutterというAndroid, iOSの両方の開発ができるフレームワークが登場したこと、SassがRubyとDartで開発されていることなど、まだまだオワコンではないという紹介をしました。
Dartのどこが良さそうか
サンプルコードを少しだけ書いてみたのですが、たしかに馴染みのある機能が多く、かつ、書きやすいです。
- クラスベースで馴染み深い
- 全てがオブジェクトでプリミティブ型とかない
- 型の柔軟性がすごい(var宣言のみで動く。型も使える)
- 単一継承、インターフェース、mixinが使える
- ジェネリクスが使える
- 非同期処理が得意(Future,Completer, async, await)
- スレッドセーフ
- 変数やメソッドにスコープがある(public, private)
- Dart VMが速い(らしい)
型の柔軟性
var宣言でdynamic型を使う
あまり型を考えずに実装しても、動きます。このコードはvar宣言を使ってdynamic型で実装しています。Userクラスには名前を入れるようにしてあるので、文字列がわかることを想定していますが、user2のように数字を渡しても動いてしまいます。とはいえ、とりあえず作るのには楽ですね。
class User { var _forename; get forename => _forename; set forename(value) => _forename = value; var surname; getFullName() { return "$forename $surname"; } } void main() { var user1 = new User(); user1.forename = "Taro"; user1.surname = "Yamada"; print(user1.getFullName()); var user2 = new User(); user2.forename = 1; user2.surname = 2; print(user2.getFullName()); }
dynamic型をString型, User型に変更する
var宣言していた変数を、ちゃんと型宣言するようにしてみました。こうすると、user2で数字を入れている箇所は、Strongモードにするとエラーになります(普通でもwarningは出る)。最初から型でガチガチにしなくても、後々変更していくことができるので、トライアンドエラーしやすい言語だなという印象です。
class User { String _forename; String get forename => _forename; set forename(value) => _forename = value; String surname; String getFullName() { return "$forename $surname"; } } void main() { User user1 = new User(); user1.forename = "Taro"; user1.surname = "Yamada"; print(user1.getFullName()); User user2 = new User(); user2.forename = 1; user2.surname = 2; print(user2.getFullName()); }
まとめ
柔軟なのに型も使えて速い言語という印象に変わったので、個人プロジェクトでDartを使ってみたいなぁ〜と強く感じています。 ちょうど昨日は東京でDart MeetUp Tokyo #2というイベントがあったみたいでした。聞いてみたかった!

- 作者: Chris Buckett,あんどうやすし,粟納裕貴,勝又雅史,川俣千恵子,植田大貴,井関正也,水野あゆみ
- 出版社/メーカー: KADOKAWA/アスキー・メディアワークス
- 発売日: 2014/03/28
- メディア: 大型本
- この商品を含むブログ (2件) を見る
Dartの本といえば、日本ではこれくらいしかないのですが、海外では結構出ています。でもこの本もまだまだいいと思います。中古市場がどえらい値崩れしてますので、興味のある人には買ってみるといいかもですね。
並列処理でActiveRecordの処理時間を短縮する
ここ最近は並列化による処理速度アップを色々試しています。 Railsプロジェクトのデータに少々不備があることに気づいたので、それを修正するために該当データを抽出しようと思って雑にループを回したら、データ量が多いせいか、全然終了しませんでした。業を煮やした私は、これも並列化してしまおう!と思って並列化の情報を集めることに。
Rubyでの並列処理は、parallelというgemを使うと並列処理がすごく簡単にできました。
parallelのいいところ
parallelのいいところは、
- 並列処理したい対象の配列データを渡すだけでいい
- map, each, any?, all? などに対応している
- マルチプロセス、マルチスレッドの両方に対応している
というところでしょうか。
簡単な使い方の例
Parallelに対して、配列を渡したら、自動的にCPUの数だけプロセスをフォークして処理してくれます。並列数を指定することもできます。
require 'parallel' # デフォルトでCPUの数だけプロセスが立ち上がる Parallel.each(['a', 'b', 'c']) do |one_letter| expensive_calculation(one_letter) end # 3プロセスで処理 Parallel.each(['a', 'b', 'c'], in_processes: 3) do |one_letter| expensive_calculation(one_letter) end # 3スレッドで処理 Parallel.each(['a', 'b', 'c'], in_threads: 3) do |one_letter| expensive_calculation(one_letter) end
ActiveRecordのデータを並列で処理する
ActiveRecordのデータを並列で処理する場合、フォークしたプロセスの数だけデータベースに接続する必要があるので、reconnectを使って再接続命令を行います。
Parallel.each(User.all, in_processes: 8) do |user| @reconnected ||= User.connection.reconnect! || true user.update_attribute(:some_attribute, some_value) end
もっとセンスよく並列化したい
データがそんなに多くなければ、上記のようにParallelにUser.allみたいに雑にやってもいいとは思うのですが、もしUserに大量のデータがあった場合、メモリ消費量が多くなり、重くなると思います。さすがにこれはやばいだろう…と思って調べていたら、すごくセンスのある並列処理のサンプルコードが載ってる記事を見つけました。
find_in_batches
を使うところにすごく感動しました。
User.find_in_batches do |users| Parallel.each(users) do |user| @reconnected ||= User.connection.reconnect! || true user.update_attribute(:some_attribute, some_value) end User.connection.reconnect! end
これならば、デフォルトで1,000件ずつ取得したデータを並列で処理して、次の1,000件へ…のようにできるので、メモリ使用量も少なくて済みます。
この形式に変更してから処理を行なったところ、20分程度で結果が返ってきたので並列化最高!!という気持ちになりました。
気づき
実はfind_eachはよく使っていたのですが、find_in_batchesは使ったことがありませんでした。多分、初めて使ったと思います。Parallelとの相性がいいから、他のプロジェクトなどでも高速化を狙えるところがあったら積極的に使っていこうと思います。
体調管理について振り返りを行う
9月は何度か体調を崩したりしてしまって、この間支給された有給休暇を早速ガンガン使うことになってしまっています。うぅ、旅行とかに使うつもりだったのに…。
やっぱり体が資本
体調不良になるとこれを痛感させられます。読書とか、新しい技術とかを調べて何かやってみたい!と思ってもできなくなるので、体調管理は本当に大事です。自戒を込めて、今回はなぜ体調を崩してしまったのかを考察しておきます。
削ってはいけない睡眠時間
子育てしてると時間の捻出が難しくなるので、ついつい睡眠時間を削ってしまいました。意図的に削ったというよりは、作業とかしていて気づいたら深夜だった(AM3時近く)という感じです。ここから寝ると、私の場合、最長で寝られても5時間程度になるので、翌日のパフォーマンスにすごく響きました。1日程度ならいいのですが、数日続くとダメな感じですね…。
ベッドでプログラミングして腰痛
少しの時間のつもりで、ベッドでごろ寝しながらコーディングなり調べ物なりしていたからか、肩や腰が痛くなったりと、すごく体に負担になっていました。やっぱり正しい姿勢で作業をしないといけません。私の場合は体の凝りから体調不良になって熱が出るパターンがあるので、体に優しい環境づくりをしないといけないなと痛感しています。
改善案を考える
朝型にする
理想論だと、生活リズムを朝型にすることなんですが、自分たちのせいでもあるんですが子供も夜型になっていて、早めに寝かしつけようとしてもなかなか寝てくれません。徐々に全ての事項を早めにすることが大事なのだろうなぁと思うので、このあたり夫婦で認識合わせをしていきたいところです。
作業用スペースを確保する
これが目下の課題。片付けしてスペースを確保しなければ。今はもう部屋を物置にしているだけで有効活用できていないので、少しずつでも片付けていかないといけません。疲れが溜まって放置気味なので、1日5分でもいいからやる!くらいで実行していこうかなと思います。
ストレッチを行う
昔は体が柔らかかったのに、今は自分でも信じられないほど硬くなっています…。恐らく、これが原因で疲れが溜まりやすく、肩こり、腰痛になっているのだと思うので、体のメンテナンスに時間を割くようにして、場当たり的な対応ではなく、根本解決を図ろうと思います。Kindle Unlimitedにストレッチの本があったので、それを参考にしつつ、ストレッチと体幹トレーニングを行う習慣をつけていこうと思います。

- 作者: 岩井隆彰
- 出版社/メーカー: マイナビ出版
- 発売日: 2014/03/25
- メディア: Kindle版
- この商品を含むブログ (1件) を見る
ちなみにこの本、体の硬い人はここまででいい、と書いてくれているので、気軽にやることができます。自分の場合、その姿勢すらキツいときもあります…。ヨガマット買わないと床でやるとめっちゃ痛い姿勢とかあるので今度買わないと。