LoginSignup
30
28

More than 5 years have passed since last update.

TODOリストを作って理解するAngular2

Last updated at Posted at 2016-01-04

はじめに

Angular2 Dart版で作成した単純なTodo Listアプリの動作を確認し、そのコードの解説を通じてAngular2の基本を見ていきます。

この記事は、Angular2がbeta版になった契機に私が年末年始にAngular2の学習を開始した上で、この記事を下敷きにして、せっかくなのでコードをDart版に変更し、一部内容を加筆したものです。もし誤りを発見されましたらコメント欄にでもお知らせください。

TypeScript版の解説を読みたい方、または簡潔な説明を読みたい方は元の記事を読んでください。

Angular2 Dart

Angular2のDart版は、基本的にはTypeScriptで開発されているAngularをDartにトランスパイル(ts2dart)したものであるようで、APIがほぼ同じです。(現時点では開発の進捗状況によって、部分的にDart版のAPIの変更が遅れたり、逆に一部の機能がDart版のみ実験的にサポートされたりすることもあるようです。)したがって、TypeScript版を学べばDart版でも違和感なく開発でき、その逆も成り立つことが期待できます。

また、Dartはセマンティクスがシンプルで学習コストが低いので、TypeScriptやJavaなどの言語に慣れていればDartを学ぶことは容易です。さらに、SDKが充実、安定しており、特にStreamasync/awaitといった非同期APIやZoneの導入等、非同期プログラミングのサポートが手厚い言語です。

既存のプロジェクトをAngular2にアップグレードする場合は現時点ではES6、ES5やTypeScriptを採用するのが妥当ですが、新規プロジェクトの場合はDartの採用を検討してみるのも良いかもしれません。

なお、Dartで書いたサンプルコードはGithubに置いてあります。
https://github.com/ntaoo/angular2_dart_todo/releases/tag/20160104-qiita

Todo Listの動作を確認する

Dart SDKのインストール

まだDart SDKがインストールされていない場合は、以下に従いインストールします。https://www.dartlang.org/downloads/
Macの場合はHomebrewでインストールできます。

brew tap dart-lang/dart
brew install dart --with-content-shell --with-dartium

動作確認

angular2_dart_todoをcloneします。

git clone https://github.com/ntaoo/angular2_dart_todo.git

pub get

pub getしてください。pub getnpm installのようなものです。

pub get

Development server

次に、pub serveしてください。Dartプロジェクト用dev serverが立ち上がります(デフォルトではhttp://localhost:8080)。

pub serve

Dartium

Dartプロジェクトでは、開発用ブラウザとしてDartiumを使用します。Macの場合は以下のコマンドでDartiumがchecked modeで起動します。

DART_FLAGS='--checked' open /usr/local/opt/dart/Chromium.app

実際の開発ではWebStormなどのIDEからDartiumを起動させるなどすると良いでしょう。

http://localhost:8080にアクセスすると、以下の画面になるはずです。

Screen Shot 2016-01-02 at 12.15.20 AM.png

ちなみに、Chrome等のモダンブラウザでURLを開くと、dev serverは必要に応じて自動的にJSへのコンパイルを行います。そのため、表示まで多少(数十秒ほど)時間がかかります。

Screen Shot 2016-01-01 at 11.22.52 PM.png

DartVMが載っているDartiumを利用するとDartコードをJSにコンパイルしなくて良いので、一瞬でbuildが終わります。コンパイル時間を意識することはありません。そして単にDartiumのタブをreloadするだけで再buildが行われ、非常に素早くコード変更のサイクルを回すことができます。Debuggerの利用もできるため、開発にはDartiumを利用しましょう。

せっかくなのでBuildしてみる。

デプロイにあたってはDartをJavaScriptにコンパイルする等、buildを行う必要があります。せっかくなので確認しておきましょう。pub buildでDartからJSへのコンパイルを含むbuildが完了します。

pub build

てっとり早くローカルサーバーを立ち上げて確認してみます。

cd build/web
python -m SimpleHTTPServer

モダンブラウザで正常に動作するはずです。

build/web/main.dart.jsファイルに、コンパイルされた、アプリケーション全体のJavaScriptコードが一つのファイルにまとめられています。このmain.dart.jsは460KBで、圧縮して127KBです。これはDartとAngular2のようなリッチなフレームワークにしてはかなりリーズナブルなサイズであると言えるのではないでしょうか。
AngularチームはDart版のJSへのコンパイル後のコードサイズを小さく保つことに気を払っており、Production Readyの際には現時点より更に小さくなることが期待できます。


Tips: 一からDartプロジェクトを作成するには

一からDartプロジェクトを始めるにあたっては、Stagehandを使うのが便利です。これはYeomanのようなproject generatorです。

また、一から手で作成していくのも難しくありません。例えば、mkdir angular2_todo && cd angular2_todoして、pubspec.yamlファイルを作成して以下の内容を書きます。

pubspec.yaml
name: todo_app
version: 0.1.0
dependencies:
  angular2: 2.0.0-beta.0
  browser: ^0.10.0
  dart_to_js_script_rewriter: ^0.1.0
transformers:
- angular2:
    entry_points: web/main.dart
- dart_to_js_script_rewriter

そしてpub getしましょう。このコマンドは、pubspec.yamlで宣言した依存packagesをプロジェクトで使用可能にするようセットアップします。
あとは、プロジェクトディレクトリ直下にweblibディレクトリを作成すれば、最低限のDartアプリケーションプロジェクトの構成が整います。そしてweb/index.htmlを用意し、pub serveすればdev serverが立ち上がります。シンプルですね。


Bootstrap(ここからが本題)

ここからが本題です。コードを見ていきましょう。

注意

Angular2は2016年1月時点でベータ版のため、APIに破壊的変更が入る可能性があります。


lib/todo_list.dartです。今回のアプリケーション全体のroot componentです。

lib/todo_list.dart
library todo_app.todo_list;

import "package:angular2/angular2.dart";

@Component(
    selector: 'todo-list',
    templateUrl: './todo_list.html',
    styleUrls: const ['./todo_list.css']
    
class TodoList {
}

import "package:angular2/angular2.dart";でAngular2の機能が一通りimportされます。
TodoListclassに@ComponentAnnotationが付与されています。AnnotationはDartの機能で、Angular2ではここでTodoListclassはAngularのComponentであると宣言し、メタデータを付加します。

(TypeScript版ではDecoratorが使用されています。少なくともAngular2においては、{}やconst宣言の有無の違いはあるにせよ、文法は概ね同じであるようです。)

ここではselector:で、css selectorの文法で、Componentに対応するElementが<todo-list>であることを宣言しています。同様に、templateUrl:styleUrls:でtemplateとstyleのurlを宣言しています。

(CSSの代わりにSASSやLESSなどを使用することは可能ですが、そのためにはbuild processを編集する必要があります。おそらくtransformerに指定を加える等、ノウハウがこれから出てくるでしょう。また、PolymerのようにCSS variablesを採用するのも良いかもしれません。)

<todo-list>はこのアプリ全体を含むComponentを表すElementです。どのようなboilerplateコードを書いてこのComponentをloadしているのでしょうか。
web/index.htmlweb/main.dartを見てみます。

index.html
<head>
  ......
  <script defer src="main.dart" type="application/dart"></script>
  <script defer src="packages/browser/dart.js"></script>
</head>
<body>
  <todo-list>Loading...</todo-list>
</body>

(packages/browser/dart.jsは、開発時にDartium以外のBrowserで動作確認する際に、DartファイルのpathをコンパイルしたJSファイルのpathに動的に置き換える小さなscriptです。開発の際に気にする必要はありません)

main.dart
library todo_app.web;

import 'package:angular2/bootstrap.dart';
import "package:todo_app/todo_list.dart";

main() => bootstrap(TodoList);

angular2/bootstrap.darttodo_list.dartをimportし、TodoList componentをこのAngular2アプリケーションのroot componentとして指定してbootstrapしているだけのシンプルなものです。(後の説明で、ここにDependency Injectionを追加します。)

なお、package:angular2/bootstrap.dart';@deprecated宣言されているので、近々APIが変わる見込みです

このAPIの詳細はpackage:angular2/platform/browser.dartのコメントやangular.ioのドキュメントを確認するなどすると良いでしょう。Angular1との違いやAngular2アプリケーションの設計思想、複数のAngular2アプリケーションの混在についても説明されています。

なお、TypeScript版とは異なり、DartではSystemJSのようなソリューションは必要なく、単にDartのPackage管理システムを利用するだけです。

Web Components

さて、Angular2のComponentは、もしかしたらWeb Componentなのでしょうか?
Dartium、またはChromeのInspectorを開いて確認してみます。

Screen Shot 2016-01-02 at 6.57.51 AM.png

ShadowDOMは見当たりません。代わりに、<todo-list>elementには-ghost-yfy-1、child elementsには_ngcontent-yfy-1という奇妙なattributeが付与されています。

Angular2はWeb Componentsのカプセル化をShadow DOMを使わずに擬似的に実現しています。対象elementとそのchildrenそれぞれに"代理ID(surrogate id)"情報を含むAttributeを付与し、style ruleをpre-processingしてからDOMにinsertしています。
Screen Shot 2016-01-02 at 7.26.22 AM.png
.todoappというセレクターは、実行時に.todoapp[_ngcontent-yfy-1]に変更されていることが確認できます。

3つのモード

Angular2では擬似的にWeb Componentのメリットを実現していることがわかりました。ではAngular2 Componentを本物のWeb Componentにできるのでしょうか?
すこしAngular2 Dartの実装をのぞいてみます。

angular2-2.0.0-beta.0/lib/src/core/metadata/view.dart
/**
 * Defines template and style encapsulation options available for Component's [View].
 *
 * See [ViewMetadata#encapsulation].
 */
enum ViewEncapsulation {
  /**
   * Emulate `Native` scoping of styles by adding an attribute containing surrogate id to the Host
   * Element and pre-processing the style rules provided via
   * [ViewMetadata#styles] or [ViewMetadata#stylesUrls], and adding the new Host Element
   * attribute to all selectors.
   *
   * This is the default option.
   */
  Emulated,
  /**
   * Use the native encapsulation mechanism of the renderer.
   *
   * For the DOM this means using [Shadow DOM](https://w3c.github.io/webcomponents/spec/shadow/) and
   * creating a ShadowRoot for Component's Host Element.
   */
  Native,
  /**
   * Don't provide any template or style encapsulation.
   */
  None
}

Angular2はTypeScriptで開発されてDartにトランスパイルされてpub packageとして提供されています。トランスパイルされたコードもこのようにソースコードの可読性が保たれ、コメントも適切にメンテナンスされていることが期待できます。


enumでモードが定義されています。
つまり、Angular2 Componentのカプセル化には3つのモードが存在します。

  • Emulated: 上記説明の通り、擬似的なカプセル化を行う。
  • Native: レンダラーの提供するカプセル化を利用する。(DOMの場合はShadow DOM)
  • None: templateとstyleのカプセル化を行わない。

DefaultはEmulatedです。ですので、なにも指定していない先程の例では、Emulatedモードが適用されていました。

では試しにencapsulation: ViewEncapsulation.Nativeを指定してみます。

lib/todo_list.dart
@Component(
    selector: 'todo-list',
    templateUrl: './todo_list.html',
    styleUrls: const ['./todo_list.css'],
    encapsulation: ViewEncapsulation.Native,

Chromeで開くと、あの奇妙なattributeが存在しなくなり、代わりにShadowRootが現れました。大変きれいでいいですね!

Screen Shot 2016-01-02 at 6.37.50 AM.png

しかし、Web Components技術、要するにShadow DOMを実装していないブラウザだとどうなるのでしょうか?

試しにFirefoxで開いてみると、案の定、
EXCEPTION: NullError: receiver.webkitCreateShadowRoot is undefined
とエラーが出てアプリケーションが正常に起動しません。実装していないので当たり前ですね。Angular2内部でWeb ComponentsのPolyfillを使っているわけではないということも確認できました。

どのモードを指定するべきか

Web Componentsの進捗はざっくり言うと2015年春から秋にかけて各ブラウザーベンダー間の合意が大きく進み、特にSafariにShadowDOMの実装が進められているという明るいニュースがあり全体としては大いに希望が持てるものの、2016年1月の時点では未だにChrome系でしかサポートされていません

したがって、Chrome系しかサポートしなくて良い特殊な事情か、ViewがDOM以外である(これはまだよく調べていないが、例えばNativeScriptを想定しているのだろうか?)といった事情がない限り、ViewEncapsulation.Nativeは現時点では指定するべきではありません。ShadowDOM Polyfillも使うべきではないでしょう。

全てのモダンブラウザでCustom Elements、ShadowDOMの実装が完了し、ブラウザの実装バグも取れて状況が落ち着くまでは、DefaultのViewEncapsulation.Emulatedがやはり妥当です。

また、自分は動作を確認していませんが、ViewEncapsulation.Noneという選択は、組織とプロジェクトの状況によっては、CSSのBEMなどの命名規則を守る方針の下でAngular2の導入コストを下げるという目的ならばありなのかもしれません。

シンプルなData Binding

Angular2ではData Bindingがシンプルになって、動作の理解が簡単になりました。

Property Binding

まずは単にDartのclasspropertyを追加します。Dartのpropertyの文法そのものですね。Angular1とは異なり、scopeのコンセプトはありません。その代わりにComponentが対応するViewのcontextになります。

lib/todo_list.dart
class TodoList {
  String newItem = 'test';

そしてそのpropertyをDOM elementのpropertyに以下のように[value]="newItem"とすることでbindします。

lib/todo_list.html
<input class="new-todo" [value]="newItem">

(実際のコードは[(ngModel)]="newItem"で双方向に(Bidirectional)Bindingをしていますが、説明のため一時的なコードを提示しています。)

これだけでinput elementのvalue propertyにdata bindできました。

Screen Shot 2016-01-02 at 12.15.20 AM.png

Propery bindingでは、dataはComponentから対応するViewに常に一方向に(Unidirectional)流れます。

Event Binding

Event Bindingもシンプルです。TodoList classのmethodをDartの通常の文法で書きます(addItem())。

todo_list.dart
class TodoList {
  String newItem = 'test';

  addItem() {
    this.store.addItem(this.newItem);
    this.newItem = '';
  }

そして以下のように(click)="addItem()"と書けばaddItem()がclickイベントにbindされます。

todo_list.html
<button class="add" (click)="addItem()">Add</button>

Event bindingでは、dataはViewから対応するComponentに常に一方向に(Unidirectional)流れます。

双方向Binding

Property bindingは[]記法で書き常にComponentからViewにdataが流れ、Event bindingは()記法で書き常にViewからComponentにdataが流れます。

さて、ユーザーのinputの更新をnewItem propertyに反映させたい場合は、(input)="newItemChanged($event)"とevent bindingするのが良さそうです。($eventはDOMのeventです。event bindingのexpressionには$eventを引数に取る事ができます。)

<input class="new-todo"
      [value]="newItem" (input)="newItemChanged($event)">
newItemChanged(KeyboardEvent event) {
  InputElement target = event.target;
  this.newItem = target.value;
}

これで結果的に双方向(bidirectional)Bindingを実現できました。

ローカルテンプレート変数

先ほどの例で、expressionがDOMのEventを引数に取ることができることを説明しました。

<input class="new-todo"
      [value]="newItem" (input)="newItemChanged($event)">

EventはDOM APIなので汎用性があるのですが、取り回しと型チェックがやや面倒なのも事実です。もっと簡単にできないでしょうか。ローカルテンプレート変数を使うと簡略化できます。

<input class="new-todo"
      #todoInput [value]="newItem" (input)="newItemChanged(todoInput.value)">

#todoInputと書くことで、todoInputという変数を宣言することができます。ここではInputvalue propertyをtodoInput.valueで参照することができました。

以下はExpressionに対応するTodoList classのmethodです。

newItemChanged(String value) {
  this.newItem = value;
}

Keyboard eventからevent targetのvalueを取り出すといったEventを取り回すコードをなくすことで、シンプルで見通しの良いコードになりました。

NgModel

さらに、これを[(ngModel)]で簡略化することができます。

<input class="new-todo" [(ngModel)]="newItem">

これだけです。Dart側のmethodも不要になりました。[(ngModel)]はPropery BindingとEvent Bindingの組み合わせのSyntactic Sugarで、特にinputを処理する際に便利です。[(ngModel)]の内部構造を知りたい場合はANGULAR 2 TEMPLATE SYNTAXを読むかソースコードを読むなどしてください。

擬似イベント

Todo Itemを追加する操作として、Addボタンを押す他に、Enterキーを押す操作もサポートするべきでしょう。Dartコード側でkeyupイベントのキーコードをfilterするのもDartのEvent Streamのおかげで書くのは簡単なのですが、Angular2ではさらに簡便な方法を用意してくれています。

todo_list.html
<input class="new-todo" [(ngModel)]="newItem" (keyup.enter)="addItem()">
<button class="add" (click)="addItem()">Add</button>

ここでは、(keyup.enter)と書くだけで、キーコードをfilterしてくれます。便利ですね。

なぜData Bindingの文法をAngular1から変えたのか

この決断の背景は非常に面白いので、ng-conf 2015のkeynoteを見てください。(1年近く前の情報です。自分は最近までチェックしていなかったことを後悔しました。)

要点の一部を抜き出すと、

  • Html AttributeはDOM Propertyの値がserializeされたもので、その後にPropertyの値が更新されてもAttributeの値は更新されないケースもある。
  • Attributeと異なりPropertyはserializeしなくても良い。よってmodelの値を型を保持したまま子componentに渡すことができる。{{}}expressionのserializeが不要になる。
  • ng-*の多数のdirectiveが必要なくなり、DOM propertyにbindするだけで良くなる。

Screen Shot 2016-01-02 at 9.16.49 AM.png

余談ですが、設計思想の違いがあるので単純に比較してもあまり意味はありませんが、ComponentはAngular2のほうがPolymer1よりもよりもかなり書きやすい印象です。

Storeを追加する

ではここからModel層を追加します。ここではFluxパターンのコンセプトを一部借用して、Storeを追加します。Storeはビジネスロジックを処理し、データを保持します。(ここではオリジナルの記事の考えとコードを尊重しそれに合わせるため、Storeと名づけています。)

class TodoItem {
  String text;
  TodoItem(this.text);
}

class TodoStore {
  List<TodoItem> items;

  TodoStore() : this.items = [];
}

さしあたってはTodoList Componentに直接インスタンス化しておきます。後にDIする方法に改良します。

import './todo_store.dart' as model;

final store = new model.TodoStore();

@Component({ ... })
class TodoList {
  String newItem = 'test';
  model.TodoStore store;
  // 略
}

なお、この単純なアプリケーションにおいてActionを定義してDispatherを追加し、それを経由してStoreを駆動させるのは大げさすぎてサンプルとして適当でないので、ここは簡略化して、StoreはTodoList Componentから直接messageを受ける実装に留めています。

このTodoStoreが保持するTodoItemのListをngForでレンダリングします。

<section class="main">
  <ul class="todo-list">
    <li *ngFor="#item of items">
      <div class="view">
        <label></label>
      </div>
    </li>
  </ul>
</section>

(Dart版では、ofではなく#item in itemsと書けるようになって欲しいですね)

DI (Dependency Injection)

TodoStoreはSingletonであることが求められます。Angular1ではServiceを作りControllerにDIすることで実現していました。Angular2でもDIしましょう。

TodoList classにmodel.TodoStore store propertyを追加し、constructorに指定を追加しています。

import './todo_store.dart' as model;
@Component(
  // 略
)
class TodoList {
  String newItem = 'test';
  model.TodoStore store;

  TodoList(this.store);
  // 略
}   

なお、TodoList(this.store);は、

TodoList(model.TodoStore store) {
  this.store = store;
}

と読み替えることができます。

そしてbootstrap()TodoStoreを加える事で完了です。

main.dart
library todo_app.web;

import 'package:angular2/bootstrap.dart';
import "package:todo_app/todo_list.dart";
import 'package:todo_app/todo_store.dart';

main() => bootstrap(TodoList, [TodoStore]);

詳細はDEPENDENCY INJECTIONを参照してください。

TodoItem追加機能

Storeを追加したので、Todo Itemを追加する機能を加えます。

todo_list.dart
@Component(
  // 略
)
class TodoList {
  String newItem = 'test';
  model.TodoStore store;

  TodoList(this.store);

  addItem() {
    this.store.addItem(this.newItem);
    this.newItem = '';
  }
  // 略
}
todo
class TodoStore {
  // 略
  addItem(String newItem) {
    this.items.add(new TodoItem(newItem));
  }
 // 略
} 

これだけで、Storeへのitemの追加によるstore propertyの更新、そしてnewItem propertyの更新がViewに反映されました。

Screen Shot 2016-01-03 at 9.40.28 AM.png

Angular2は、いつ、どのようにデータの更新をViewに反映するのか?

Angular2はいつ、どのようにデータの更新をViewに反映するのでしょうか?

Angular1では、ScopeとDigest Cycleの概念を用いてデータの更新管理をしていました。これはシンプルなアプリケーションには便利ですが、アプリケーションのサイズが大きくなり管理すべきデータが増えるにつれ無駄な更新チェックが増えて性能が劣化したり、複雑になりがちなDigest Cycleを制御するためにプログラマーがヒューリスティックにDigest Cycleを回すコードを書くケースも有るなど問題を孕んでおり、特に性能の低いモバイル端末では問題になりやすいものでした。

Angular2ではこの問題を解決するために、新たにChange Detectionと名づけた仕組みを導入しています。これにより、"Digest Cycle"的なものの隠蔽と劇的な性能向上を実現しています。

このChange Detectionは、"Zone"(JS,Dart)を用いてBrowserの非同期APIにパッチを当て、DOM eventやHttp request等の各event周辺で自動的に適切に実行される設計になっているようです。そして、Angular1とは異なり、常にRoot Componentから深さ優先探索でConponent treeを捜査してPropertyにbindされている値をチェックし、Viewを更新します。(注:詳細は要確認)。

また、Developmer modeというものがあり、プログラマーがChange Detectionによる更新管理を破壊するコードを書くと、実行時にエラーを吐いてプログラマーに修正を促してくれるようです。この辺りの動作は要確認です。

Change Detectionのさらなる解説は、Angular2のChange Detectionについて
およびCHANGE DETECTION IN ANGULAR 2等を参照してください。

子componentの追加

現実のAngular2アプリケーションは多数のComponentが木構造を構成するはずです。このTodoListのサンプルでも、子componentの例を説明するため、Root ComponentであるTodoListしかない現在の構成を変更してみます。

todo_list.htmlの一部の、

lib/todo_item.html
<div class="view">
  <label></label>
</div>

をView templateにし、TodoItem componentを作成します。

lib/todo_item.dart
import "package:angular2/angular2.dart";
import "./todo_store.dart" as model show TodoItem;

@Component(
    selector: 'todo-item',
    templateUrl: './todo_item.html',
    styleUrls: const ['./todo_item.css']
)    
class TodoItem {
  @Input()
  model.TodoItem item;
}

@Input() annotationに注目してください。このannotationが付けられたpropertyは、このcomponentのpublic APIとなり、bindingが可能になります。実際に<todo-item>に置き換えた例が以下です。

todo_list.html
<li *ngFor="#item of store.items">
  <todo-item [item]="item"></todo-item>
</li>

[item]="item"において、@Input() annotationによってTodoItem componentのpublic propertyとして宣言されたitemが、StoreのTodoItem instanceにbindingされています。

Deleteボタンの追加

子componentのpropertyにbindingする例は確認できました。次はDeleteボタンの追加を通じて、componentをDOM elementとみなし、componentからeventを発火させる例を見てみます。

lib/todo_item.html
<div class="view">
  <label>{{item.text}}</label>
  <button (click)="doneClicked()" class="destroy"></button>
</div>

TodoItem componentのtemplateにDeleteボタンを追加します。

lib/todo_item.dart
library todo_app.todo_item;

import "package:angular2/angular2.dart";
import "./todo_store.dart" as model show TodoItem;

@Component(
    selector: 'todo-item',
    templateUrl: './todo_item.html',
    styleUrls: const ['./todo_item.css']
)    
class TodoItem {
  @Input()
  model.TodoItem item;

  @Output()
  EventEmitter<model.TodoItem> done = new EventEmitter();

  doneClicked() {
    this.done.add(this.item);
  }
}

ここで、<todo-item>からeventを発火させるために、@Output()が付加されたEventEmitterを追加しました。そして、DeleteボタンをクリックするとこのEventEmitterがeventを発火させるmethodをbindingしています。

これで、done eventがTodoItem componentから発火するようになったので、親componentのTodoList componentでこのdone eventをbindingしましょう。

todo_list.html
<todo-item [item]="item" (done)="removeItem($event)"></todo-item>
todo_list.dart
removeItem(model.TodoItem item) {
  this.store.removeItem(item);
}

ここで、TodoStoreにmodel.TodoItemを削除させます。

これでDeleteボタンの実装は完了です。

Change Detectionのさらなる効率化

実際に、Change Detectionがいつ実行され、Propery Bindingを通じてStoreのmodel objectであるTodoItemのデータにアクセスするのか確認してみましょう。

lib/todo_item.html
<div class="view">
  <label>{{item.text}}</label>
  <button (click)="doneClicked()" class="destroy"></button>
</div>

ここで、DartのSetterとGetterの機能を使い、textにアクセスするたびにconsoleにlogを出すようにします。

todo_store.dart
class TodoItem {
  String _text;

  get text {
    print('getting value for text ${this._text}');
    return this._text;
  }

  set text(String value) {
    this._text = value;
  }

  TodoItem(this._text);
}

(print('getting value for text ${this._text}');に注目してください。)

Browserで動作を確認してみます。

Screen Shot 2016-01-04 at 10.21.37 AM.png

Input fieldにKey入力するたびに大量のlogが吐かれてしまいました。つまり、ここでChange DetectionはKey入力するたびに全てのTodoInputのtextの値をチェックし、変更がないか確認しています。これは実際のTodo Listアプリケーションの動作から考えると明らかに不要なチェックであり、非効率に思えます。

Angular2はBrowserのeventの際に毎回Change Detectionを実行し、深さ優先探索でデフォルトでは全てのComponentを捜査します。これは非効率に思えますが、実際はそのパフォーマンスは非常に高いため殆どのケースでは問題にならないようです。とはいえ、明らかに不要なチェックは省略できないでしょうか。

イミュータビリティ

Angular2のComponentはそれぞれにChangeDetectionStrategyを指定する事ができます。この指定によってより効率的なチェックにすることが可能です。

今回のTodo Listアプリケーションの仕様では、TodoItem componentのデータはcomponent生成後に更新されることはないため、実質的にイミュータブルであるとみなすことができます。そのためComponentの生成後はChange Detectionのチェックをスキップさせて問題ないと判断できます。ChangeDetectionStrategyOnPushに変更することでそのように指示しましょう。

lib/todo_item.dart
@Component(
    selector: 'todo-item',
    // 略
    changeDetection: ChangeDetectionStrategy.OnPush)
class TodoItem {
  // 略
}

Screen Shot 2016-01-04 at 11.13.19 AM.png

これでKey入力のたびに無駄に各TodoItem componentをチェックされることがなくなり、生成時に一度だけチェックされるようになりました。

さらなる学習には以下の資料が参考になります。
Angular2のChange Detectionについて(再掲)
CHANGE DETECTION IN ANGULAR 2(再掲)
ANGULAR, IMMUTABILITY AND ENCAPSULATION

Change Detectionの実装を探検してみるのも面白いかもしれません。

まとめ

ベータ版が出てから一気に学んだ印象では、Angular2は文法と動作を理解しやすくかつ素晴らしく効率的なWebフレームワークです。また、これも個人的な印象ですが、Angular2はAngular1時代に感じたフラストレーションを解消して自分のメンタルモデルに自然にフィットする感覚で、書いていて気持ちが良いです。

Angular2とImmutableライブラリやFluxパターンとの組み合わせなど、これからプラクティスが沢山出てくるでしょう。自分もAngular2を追っていく中で紹介していけたらと思います。

また、Dartは言語そのものは生産性が高くて素晴らしいのですが、Reflection(Mirror)APIを使うと注意深く設定しなければJSへのCompile時にTree Shakingが失敗してコードサイズが膨れ上がるという問題を抱え、私見では恐らくそれが大きな要因でなかなか信頼の置けるWebフレームワークが確立されないという状況でした。Reflection問題はReflectableで解決が期待されますが、Angular2では内部でこの問題に独自のReflectable的な仕組みを導入するなどJSへのコンパイル後のコードサイズを小さく保つことに気を払っており、DartにとってはAngular2がキラーフレームワークとなる期待が持てます。

2016年は、Angular2と願わくばDartでReactiveなWebApp開発が盛り上がる年になったらいいですね。

個人的なTODO

  • Client side routing (For SPA)
  • Dart版ののhttp moduleについて。Dart SDKのものをそのまま使うのか各自工夫するのか、それともAPIをTypeScript版に合わせた薄いwrapperが提供されるのか。
  • Angular Material for Angular2の進捗(pub packageとしてすでにbeta versionが提供されているが、まだ初期段階に見える)
  • Angular2は必要に応じてPolymer Element等のWeb ComponentをProperty BindingやEvent Bindingで取り扱うことができるが、例えばPolymerDartとの組み合わせは実際に実用的なのかどうか。
  • Angular2独自のreflectableの仕組み
  • Angular2のtransformerの仕組み
30
28
0

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
  3. You can use dark theme
What you can do with signing up
30
28