2015/01/22

AngularJSのDIについてDeveloper Guideを読んでみた。

を纏める時に、AngularJSのDeveloper Guideを読んだわけですが、AngularJSがどのようにDIを実現しているか、についても書かれています。興味あったんで纏めてみます(というか訳してみます、に近いか・・・)。

コンポーネント(objectやfunction)が依存するブツを得る方法は3つしかない。

  1. コンポーネントは依存するブツを生成することができ、一般的にはnew演算子を使う。
  2. コンポーネントはグローバル変数から依存するブツを探すことができる。
  3. コンポーネントはそれが必要となる場所で渡される。

最初の生成する、とか、探すっていう2つの選択肢は、コンポーネントに依存するブツをハードコードしなきゃいけなくなるので、最適とはいえない。依存するブツを変更することが面倒になってしまう。これは特にテスト用に分離したブツのモックを提供したいといった場面で問題になる。

3番目の選択肢は、コンポーネントから依存するブツの場所を知る責務を取り除くのでもっともよさそう。依存するブツは単純にそのコンポーネントに渡されるだけ。

function SomeClass(greeter) {
  this.greeter = greeter;
}

SomeClass.prototype.doSomething = function(name) {
  this.greeter.greet(name);
};

上の例のSomeClassはgreeterの生成や場所の特定などには関係していなくて、インスタンスが作成される際に、単純にgreeterを手渡されるだけ。 それはいいんだけど、SomeClassを生成するコード上に依存するブツを得るっていう責務を生んでる。

依存するブツを生成するっていう責務を管理するために、angularjsアプリケーションは”injector”を持ってる。injectorは、依存するブツの生成や検索を責務とするサービスロケータである。

injectorサービスを使った例を挙げる。

var myModule = angular.module('myModule', []);

injectorにどうやってgreeterサービスを組み立てるか教えてあげる。greeterは$windowサービスに依存していることに注目しよう。greeterサービスはgreetメソッドを持つオブジェクト。

myModule.factory('greeter', function($window) {
  return {
    greet: function(text) {
      $window.alert(text);
    }
  };
});

で、myModuleに定義されたコンポーネントを提供することができる新しいinjectorを生成して、 そいつにgreeterサービスをリクエストする。(これは通常angular bootstrapで自動的に行われる。)

var injector = angular.injector(['myModule', 'ng']);
var greeter = injector.get('greeter');

ハードコードの問題の解決を望んだら、今度はinjectorがアプリケーション全体を通じて存在する必要性が出ちゃった。injectorを渡すことが「デメテルの法則」を破ってしまう。これを救済するために、下の例のように、コンポーネントを生成する責務をinjectorに移譲するために、HTMLテンプレート上で宣言的な記法を使っている。

function MyController($scope, greeter) {
  $scope.sayHello = function() {
    greeter.greet('Hello World');
  };
}

AngularはHTMLをコンパイルする時、injectorに対してコントローラとそれが依存するブツのインスタンスを次々に生成するよう要求しながらng-controllerディレクティブを処理する。

injector.instantiate(MyController);

これは全て舞台裏で完了する。ng-controllerを使うってことは、これまでにinjectorを知っているコントローラなしで、injectorにそのクラスのインスタンスを生成するよう要求し、MyControllerの全ての依存性を満たすってことに注意しよう。

これがベストな結果。アプリケーションコードはinjectorを使ってどうこうするとかなしに、単純に必要な依存性を宣言するだけ。この仕組なら「デメテルの法則」を破らないですむ。

というようなことが書いてあって、訳しかたがうまくなくて、はっきりと理解できたわけではないが、とりあえずメモとして残しておきます。

2015/01/06

AngularJSのDIの定義の仕方

AngularJSはDI(Dependency Injection)の機構を提供してるっつーんで、AngularJS: Developer Guide: Dependency Injectionを読みつつ、使い方を纏めておくとしよう。

依存するコンポーネントをどう書くか

以下の3つの方法がある。

  • inline array annotation (最も好ましい方法)
  • $inject property annotation
  • 関数のパラメータ名から暗黙的に注入する方法(この方法は注意が必要)

inline array annotation

someModule.controller('MyController', ['$scope', 'greeter', function($scope, greeter) {
  // do something.
}]);
と書くのがinline array annotationだと。この例で言うなら、'$scope'と'greeter'という名前で生成&登録されたオブジェクトが存在する前提で、MyControllerは$scopeとgreeterオブジェクトに依存している、ということを表現している。依存するコンポーネントの名称を文字列で並べつつ、関数のパラメータとして定義する。このとき、配列の要素の順番と関数の引数の順番が一致するよう注意する必要がある。

$inject array annotation

この方法は、仮にミニファイツールなどでコードがミニファイされて、関数のパラメータ名がツールに変更されてしまったとしても、正しく依存するオブジェクトが注入されるようにする方法、だと。

var MyController = function($scope, greeter) {
  // ...
}
MyController.$inject = ['$scope', 'greeter'];
someModule.controller('MyController', MyController);
この例は、inline array annotationとよく似ているからわかりやすい。$injectに依存するオブジェクトの名前を設定する、ってことね。これもinline array annotationと同じく、$injectに指定する配列の要素と、関数のパラメータの順序を一致させないとダメよ。

関数のパラメータ名から暗黙的に注入する方法

で、この暗黙の注入が一番簡単なんだけど、問題も有る、と。 コードを書くとすると、

someModule.controller('MyController', function($scope, greeter) {
  // ...
});
みたいになって、配列の順序と関数の引数の順序を一致させるとかも気にしなくていい、というメリットはある。だけど、ミニファイツールで関数の引数名が変えられちゃったら、もうAngularは正しく注入できないから、この方法は使わない方がいいみたい。
However this method will not work with JavaScript minifiers/obfuscators because of how they rename parameters. Tools like ng-annotate let you use implicit dependency annotations in your app and automatically add inline array annotations prior to minifying. If you decide to take this approach, you probably want to use ng-strict-di. Because of these caveats, we recommend avoiding this style of annotation.

AngularJSのConceptual Overview

AngularJSの機能やコンセプトなどよくわかってないので、公式サイトのガイドを読んでいる。そこには、Conceptual Overviewなる一覧があり、例えばAngularJSのFilterとかわかっていない僕には嬉しい一覧であった。ので、自分の備忘録としてここに訳しつつ纏めておく。

Template
追加のマークアップを持つHTML
Directives
カスタム属性、カスタム要素を持つHTMLの拡張
Model
ユーザがView上で見たり操作するデータ
Scope
モデルが格納されるコンテキスト。controller、directive、expressionからアクセスできる。
Expressions
Scopeからアクセスする変数や関数を記述する式
Compiler
Templateを解析し、directiveやexpressionsをインスタンス化する。
Filter
式の値をユーザに見せるために整形する。
View
ユーザが目にするもの。
Data Binding
Model-View間でデータを同期する。
Controller
Viewの裏側のビジネスロジック。
Dependency Injection
オブジェクトと関数を生成し、結び付ける。
Injector
DIコンテナ
Module
Injectorを構成するcontroller、service、filter、directiveなどアプリの異なるパーツのためのコンテナ。
Service
Viewから独立した再利用可能なビジネスロジック

2015/01/04

YeomanのAngularJS Full-Stack generatorが提供するジェネレータ

AngularJS Full-Stack generatorは、アプリケーションのひな形だけでなく、アプリケーションの中身を開発する上で使えるジェネレータをいくつか提供しているってことに気づいたので纏めてみます。

angular-fullstack

アプリケーションそのものを作るジェネレータ。angular-fullstack:appのエイリアス。

$ yo angular-fullstack

これ以外のジェネレータには、サーバサイドの機能を作るためのジェネレータと、クライアントサイドの機能を作るためのジェネレータ、そしてデプロイのためのジェネレータがある。

サーバサイド

angular-fullstack:endpoint

REST APIのエンドポイントを生成するジェネレータ。例えば、issueというリソースに対するREST APIのエンドポイントを定義する場合は、

$ yo angular-fullstack:endpoint issue
を実行する。すると、
? What will the url of your endpoint to be? /api/v1/issues
   create server/api/issue/index.js
   create server/api/issue/issue.controller.js
   create server/api/issue/issue.model.js
   create server/api/issue/issue.socket.js
   create server/api/issue/issue.spec.js
となる。URLをどうするか聞かれるので、/api/v1/issuesと入力した。モデルやコントローラ、specファイルが生成される。

クライアントサイド

angular-fullstack:route

あるURLに対するクライアントサイドのリソース一式を生成するジェネレータ。例えば、/issuesというURLを有効にするために、

$ yo angular-fullstack:route issues
を実行すると、
? Where would you like to create this route? client/app/
? What will the url of your route be? /issues
   create client/app/issues/issues.js
   create client/app/issues/issues.controller.js
   create client/app/issues/issues.controller.spec.js
   create client/app/issues/issues.jade
   create client/app/issues/issues.less
が得られる。

angular-fullstack:controller

クライアントサイドのコントローラを生成する。

$ yo angular-fullstack:controller chat
を実行すると、
? Where would you like to create this controller? client/app/
   create client/app/chat/chat.controller.js
   create client/app/chat/chat.controller.spec.js
が得られる。

angular-fullstack:directive

AngularJSのdirectiveを生成する。何らかのチャートを表示するためのディレクティブを作るとして、

$ yo angular-fullstack:directive chart
を実行すると、
? Where would you like to create this directive? client/app/
? Does this directive need an external html file? Yes
   create client/app/chart/chart.directive.js
   create client/app/chart/chart.directive.spec.js
   create client/app/chart/chart.jade
   create client/app/chart/chart.less
が得られる。

angular-fullstack:filter

filterって何だろう。AngularJS初心者なのでまだ知りません。 でも、

$ yo angular-fullstack:filter someFilter
を実行すると、filterのためのファイルを生成してくれる、と。

angular-fullstack:service

AngularJSのserviceを生成する。 例えば、issueというリソースに関するAPIを提供するサービスを作るとして、

$ yo angular-fullstack:service issue
を実行すると、
? Where would you like to create this service? client/components
   create client/components/issue/issue.service.js
   create client/components/issue/issue.service.spec.js
が得られる。同様に、angular-fullstack:factoryangular-fullstack:providerというgeneratorも存在していて、それぞれ、
// yo angular-fullstack:service issue
'use strict';

angular.module('teamApp')
  .service('issue', function () {
    // AngularJS will instantiate a singleton by calling "new" on this function
  });
// yo angular-fullstack:factory issue
'use strict';

angular.module('teamApp')
  .factory('issue', function () {
    // Service logic
    // ...

    var meaningOfLife = 42;

    // Public API here
    return {
      someMethod: function () {
        return meaningOfLife;
      }
    };
  });
// yo angular-fullstack:provider issue
'use strict';

angular.module('teamApp')
  .provider('issue', function () {

    // Private variables
    var salutation = 'Hello';

    // Private constructor
    function Greeter() {
      this.greet = function () {
        return salutation;
      };
    }

    // Public API for configuration
    this.setSalutation = function (s) {
      salutation = s;
    };

    // Method for instantiating
    this.$get = function () {
      return new Greeter();
    };
  });
を生成する。

angular-fullstack:decorator

公式サイトには

Generates an AngularJS service decorator.
とあるが、これもよくわかっていない。が、
yo angular-fullstack:decorator issue
を実行すると、
? Where would you like to create this decorator? client/components
   create client/components/issue/issue.decorator.js
が得られ、
'use strict';

angular.module('teamApp')
  .config(function ($provide) {
    $provide.decorator('issue', function ($delegate) {
      // decorate the $delegate
      return $delegate;
    });
  });
が生成された。

デプロイ

デプロイのためのgeneratorとしては、

  • yo angular-fullstack:openshift
  • yo angular-fullstack:heroku
があって、それぞれOpenShift、Herokuへのデプロイを簡単に行うためのコマンドのようだが、今回は割愛。

終わりに

filterや、service decoratorなどAngularJSを理解していないと使い方がわからないものがあり、使いこなすにはもっと勉強しないと、と感じた。あとは、クライアントサイドのディレクトリ構成を意識するというか、戦略を持たないといけないな。

2015/01/01

YeomanでAngularJS、Express、MongoDB、Node.jsなアプリのひな形を作る。

Yeomanを使ってMEAN(MongoDB、Express、AngularJS、Node.js)なWebアプリケーションのひな形を作ってみます。

AngularJS Full-Stack generator

AngularJS Full-Stack generatorというgeneratorを使用する。

$ npm install -g generator-angular-fullstack
でインストールする。-gオプションを付けてglobalなパッケージとしてインストールする。

AngularJS Full-Stackを実行する。

$ yo angular-fullstack
を実行すると、generatorが次のような質問をしてくるので、適宜答えてあげる。
  • JavaScript、CoffeeScriptのどっちを使う?
  • HTMLとJade、どっちでマークアップする?
  • css、sass、stylus、lessのどれでスタイル書く?
  • AngularのRouterは、ngRouterとuiRouterのどっちを使う?
  • Twitter Bootstrap使う?
  • ui-bootstrapを使う?
  • MongoDBのモデリングにはmongooseを使う?
  • 認証処理のboilerplateを生成する?
  • Google、Facebook、TwitterのOAuthストラテジを有効にする?
  • socket.ioを使う?
質問に答えていくと↑のような画面になる。ここから先は、npm installとかbower installとかYoermanが自動で進めてくれる。

生成したプロジェクト

このgeneratorによって生成したプロジェクトは↓のようなディレクトリ構造になる。

.
├── Gruntfile.js
├── bower.json
├── client
├── e2e
├── karma.conf.js
├── node_modules
├── package.json
├── protractor.conf.js
└── server
clientディレクトリにはクライアントサイドのコードを、serverディレクトリにはサーバサイドのコードを格納するっていうのが、以前のバージョンから変わったところか。e2eディレクトリの目的は詳細を調べる必要がある。

アプリケーションを起動する。

$ grunt serve
でアプリケーションが起動する。