AngularJS開発者のためのAngularDart

AngularDartはすごい勢いで進化していて、今のAngularDartはこの記事とはずいぶん違っています。元記事の内容は最新版に合わせて書き換えられているので取り急ぎそちらを見たほうがいいです。時間が取れたら訳文も直します。(2014-06-06追記)


みんな大好きangular.jsのdart版がangularチームによって絶賛開発中なんですが、これが単なるJS版の移植ではなくて、ちゃんとdart用に新たに練りなおしてる感じなわけです。

そんなAngularDartをAngularJSと比較した紹介記事がよかったのでちょっと訳してみました。個人的にはDart版の方が黒魔術分が減ってていい感じなんじゃないかと。

記事に書いてあるけど、AngularDartの機能の幾つかはAngularJSに逆輸入されるらしいので、Dartなんか知らんわって人も読んでおいて損はなさそう。

http://victorsavkin.com/post/72452331552/angulardart-for-angularjs-developers-introduction-to



AngularJS開発者のためのAngularDart - 最高のAngularの紹介

AngularDartは高い評価を受けているフレームワークDartプラットフォームへの移植版で、Angularコアチームによって開発されています。この記事ではこのフレームワークDart版とJS版を比較します。中でも特に依存性の注入、ディレクティブ、digestingについて詳しく述べます。

想定される読者

この記事は次のような人に向けて書かれています:

  • いくらかのAngularJSの経験を持ったDart開発者。
  • AngularDartを試してみたいと考えているAngularJS開発者。
  • Dartに切り替えるつもりはないが、このフレームワークの未来について知りたいと思っているAngularJS開発者。Angular開発陣によると、AngularDartの機能の多くは将来的にAngularJSに移植されるそうです。そのため、このフレームワークDart版について学ぶことは、実際に使う予定がないとしても興味深く感じられることでしょう。

依存性の注入

名前に基づいた注入と型に基づいた注入

AngularDartはDartのオプショナルな型システムを興味深い形で利用しています。つまり、型情報をインジェクタを設定するために利用します。言い換えると、注入は名前ではなく型に基づいて実行されます。

//JS:
// ここで、名前は重要です。本番環境でminifyされるので
// このように配列を使用しなければいけません。
m.factory("usersRepository", ["$http", function($http){
  return {
    all: function(){/* ... */}
  }
}]);
//DART:
class UsersRepository {
  // ここでは型だけが重要です。変数名はDIと無関係です。
  UsersRepository(Http h){/*...*/}
  all(){/* ... */}
}
注入可能なオブジェクトの登録

AngularJSでは注入可能なオブジェクトは、filter, directive, controller, value, constant, service, factory, providerなどのメソッドを使用することでAngular DIシステムに登録できます。

メソッド 目的
filter フィルターの登録
directive ディレクティブの登録
controller コントローラーの登録
value, constant 設定オブジェクトの登録
service, factory, provider サービスの登録


見て分かる通り、注入可能なオブジェクトを登録する方法がいくつもあることが開発者の混乱の種になってきました。ひとつには、filter、directive、controller関数はすべて型の異なるオブジェクトのために利用されるため、交換可能にはできなかったということがあります。しかし一方で、service、factory、provider関数は(providerが最も汎用的ですが)すべてサービスオブジェクトを登録するものです。

AngularDartはこれとは全く異なるアプローチを取りました。オブジェクトの型とDIシステムにどのように登録されるかを分離したのです。

value、type、factoryを使用すると任意のオブジェクトを登録できます。

メソッド 目的
value, type, factory あらゆるオブジェクトの登録


これは次のように利用されます。

  //DART:

  // 渡されたオブジェクトは注入に利用できます。
  value(UsersRepositoryConfig, new UsersRepositoryConfig());

  // AngularDartはすべての依存性を解決して
  // UsersRepositoryをインスタンス化します。
  type(UsersRepository);

  // AngularDartはfactory関数を呼び出します。
  // 渡されたインジェクタを使用して依存性を解決する必要があり、
  // その後、UsersRepositoryをインスタンス化します。
  factory(UsersRepository, (Injector inj) => new UsersRepository(inj.get(Http)));


これらの関数を使用するとどのようなオブジェクトでも登録できるため、APIが非常に単純になっています。

任意のクラスをサービスとして利用できます。単にAugular DIシステムを利用して登録するだけで構いません。必要になった時にAngularがそのクラスをインスタンス化して、コンストラクタ引数を通してすべての依存性を注入します。

しかし、他の型のオブジェクトについては追加の情報を与える必要があります。それにはアノテーションを使用します。

//DART:

@NgController(
    selector: '[users-ctrl]',
    publishAs: 'ctrl'
)
class UsersCtrl {
  UsersCtrl(UsersRepository repo);
}


同様に、filterやcomponent、directiveを定義するための特別なアノテーションがあります。

AngularDartでは注入可能オブジェクトの型と、どのようにそれらがDIシステムに登録されるかということは、直交した概念です。

モジュールの作成とアプリケーションの開始

以下はAngularJSでアプリケーションを作成する標準的な方法です。

//JS:
var m = angular.module("users", ['common.errors']);
m.service("usersRepository", UsersRepository);
angular.bootstrap(document, ["users"]);


これはAngularDartには次のように対応付けられます。

//DART:
final users = new Module()
  ..type(UsersRepository)
  ..install(new CommonErrors());

ngBootstrap(module: users);


同様のことをModuleを継承することによっても実現できます。

//DART:
class Users extends Module {
  Users(){
    type(UsersRepository);
    install(new CommonErrors())
  }
}

ngBootstrap(module: new Users());


実行中のプラットフォームやアプリケーションに応じて異なるコンポーネントと紐付けたいような場合には後者の方法がより適切でしょう。

注入可能オブジェクトの設定

AngularJSは注入可能オブジェクトを設定するためのオプションをいくつか提供しています。もっとも単純な方法はvalueを使用して設定用のオブジェクトを注入することです。

//JS:
m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.service("usersRepository", function (usersRepositoryConfig){ 
  //...
});


同様なことはDartでも可能です。

//DART:
class UsersRepositoryConfig {
  String login;
  String password;
}

class UsersRepository {
  UsersRepository(UsersRepositoryConfig config){/* ... */}
}

type(UsersRepository);
value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");


ではここでUsersRepositoryはhashではなく引数を2つ取り、それは変更できないものとしましょう。この場合は、factoryを使用することもできます。

//JS:
m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.factory("usersRepository", function (usersRepositoryConfig){ 
  return new UsersRepository(usersRepositoryConfig.login, usersRepositoryConfig.password);
});


AngularDart版でも、再び非常によく似たコードになります。

//DART:
value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");

factory(UsersRepository, (Injector inj){
  final c = inj.get(UsersRepositoryConfig);
  return new UsersRepository(c.login, c.password);
});


このような目的ではproviderを定義することを好む人もいるでしょう。

//JS:
m.provider("usersRepository", function(){
  var configuration;

  return {
    setConfiguration: function(config){
      configuration = config;
    },

    $get: function($modal){
      return function(){
        return new UsersRepository(configuration);
      }
    }
  };
});


setConfigurationメソッドはアプリケーションの設定フェーズ中に呼び出されなければいけません。

//JS:
m.config(
  function(usersRepositoryProvider){
    usersRepositoryProvider.setConfiguration({login: 'Jim', password: 'password'});
  }
);


AngularDartにはproviderも明示的な設定フェーズもないため、上記の例を直接Dartに置き換えることはできません。思いつく最も近い例は次のようなものです。

//DART:
final users = new Module()..type(UsersRepositoryConfig)
                          ..type(UsersRepository);

Injector inj = ngBootstrap(module: users);

inj.get(UsersRepositoryConfig)..login = "jim"
                              ..password = "password";

directive、controller、component

それでは話題を変えて、フレームワークのもう一つの柱、 directiveについて話しましょう。

AngularJSのdirectiveは非常に強力で基本的には使いやすいものですが、新しいdirectiveを定義することが混乱の元になる場合があります。思うに、Augularチームもこれは認識していて、それがこのフレームワークDart版ではAPIが大きく異る理由なのでしょう。

AngularJSにはUIのインタラクションをまとめるためのオブジェクトが2つあります:

  • directiveはDOMとのすべてのやりとりをカプセル化します。これは宣言的で、htmlを拡張する手段とみなすことができます。
  • controllerは命令的です。DOMについては意識せず、アプリケーションのロジックを扱うことができます。

AngularJSではこれら2種類のオブジェクトが区別されています。それらを登録するために異なるヘルパーが利用され、全く異なるAPIを使用して定義されます。

AngularDartに導入された1つ目の重大な変更は、これら2つのオブジェクトがより似通ったものになったということです。controllerは基本的に要素に新しいスコープを作成するdirectiveです。

2つ目の変更は新しいオブジェクト型、componentです。AngularDartでは、directiveはほとんどの場合で既存のDOM要素を拡張するために利用されます。新しい独自要素を定義するには、componentを使用します。

それでは例をいくつか見てみましょう。

directive

vs-match directiveはinput要素に適用できます。要素の変更を監視して、値が特定のパターンに一致した時にdirectiveが要素にmatchクラスを追加します。

これは次のように使用します:

<input type="text" vs-match="^\d\d$">

説明したdirectiveの非常に簡単なAngularJSでの実装は次のようになります:

//JS:
directive("vsMatch", function(){
  return {
    restrict: 'A',

    scope: {pattern: '@vsMatch'},

    link: function(scope, element){
      var exp = new RegExp(scope.pattern);
      element.on("keyup", function(){
        exp.test(element.val()) ?  
          element.addClass('match') : 
          element.removeClass('match');
      });
    }
  };
});


ではこれをAngularDart版と比較してみましょう。

//DART:
@NgDirective(selector: '[vs-match]')
class Match implements NgAttachAware{
  @NgAttr("vs-match") 
  String pattern;

  Element el;

  Match(this.el);

  attach(){
    final exp = new RegExp(pattern);
    el.onKeyUp.listen((_) =>
      exp.hasMatch(el.value) ? 
        el.classes.add("match") : 
        el.classes.remove("match"));
  }
}


順に説明します:

  • NgDirectiveはAngularにこのクラスがdirectiveであると伝えます。
  • selector属性はこのdirectiveがいつ活性化されるかを定義します。今回の例では、それは要素がvs-match属性を持つ時です。
  • 任意のserviceをdirectiveに注入できるだけでなく、directiveが適用される要素も注入できます。これがMatch(this.el)の行っていることです。
  • bindingはAngularJSと同様にマップを渡すことで設定できます。しかしこれはアノテーションを使用しても実現でき、個人的にはそちらの方がずっと読みやすく理解もしやすいと感じます。
  • directiveのコンストラクタが実行された時には、パターンの値はまだ設定されていません。これはNgAttachAwareインターフェースを実装することで解決します。NgAttachAwareインターフェースにはattachメソッドが宣言されていて、新しいdigestが発生した時に実行されます。この時点ではすべての属性のマッピングが完了しているので、安全に正規表現を構築できます。
  • 最終的に、リンクされる関数もコンパイルされる関数もありません。
component

次に見ていくことになるcomponentは、コンテンツの表示非表示を切り替えます。これは次のようにして使用します:

<toggle button="Toggle">
  <p>Inside</p>
</toggle>

このcomponentのAngularJSでの実装は次のようになります:

//JS:
directive("toggle", function(){
  return {
    restrict: 'E',

    replace: true,
    transclude: true,

    scope: {button: '@'},

    template: "<div><button ng-click='toggle()'>{{button}}</button><div ng-transclude ng-if='showContent'/></div>",

    controller: function($scope){
      $scope.showContent  = false;
      $scope.toggle = function(){
        $scope.showContent  = !$scope.showContent ;
      };
    }
  }
})


ではこれをDart版と比較してみましょう:

//DART:
@NgComponent(
  selector: "toggle",
  publishAs: 't',
  template: "<button ng-click='t.toggle()'>{{t.button}}</button><content ng-if='t.showContent'/>"
)
class Toggle {
  @NgAttr("button")
  String button;

  bool showContent = false;
  toggle() => showContent = !showContent;
}
  • NgComponentはAngularにこのクラスがcomponentであることを伝えます。
  • publishAsはテンプレートの中でトグルするオブジェクトにアクセスするために利用できる名前です。この名前は挿入されるコンテンツではなく、コンポーネントのテンプレート内でだけ利用できるということに注意してください。
  • templateは当然この独自要素をどのように描画するかを定義します。

JS版とDart版は同じように見えますが、その内側には重大な違いがあります。

AngularDartのcomponentはテンプレートを描画するためにshadow DOMを使用します。

AngularJS:

https://31.media.tumblr.com/bfd2da4f739e1e7c243cb0c663faabfa/tumblr_myzotyIoMO1qc0howo1_1280.png

AngularDart:

https://31.media.tumblr.com/b13c1a49a840a10fe4d5d65e267e95aa/tumblr_myzotyIoMO1qc0howo2_1280.png

Shadow DOMを使用するとDOMとCSSカプセル化でき、再利用可能なコンポーネントを構築するのに非常に役立ちます。APIについてもWeb componentsの仕様に沿うよう変更が加えられています(例えばng-transcludeはcontentと置き換えられました)。

AngularDartのcomponentはそのテンプレートを保持するためにtemplate要素を使用します。

これによりng-srcのようなハックが不要になりました。

まとめると、directiveはDOM要素を拡張するために使用されます。componentはWeb Componentsの軽量版で、独自要素を作成するために利用されます。

controller

以下の例はAngularJSで実装された非常に単純なcontrollerです。

//JS:
<div ng-controller="CompareCtrl as ctrl">
  First <input type="text" ng-model="ctrl.firstValue">
  Second <input type="text" ng-model="ctrl.secondValue">

  {{ctrl.valuesAreEqual()}}
</div>
controller("CompareCtrl", function(){
  this.firstValue = "";
  this.secondValue = "";

  this.valuesAreEqual = function(){
    return this.firstValue == this.secondValue;
  };
});


Dart版はこれと大きく異なります。

//DART:
<div compare-ctrl>
  First <input type="text" ng-model="ctrl.firstValue">
  Second <input type="text" ng-model="ctrl.secondValue">

  {{ctrl.valuesAreEqual}}
</div>
@NgController(
  selector: "[compare-ctrl]",
  publishAs: 'ctrl'
)
class CompareCtlr {
  String firstValue = "";
  String secondValue = "";

  get valuesAreEqual => firstValue == secondValue;
}


先に述べたように、contollerは基本的にはdirectiveで、要素上に新しいスコープを作成します。新しいdirectiveを定義するときに利用できるオプションは全てcontrollerを定義するときにも利用できます。とはいえ、フレームワークによって規定されているわけではありませんが、controller内であらゆるDOM操作ロジックを書くことを避けるのはいい考えです。

filter

最後に、どのようにしてfilterを定義できるか見て行きましょう。

//JS:
filter("isBlank", function(){
  return function(value){
    return value.length == 0;
  };
});

Dart版では:

//DART:
@NgFilter(name: 'isBlank')
class IsBlank {
  call(value) => value.isEmpty;
}

zoneと$scope.$apply

熟練したAngular開発者は次の機能をありがたく思うでしょう。この記事で紹介する内容の中で、最も衝撃的だと感じる人もいるかもしれません:

サードパーティ製のcomponentと結合するときに$scope.$applyを呼ぶ必要はありません。

次の例を使用して上記についてを説明しましょう。

<div ng-controller="CountCtrl as ctrl">
  {{ctrl.count}}
</div>

CountCtrlは単にcount変数をインクリメントするだけのcontrollerです。

//JS:
controller("CountCtrl", function(){
  var c = this;
  this.count = 1;

  setInterval(function(){
    c.count ++;
  }, 1000);
})


熟練したAngularJS開発者であればこのコードは壊れているとすぐに気づくでしょう。このままではAngularはコールバック内でcount変数が変更されたことを知ることができません。この問題を修正するには、次のように$scope.$applyで囲まなければいけません。

//JS:
controller("CountCtrl", function($scope){
  var c = this;
  this.count = 1;

  setInterval(function(){
    $scope.$apply(function(){
      c.count ++;
    });
  }, 1000);
})


これはAngularJSの原理的な制限です。あなた自身がAngularに変更をチェックするよう明示的に示さなければいけません。フレームワークはfutureライブラリをバンドルしたり、$interval serviceを提供したりして、これの作業が必要な場所を最小化しようと努力しています。しかし、他のfutureライブラリを使い始めたり、サードパーティ製の非同期componentと結合し始めると、やはり$scope.$applyを使わなければいけません。

それでは、これをDart版と比べてみましょう。

//DART:
<div count-ctrl>
  {{ctrl.count}}
</div>
@NgController(
    selector: "[count-ctrl]",
    publishAs: 'ctrl'
)
class CountCtrl {
  num count = 0;

  CountCtrl(){
    new Timer.periodic(new Duration(seconds: 1), (_) => count++);
  }
}


Dart版には$applyはありませんがちゃんと動作します。TimerはAngularについては何も知りません。すばらしいではないですか!これがどのように動作しているか理解するためには、Zoneという概念について学ぶ必要があります。

Dartドキュメント: Zoneは動的な範囲の非同期版を表しています。非同期コールバックはそれがキューに入れられたzoneの中で実行されます。例えば、future.thenのコールバックはthenが実行されたのと同じzoneで実行されます。

Zoneはイベントベースの環境でのスレッドローカル変数のようなものだと考えられるかもしれません。環境は常にcurrent zoneを持っています。そして全ての非同期操作の全てのコールバックはcurrent zoneで動作します。これによってAngularは変更をチェックすべき場所が分かります。

さらに、このメカニズムを使うことで、フレームワークはあなたのプログラムの実行についての情報を集め、例えば、長いスタックトレースを生成することができます。そうすれば例外が発生したときに、multiple VM turnsに渡ってスタックトレースを見ることができます。言うまでもなく、これは開発環境を大幅に改善するでしょう。

まとめ

  • AngularDart APIはクラスベースです。
  • このフレームワークは名前による注入ではなく型による注入を使用します。
  • オブジェクトの型はDIシステムにどのように登録されるかとは無関係です。
  • NgControllerやNgDirectiveのようなアノテーションが注入可能なオブジェクトを設定するために使用されます。
  • directive、filter、component、controller、services、全てをvalue、type、factoryを使用して登録できます。
  • directiveはDOM要素を拡張するために使用されます。
  • componentはWeb Componentsの軽量版で、独自要素を作成するために使用されます。
  • componentはテンプレートを描画するためにshadow DOMを使用します。
  • controllerはそれが適用される要素で新しいスコープを作成するdirectiveです。
  • scopeはDartのzoneを使用して自動的に組み込まれます。$scope.$applyは不要になりました。
なにがJSに移植されるのでしょう

MiskoとIgorがDevoxxで話した内容によると、これらの変更の多く、特に以下のものがAngularJSに移植される予定のようです。

  • 型に基づいた注入
  • アノテーションを使用したオブジェクトの定義
  • Shadow DOMの利用
  • Zone

より深く学ぶには

この記事があなたの入門の役に立つことを望んでいます。より深く知りたければ以下をチェックしてください: