search
LoginSignup
3

More than 5 years have passed since last update.

posted at

Angular2 Dartを改めて使ってみたのでノウハウを忘れないうちにメモ

ハッピーバースデー俺。

さて、Dart界隈で有望そうなクライアントサイドフレームワークといえばフルスタック双方向バインディングでおなじみなAngular2さんくらいのものなのですが、少なくとも1年前はeveryday breaking changes! なヒャッハー状態で使い物にならない雰囲気を感じたものです。

が、あれから1年の間にいろんな事がおこり、GoogleマジAngular2をDartで使ってるからね!見捨ててないからね!アピールをしてきたのであらためて使ってみたいと思います。

とりあえず適当なのを作ってみた

Angular2_MaterialDesign.png

デモページ
http://takyam-git.github.io/angular2_test_20151212/

ソースコード
https://github.com/takyam-git/angular2_test_20151212

アプリケーションの概要としては、右側のフォームで入力した文字列が、左側に順番に表示されるだけの糞みたいなアプリケーションですね。

愚にもつかないアプリケーションではあるのですが、私はAngularに全然明るくないので許してちょんまげ。(半日かけてここまで出来ただけでも驚きだよ!)

で、今回これを作ってみて得られた知見を披露したうえで、後世のAngular2のDartバージョンでアプリケーションを作れる時代にきっと同じポイントでハマる人が出てくると思うのでその一助になればと思っております。
感想は最後に書いてます。

とはいえ

Angular 2 is currently in Developer Preview. We recommend using Angular 1.X for production applications:

ってREADMEの先頭に書いてある程度にはα版なので、この知見もすぐ使えなくなると思うけどね!

知見を箇条書き

  • コンポーネントが依存してるコンポーネントは derectives に渡してあげる必要がある
  • NgIfNgFor といったよく使うディレクティブも derectives に渡してあげる
  • コンポーネントからコンポーネントにオブジェクトを渡すときは @Input
  • nl2br 的モジュールは Pipe を使うと良さそう
  • Pipe を定義するときには @Injectable を忘れずに
  • テンプレートでHTMLエスケープせずに埋め込みたい場合は [innerHtml]
  • event.preventDefault() みたいなのしたいときはテンプレートから $event を渡す

アプリケーションの構成

Kobito.w7nb37.png

今回作ったclassは↑の図のような感じ。大した量じゃないね。
1個の(シングルトンな)Threadに複数のRes(書き込み)が紐づく感じ。

Angular2_MaterialDesign.png

各Componentは画面上でいうとそれぞれ↑みたいな感じ。

AppComponentの子にThreadComponentとPostComponentの2つがいる。
ThreadComponentの子に書き込みの数だけResComponentがいる。

AppComponent

階層の上から順に見たほうが構成が把握しやすいと思うのでまずはAppComponentを見てみましょ。

@Component(
    selector: 'my-app',
    templateUrl: 'app_component.html',
    directives: const [ThreadComponent, PostComponent]
)
class AppComponent {
  Thread thread = new Thread();
}
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">Simple BBS (現在の書き込み数: {{thread.resList.length}}個)</a>
        </div>
    </div>
</nav>
<div class="container">
    <div class="row">
        <div class="col-md-8">
            <my-app-thread></my-app-thread>
        </div>
        <div class="col-md-4">
            <my-app-post></my-app-post>
        </div>
    </div>
</div>

依存関係を明示的に宣言してあげる

Dartのコードの方はThreadのシングルトンを取得している他、ThreadComponentPostComponent への依存を明示しているくらいです。

HTMLの方もBootstrapのいわゆるテンプレートそのままで、タイトルのところに書き込みの数を表示するために {{thread.resList.length}} があるのと、ThreadComponentPostComponent を呼び出す <my-app-thread></my-app-thread><my-app-post></my-app-post> が書いてあるくらいであとは普通のHTML。

directivesがかなりイケてるなぁと思ったのは、ひとつのコンポーネントが依存しているコンポーネントを明示すれば十分で、たとえばこの後説明する ThreadComponentResComponent に依存してるわけなんですが、AppComponent はその事を知らなくていいし宣言しなくてよい。
あくまで自分自身が利用しているコンポーネントなりディレクティブを宣言してあげればよいので、非常に明快で分かりやすいですね。

あとは thread.resList.length なんですが、これはもうDartのオブジェクトのプロパティをHTML上で直接参照できていて、使い勝手として違和感が無いのはすごいなと思った。

ThreadComponent

@Component(
    selector: 'my-app-thread',
    templateUrl: 'thread.html',
    directives: const [NgFor, NgIf, ResComponent]
)
class ThreadComponent {
  Thread _thread = new Thread();
  List<Res> resList;

  ThreadComponent() {
    this.resList = this._thread.resList;
  }
}
<my-app-res *ngFor="#res of resList.reversed" [res]="res"></my-app-res>
<p *ngIf="resList.length == 0">右の投稿フォームから入力してください</p>

NgIfNgForもディレクティブとして使うための宣言が必要

こちらも同じように依存している ResComponentdirectives に追加してあげてます。
AppComponentでは使ってなかったので書いてませんでしたが、ThreadComponentでは*ngFor*ngIfをHTML上で利用しているのでNgForNgIfdirectivesに追加しています。

DartのオブジェクトはDartのオブジェクトとして利用可能

あとはResオブジェクトの配列である Thraed.resList を自身のプロパティresListにセットする事でHTMLからResのリストにアクセスできるようにしています。

<my-app-res [res]="res"></my-app-res> ですが、 resRes のインスタンスで、それをそのまま渡せてる点がマジで便利です。すごい。ただしこれも渡せるようにするのに ResComponent 側で設定がいるので注意です(後述)。

#res of resList.reserved を見ると分かるように .reserved はDartのListのメソッドで、順序を反転したIterableを返してくれるのですが、そういったものも特にAngularだからどうのこうのを意識せずにHTML上で呼び出すことができます。すごい。

ResComponent

@Component(
    selector: 'my-app-res',
    templateUrl: 'res.html',
    directives: const [NgIf],
    pipes: const [Nl2BrPipe]
)
class ResComponent {
  @Input() Res res;

  ResComponent();

  void destroy(Event event) {
    event.preventDefault();
    new Thread().removeRes(this.res);
  }
}
<div class="panel panel-default">
    <div class="panel-heading">
        ID: {{res.id}} 投稿日時: {{res.postedAt.toString()}} <a (click)="destroy($event)" class="btn btn-default btn-sm">削除する</a>
    </div>
    <div class="panel-body" [innerHtml]="res.body|nl2br">
    </div>
</div>

これは結構ノウハウが詰まったComponentなんで細かく説明します。

directivesだけじゃなくpipesも宣言が必要

まず、directivesにHTMLで利用しているNgIfを追加しています。
次にほげほげ\nふがふがほげほげ<br>ふがふがにするためのNl2BrPipeを利用することをpipesで宣言しています。ディレクティブと同じように、利用するPipeも個別のコンポーネント毎に宣言が必要なようです。

@Input()アノテーションが無いと値を渡せない

つぎに @Input() Res res; についている @Input() アノテーションが重要で、
これがあることで ThreadComponent から <my-app-res [res]="res"> のように、まるでComponentの引数のようにプロパティをセットする事ができるようになります。このアノテーションを設定していないと、利用する側が [res]="res"のように記述したとしてもComponentに渡ることはありません。

HTMLをテンプレートに突っ込みたい場合は[innerHtml]

Nl2BrPipe実装を見ると分かるんですが、HTMLエスケープ+改行をBRタグに変換した HTML文字列 を返してくれるのですが、それをAngularのテンプレート(HTML)上に直接埋め込もうとすると(例: <div>{{res.body | nl2br}}</div>)、HTML文字列をさらにHTMLエスケープしてしまいます。こういった場合は以前は ng-bind-unsafe-html のような機能を利用したりして埋め込んでいたようなのですが、現在は [innerHtml]="res.body|nl2br" のように、DOMのinnerHtmlに突っ込むのが割と正しいアプローチのようでした。

ぐぐると [inner-html] とかいろいろでてきますが [innerHtml] が正しいです。

イベントは$event変数を渡してあげればオッケー

最後に (click)="destroy($event)" とそれから呼び出される destroy(Event event) を見てもらえると分かるように、クリックイベントをキャンセルさせたい場合は $event を渡してあげることで、event.preventDefault()が呼び出せるようになるようです。詳細は分かっていないのですが、この $event 変数は特に明示的に宣言せずに使える変数になっているようです。

データを消せばDOMも消える

なお、 new Thread().removeRes(this.res) を実行することで、Thread.resList からこのResComponentが持ってるResが削除され、当然なが同じListを参照している ThreadComponent.resList からも消えますので、この ResComponent 自体も削除される、といった処理の流れになり、この削除ボタンが動作します。

明示的にDOMの削除のような事は一切せずに、データストアである Thread からデータを1件削除するとそのデータを表示していたDOMがちゃんと消えるあたり、ナウい感じはします。

PostComponent

@Component(
    selector: 'my-app-post',
    templateUrl: 'post.html'
)
class PostComponent {
  Thread _thread = new Thread();

  PostComponent();

  void postNewRes(Event event, TextAreaElement resInput) {
    event.preventDefault();
    String resBody = resInput.value;
    resInput.value = '';
    this._thread.addRes(resBody);
  }
}

<form (submit)="postNewRes($event,resInput)">
    <div class="form-group">
        <label for="resInput">投稿内容</label>
        <textarea #resInput id="resInput" class="form-control" rows="5"></textarea>
    </div>
    <button class="btn btn-block btn-primary">書き込む</button>
</form>

投稿フォーム部分で使ったノウハウとしては<textarea #resInput> のように #resInput でDOMを変数化(のようなイメージ)でき、postNewRes($event, resInput) のように関数呼び出し時にそのDOMのオブジェクトを渡すことができるようになります。

ここは本来は入力中の文字列をデータバインディングする設計のほうが美しいのですが、何かうまいこといかなかったのでDOMを直接Component側が操作するような対応になっています。

ComponentがDOMを操作する事は許されることでしょうし、必要不可欠な場面もあるとは思うのですが、やらなくてすむならやりたくないですね。

Nl2BrPipe

@Pipe(name: "nl2br")
@Injectable()
class Nl2BrPipe implements PipeTransform {
  final _regexp = new RegExp('(\r\n|\n\r|\n|\r)');

  dynamic transform(dynamic value, List<dynamic> args) {
    return HTML_ESCAPE.convert(value).replaceAll(this._regexp, '<br>');
  }
}

以前は class HogePipe extends Pipe {} のような宣言方法だったようですが、現在はこのように @Pipe() アノテーションをつけたうえで PipeTransform をimplementsするのが正しいようです。また、Componentのpipesに渡せるようにするために@Injectable()アノテーションも併せて宣言しておく必要があるようでした。

感想

1年前と比較して、それほど開発しやすくなった印象はありませんでした。
CHANGELOGを見てもBREAKING CHANGESがかなり並んでますし。

開発しづらい理由

特にとっかかりとして開発しづらい理由は明確で、 最新のDartの資料が無い ことにつきます。
ドキュメントとしてはTypeScriptが一番充実しているので、TypeScriptのドキュメントを参考にしながら紐解いていく感じになります。
とはいえそもそも公式ドキュメントの言葉足らず感がパネェので、結局はググることになるんですが、その際に出てくるものの大半が 「 既にBreakingChangeされた情報 」「 Angular1.xの情報 」「 JSあるいはTypeScriptにおける情報 」の3種類で構成されており、今まさに必要な 「 Angular2.xのDartの場合の情報 」を拾ってくる難易度が超高いです。

Angular2の出来の良さ

ある程度分かってみれば、非常に洗練された設計になっているし、セーフティだし、パフォーマンスも想像以上に良いしで、かなり良いフレームワークに仕上がってきている印象は受けるのですが、いかんせん「 こうしたい場合にどうしたらいいの? 」を探しだすコストがパネェので、早くBREAKINGなCHANGESが無くなってくれて、Angular2の確かな情報が出回る世界になるといいなぁと思います。

いまとこれからザックリまとめ

  • 悪くない(他のDartのクライアントサイドフレームワークと比べると非常に良い出来)
  • あるていど走りだしが辛いのは覚悟が必要
  • 頑張って手に入れたノウハウは枯れる前に使えなくなる覚悟も必要
  • 普通に考えてDeveloper Previewじゃなくなるまで待てばいいんじゃないかな

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
What you can do with signing up
3