AngularJSのパターン

短いレビュー


何か新しいことを学ぶ最良の方法の1つは、すでに知っていることをその中でどのように使用するかを確認することです。 この記事は、読者にデザインやデザインパターンを理解することを意図していません。 OOPの概念、設計パターン、およびアーキテクチャパターンの基本的な理解を提供します。 この記事の目的は、AngularJSとその書かれたSPAでさまざまなソフトウェア設計とアーキテクチャパターンがどのように使用されるかを説明することです。

はじめに


この記事は、AngularJSフレームワークの簡単な概要から始まります。 概要では、AngularJSの主要コンポーネント(ディレクティブ、フィルター、コントローラー、サービス、スコープ)について説明します。 2番目のセクションでは、フレームワーク内に実装されるさまざまな設計とアーキテクチャパターンをリストして説明します。 テンプレートは、使用されるAngularJSコンポーネントごとにグループ化されます。 いくつかのテンプレートが複数のコンポーネントで使用されている場合、これが示されます。
最後のセクションには、AngularJS上に構築されたSPAで一般的に使用されるいくつかのアーキテクチャパターンが含まれています。

AngularJSの概要

AngularJSの概要


AngularJSは、Googleが開発したJavaScript Webフレームワークです。 彼はCRUD SPAの開発のための強固な基盤を提供するつもりです。 SPAは1回だけロードされ、SPAを使用するときにページをリロードする必要はありません。 これは、メインページを読み込むときまたはオンデマンドで、すべてのアプリケーションリソース(データ、テンプレート、スクリプト、スタイル)を読み込む必要があることを意味します。 ほとんどのCRUDアプリケーションには共通の特性と要件があるため、AngularJSはすぐに最適なセットを提供する予定です。 AngularJSの重要な機能は次のとおりです。
  • 双方向のデータバインディング
  • 依存性注入、DI (依存性注入)
  • 関心の分離
  • テスタビリティ
  • 抽象化

責任の分離は、各AngularJSアプリケーションを次のような個別のコンポーネントに分割することにより実現されます。
  • パーシャル
  • コントローラー
  • 指令
  • サービス
  • フィルター

これらのコンポーネントは、さまざまなモジュール内でグループ化して、より高いレベルの抽象化を実現できます。 各コンポーネントには、アプリケーションロジックの特定の部分が含まれます。

パーシャル


パーシャルはHTML行です。 要素またはその属性内にAngularJS式を含めることができます。 他のフレームワークに対するAngularJSの利点の1つは、AngularJSテンプレートがHTMLに変換する必要のある中間形式(mustache.jsなど)ではないことです

最初に、各SPAはIndex.htmlファイルをロードします。 AngularJSの場合、このファイルには、アプリケーションを構成および起動する一連の標準(およびそうではない)HTML属性、要素、およびコメントが含まれています。 各ユーザーアクションでは、他のパーシャル(HTML行またはHTMLピースを含むファイル)をロードするか、たとえばフレームワークによって提供されるデータバインディングを介してアプリケーションの状態を変更する必要があります。
パーシャルの例:

<html ng-app> <!-- Body tag augmented with ngController directive --> <body ng-controller="MyController"> <input ng-model="foo" value="bar"> <!-- Button tag with ng-click directive, and string expression 'buttonText' wrapped in "{{ }}" markup --> <button ng-click="changeFoo()">{{buttonText}}</button> <script src="angular.js"></script> </body> </html> 

AngularJS式とともに、パーシャルはユーザーと対話するために実行する必要があるアクションを決定します。 上記の例では、ng-click属性の値は、changeFoo()メソッドが現在のスコープから呼び出されることを意味します。

コントローラー


AngularJSのコントローラーは、スコープにメソッドを追加することにより、ユーザーとアプリケーションの対話(マウス、キーボードなど)イベントを処理できる一般的な機能です。 コントローラのすべての外部依存関係は、AngularJSのDIメカニズムを使用して提供されます。 コントローラーは、データをスコープに追加することにより、モデルとパーシャルの相互作用も担当します。 これはビューモデルとして見ることができます。

 function MyController($scope) { $scope.buttonText = 'Click me to change foo!'; $scope.foo = 42; $scope.changeFoo = function () { $scope.foo += 1; alert('Foo changed'); }; } 

たとえば、上記のコントローラーを前のセクションに接続すると、ユーザーはいくつかの方法でアプリケーションと対話できます。
  1. 入力フィールドにデータを入力して「foo」を変更します。 これは、双方向のデータバインディングにより、「foo」値にすぐに影響します。
  2. 「Click me to foo!」という名前のボタンをクリックして、「foo」の値を変更します

すべてのユーザー要素、属性、コメント、またはクラスは、AngularJSディレクティブにすることができます(事前定義されている場合)。

範囲


AngularJSでは、スコープはパーシャルにアクセス可能なJavaScriptオブジェクトです。 スコープには、プリミティブ、オブジェクト、またはメソッドなどのさまざまなプロパティを含めることができます。 スコープに追加されたすべてのメソッドは、このスコープに関連付けられたパーシャル内のAngularJS式を使用して、またはスコープへの参照を持つコンポーネントによる直接メソッド呼び出しによって呼び出すことができます。 適切なディレクティブを使用して、データがスコープに追加され、ビューに関連付けることができるため、スコーププロパティのすべての変更がビューに反映され、ビューのすべての変更がスコープに表示されます。

AngularJSアプリケーションのスコープのもう1つの重要な特徴は、プロトタイプの継承メカニズムを介してリンクされていることです(分離されたスコープを除く)。 したがって、すべての子スコープはその親のメソッドを使用できます。これらは直接または間接プロトタイプのプロパティであるためです。

スコープの継承を次の例に示します。

 <div ng-controller="BaseCtrl"> <div id="child" ng-controller="ChildCtrl"> <button id="parent-method" ng-click="foo()">Parent method</button> <button ng-click="bar()">Child method</button> </div> </div> 

 function BaseCtrl($scope) { $scope.foo = function () { alert('Base foo'); }; } function ChildCtrl($scope) { $scope.bar = function () { alert('Child bar'); }; } 

ChildCtrlのスコープはdiv#子に関連付けられていますが、ChildCtrlのスコープはBaseCtrlにネストされているため、BaseCtrlのすべてのメソッドはプロトタイプの継承を使用してChildCtrlで使用でき、したがってfooメソッドはボタン#parent-methodをクリックすると使用可能になります。

指令


AngularJSのディレクティブは、すべてのDOM操作を実行する場所です。 原則として、コントローラーのDOMを使用した操作がある場合は、コントローラーのDOMを使用した操作を排除する新しいディレクティブまたはリファクタリングを作成する必要があります。 最も単純なケースでは、ディレクティブには、ディレクティブのロジックを含むpostLink関数の名前と定義があります。 より複雑な場合、ディレクティブには次のような多くのプロパティを含めることができます。

  • テンプレート
  • コンパイル機能
  • リンク機能
  • など

ディレクティブはパーシャルで使用できます。例:

 myModule.directive('alertButton', function () { return { template: '<button ng-transclude></button>', scope: { content: '@' }, replace: true, restrict: 'E', transclude: true, link: function (scope, el) { el.click(function () { alert(scope.content); }); } }; }); 

 <alert-button content="42">Click me</alert-button> 

上記の例では、<alert-button> </ alert-button>タグはボタン要素に置き換えられ、ボタンをクリックすると、テキスト42を含む警告が表示されます。

フィルター


AngularJSのフィルターは、データのフォーマットに必要なロジックをカプセル化します。 通常、フィルターはパーシャル内で使用されますが、DIを介してコントローラー、ディレクティブ、サービス、またはその他のフィルターでも使用できます。
文字列を大文字に変換する簡単なフィルターの例を次に示します。

 myModule.filter('uppercase', function () { return function (str) { return (str || '').toUpperCase(); }; }); 

パーシャル内では、Unixパイピング構文(Unixのパイピング)を使用してフィルターを使用できます。

 <div>{{ name | uppercase }}</div> 

コントローラー内では、フィルターは次のように使用できます。

 function MyCtrl(uppercaseFilter) { $scope.name = uppercaseFilter('foo'); //FOO } 

サービス


上記のコンポーネントに適用されないロジックの部分は、サービスに配置する必要があります。 通常、サービスはロジック、不変ロジック、XHR、WebSocketなどの特定の領域をカプセル化します。アプリケーションのコントローラーが「厚くなりすぎる」場合、繰り返されるコードをサービスに送信する必要があります。

 myModule.service('Developer', function () { this.name = 'Foo'; this.motherLanguage = 'JavaScript'; this.live = function () { while (true) { this.code(); } }; }); 

サービスは、DIをサポートするコンポーネント(コントローラー、フィルター、ディレクティブ、その他のサービス)に追加できます。

 function MyCtrl(developer) { var developer = new Developer(); developer.live(); } 


AngularJSパターン


次の2つのセクションでは、AngularJSコンポーネントで従来の設計およびアーキテクチャパターンがどのように使用されるかを見ていきます。

最後の章では、AngularJSで(だけでなく)SPAを開発するときによく使用されるいくつかのアーキテクチャパターンを見ていきます。

サービス


シングルトン


シングルトンは、クラスのインスタンスの作成を単一のオブジェクトに制限する設計パターンです。 これは、システム全体でアクションを調整する必要がある場合に役立ちます。 この概念は、オブジェクトが1つしかない場合や、インスタンスが特定の数のオブジェクトによって制限されている場合に、より効率的に動作するシステムに適しています。

UML図は、シングルトンパターンを示しています。

画像

コンポーネントに依存関係が必要な場合、AngularJSは次のアルゴリズムを使用してそれを解決します。

getServiceを実装するAngularJSのソースコードをよく見てください。

 function getService(serviceName) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; } throw err; } finally { path.shift(); } } } 

サービスは一度しか作成されないため、各サービスがシングルトンであると想像してください。 キャッシュはシングルトンマネージャーと考えることができます。 また、この実装はUML図に示されている実装とは若干異なります。コンストラクタ内でシングルトンへの静的なプライベートリンクを作成する代わりに、シングルトンマネージャ内でリンクを保存するためです。

したがって、サービスは実際にはシングルトンですが、シングルトンテンプレートを介して実装されていないため、標準実装よりもいくつかの利点があります。



このトピックの詳細については、Google TestingブログのMisko Heveryの記事をご覧ください。

工場方式


Factory Method(Virtual Constructorとも呼ばれる)は、クラスをインスタンス化するためのインターフェースをサブクラスに提供する汎用設計パターンです。 作成時に、相続人は作成するクラスを決定できます。 つまり、ファクトリはオブジェクトの作成を親クラスの継承者に委任します。 これにより、プログラムコードで特定のクラスを使用するのではなく、より高いレベルで抽象オブジェクトを操作できます。

画像

次のスニペットを見てみましょう。

 myModule.config(function ($provide) { $provide.provider('foo', function () { var baz = 42; return { //Factory method $get: function (bar) { var baz = bar.baz(); return { baz: baz }; } }; }); }); 

ここでは、configコールバック関数を使用して、新しい「プロバイダー」を定義しています。 「プロバイダー」は、$ getメソッドを持つオブジェクトです。 JavaScriptにはインターフェースがなく、言語はダックタイピングを使用するため、プロバイダーでファクトリメソッドを$ getとして使用することに同意しました。

各サービス、フィルター、ディレクティブ、およびコントローラーにはプロバイダー(つまり、$ get factoryメソッドを持つオブジェクト)があり、コンポーネントのインスタンス化を担当します。

AngularJSの実装をもう少し深く掘り下げることができます。

 //... createInternalInjector(instanceCache, function(servicename) { var provider = providerInjector.get(servicename + providerSuffix); return instanceInjector.invoke(provider.$get, provider, undefined, servicename); }, strictDi)); //... function invoke(fn, self, locals, serviceName){ if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = annotate(fn, strictDi, serviceName), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push(locals && locals.hasOwnProperty(key) ? locals[key] : getService(key)); } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } return fn.apply(self, args); } 

この例では、$ getメソッドの実際の使用方法を確認できます。

 instanceInjector.invoke(provider.$get, provider, undefined, servicename) 

上記のフラグメントでは、invokeメソッドが呼び出され、サービスのファクトリメソッド($ get)が最初の引数として渡されます。 invokeメソッド内で、注釈関数が呼び出され、ファクトリメソッドも最初の引数として渡されます。 注釈関数は、AngularJS DIメカニズム(前述)を介してすべての依存関係を解決します。 すべての依存関係を解決した後、ファクトリメソッドが呼び出されます。

 fn.apply(self, args). 

上記のUMLダイアグラムの観点から言えば、Creatorを呼び出すことができます。Creatorは、ファクトリメソッドを介して「ConcreteCreator」を呼び出し、「Product」を作成します。

この場合、インスタンスの間接的な作成が使用されるため、ファクトリメソッドテンプレートを使用するといくつかの利点が得られます。 したがって、フレームワークは、新しいコンポーネントを作成するためのレイアウト/テンプレートに影響します。


デコレータ


デコレータは、オブジェクトに動作を動的に追加するために設計された構造設計テンプレートです。 Decoratorパターンは、機能を拡張するためのサブクラス化の柔軟な代替手段を提供します。

画像

すぐに使用できるAngularJSには、既存のサービスの機能を拡張および/または拡張するオプションが用意されています。 デコレータまたは$提供メソッドを使用して、自分またはサードパーティライブラリから既に定義されているサービスのラッパーを作成できます。

 myModule.controller('MainCtrl', function (foo) { foo.bar(); }); myModule.factory('foo', function () { return { bar: function () { console.log('I\'m bar'); }, baz: function () { console.log('I\'m baz'); } }; }); myModule.config(function ($provide) { $provide.decorator('foo', function ($delegate) { var barBackup = $delegate.bar; $delegate.bar = function () { console.log('Decorated'); barBackup.apply($delegate, arguments); }; return $delegate; }); }); 

上記の例では、「foo」という名前の新しいサービスを定義しています。 $ provide.decoratorメソッドはコールバック関数「config」で呼び出され、デコレーションするサービスの名前が最初の引数として渡され、関数が2番目の引数として渡され、実際にデコレータが実装されます。 $デリゲートは、元のサービスfooへのリンクを保存します。 barメソッドをオーバーライドして、サービスを飾ります。 実際、装飾は、別のconsole.log状態-console.log( 'Decorated')を含めることにより、単にbarの拡張であり、適切なコンテキストで元のbarメソッドを呼び出します。

テンプレートを使用すると、サードパーティが作成したサービスの機能を変更する必要がある場合に特に便利です。 多数のデコレータが必要な場合(メソッド、承認、登録などのパフォーマンスを測定する場合など)、多くの重複コードとDRY原則の違反が発生する可能性があります。 このような場合、 アスペクト指向プログラミングを使用することをお勧めします。 AngularJSのAOPフレームワークは、 github.com / mgechev / angular-aopにあります。

正面


ファサードテンプレート-1つのオブジェクトへのすべての可能な外部呼び出しを減らし、それらを対応するシステムオブジェクトに委任することにより、システムの複雑さを隠すことができる構造設計テンプレート。
ファサードは:
  1. ファサードには一般的なタスクを実行するためのより適切な方法があるため、ライブラリの使用、理解、テストを容易にするため
  2. 同じ理由で、ライブラリを読みやすくします
  3. ほとんどのコードはファサードを使用しているため、内部ライブラリの外部コードへの依存を減らすため、システムをより柔軟に開発できます。
  4. 適切に設計されたAPIの貧弱に設計されたコレクションをラップします(タスクのニーズに応じて)


画像

AngularJSにはいくつかのファサードがあります。 一部の機能に高レベルのAPIを提供するたびに、実際にファサードを作成します。

たとえば、XMLHttpRequest POSTリクエストを作成する方法を見てみましょう。

 var http = new XMLHttpRequest(), url = '/example/new', params = encodeURIComponent(data); http.open("POST", url, true); http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http.setRequestHeader("Content-length", params.length); http.setRequestHeader("Connection", "close"); http.onreadystatechange = function () { if(http.readyState == 4 && http.status == 200) { alert(http.responseText); } } http.send(params); 

AngularJS $ httpサービスを使用してデータを送信する場合、次のことができます。
 $http({ method: 'POST', url: '/example/new', data: data }) .then(function (response) { alert(response); }); 

または:
 $http.post('/someUrl', data).then(function (response) { alert(response); }); 

2番目のオプションは、HTTP POST要求を作成する事前構成バージョンです。
3番目のオプションは、$リソースサービスを使用して作成され、$ httpサービスの上に構築される高レベルの抽象化です。 Active RecordおよびProxyセクションで、このサービスをもう一度見ていきます。

プロキシ


プロキシ(副)-構造設計テンプレートは、別のオブジェクトへのアクセスを制御するオブジェクトを提供し、すべての呼び出しをインターセプトします(コンテナーの機能を実行します)。 プロキシは、ネットワーク接続、メモリ内の大きなオブジェクト、ファイル、またはコピーが高価または不可能なその他のリソースなど、あらゆるものと対話できます。

画像

3種類のプロキシを区別できます。

このサブセクションでは、実装されたAngularJS仮想プロキシを見ていきます。
以下のスニペットでは、Userという名前の$リソースオブジェクトのgetメソッドを呼び出します。

 var User = $resource('/users/:id'), user = User.get({ id: 42 }); console.log(user); //{} 

console.logには空のオブジェクトが表示されます。 AJAXリクエストはUser.getの呼び出し後に非同期に実行されるため、console.logの呼び出し中にはユーザーデータがありません。 User.getを呼び出した直後に、GET要求が実行され、空のオブジェクトが返され、そのオブジェクトへのリンクが保存されます。 このオブジェクトは仮想プロキシと考えることができます。クライアントがサーバーから応答を受信するとすぐにデータで満たされます。

AngularJSではどのように機能しますか? 次のスニペットを見てみましょう。

 function MainCtrl($scope, $resource) { var User = $resource('/users/:id'), $scope.user = User.get({ id: 42 }); } 

 <span ng-bind="user.name"></span> 


上記のコードフラグメントを実行すると、$スコープオブジェクトのユーザープロパティは空のオブジェクト({})になります。つまり、user.nameは未定義で表示されません。 サーバーがGET要求に対する応答を返した後、AngularJSはこのオブジェクトにサーバーから受信したデータを入力します。 次の$ダイジェストサイクルで、AngularJSは$ scope.userの変更を検出し、ビューを更新します。

アクティブな記録


アクティブレコードは、データと動作を保存するオブジェクトです。 通常、このオブジェクトのほとんどのデータは永続的です。データを作成、更新、検索、削除するためにデータベースに接続するのはActive Recordオブジェクトの責任です。 彼はこの責任を下位レベルのオブジェクトに委任できますが、Active Recordオブジェクトまたはその静的メソッドへの呼び出しは、データベースとの対話につながります。

画像

AngularJSはservice $リソースを定義します。 現在のバージョンのAngularJS(1,2+)では、個別のモジュールとして配布されており、カーネルには含まれていません。
$リソースドキュメントによると:
$リソースは、RESTfulサーバー側データソースとやり取りできるようにする$リソースオブジェクトを作成するためのファクトリです。 $リソースオブジェクトには、低レベルのサービス 'ohm $ httpと対話する必要なく、高レベルの動作を提供するメソッドがあります。

$リソースの使用方法は次のとおりです。

 var User = $resource('/users/:id'), user = new User({ name: 'foo', age : 42 }); user.$save(); 

$リソースを呼び出すと、モデルのインスタンスのコンストラクターが作成されます。 各モデルインスタンスには、さまざまなCRUD操作に使用できるメソッドがあります。

したがって、コンストラクター関数とその静的メソッドを使用できます。

 User.get({ userid: userid }); 

上記のコードはすぐに空のオブジェクトを返し、それへのリンクを保存します。 回答を受信して​​分析した後、AngularJSはオブジェクトに受信データを入力します(プロキシを参照)。
$リソースとAngularJSオブジェクトの魔法に関する詳細なドキュメントを見つけることができます。
マーティン・ファウラーはこう主張しています:
Active Recordオブジェクトは、データベースとの通信を処理して...
$リソースは、データベースではなくRESTfulサービスと対話するため、完全なActive Recordテンプレートを実装しません。 いずれにしても、「RESTfulと対話するためのアクティブレコード」と見なすことができます。

インターセプトフィルター


フィルターのチェーンを作成して、単純なプリ/ポストリクエスト処理タスクを実行します。

画像

場合によっては、HTTPリクエストの何らかの種類の前処理/後処理を行う必要があります。 この場合、インターセプトフィルターは、HTTP要求/応答の前/後処理、ロギング、セキュリティ、または要求本文またはヘッダーを必要とするその他のタスク用に設計されています。 通常、Intercepting Filtersテンプレートには一連のフィルターが含まれ、それぞれが特定の順序でデータを処理します。 各フィルターの出力は、次のフィルターの入力です。

AngularJSには、$ httpProviderのインターセプトフィルターのアイデアがあります。 $ httpProviderにはinterceptorsプロパティがあり、オブジェクトのリストが含まれています。 各オブジェクトには、request、response、requestError、responseErrorのプロパティがあります。

requestErrorは、以前のインターセプターでエラーが発生した場合、または拒否された場合にそれぞれ発生します。以前の応答インターセプターが例外をスローした場合、responseErrorが発生します。

以下は、オブジェクトリテラルを使用してインターセプターを追加する方法の基本的な例です。

 $httpProvider.interceptors.push(function($q, dependency1, dependency2) { return { 'request': function(config) { // same as above }, 'response': function(response) { // same as above } }; }); 


指令


コンポジット


複合テンプレートは、構造設計パターンです。複合テンプレートは、単一のオブジェクトだけでなく、アクセスできるようにオブジェクトをグループ化する方法を説明します。コンポジットの目的は、特定から全体への階層を表すツリー構造でオブジェクトを構成することです。

画像

The Gang of Fourによると、MVCは組み合わせにすぎません。

彼らは、パフォーマンスはコンポーネントの合成であると主張しています。AngularJSにも同様の状況があります。ビューは、ディレクティブとこれらのディレクティブが構築されるDOM要素の構成によって形成されます。

次の例を見てみましょう。

 <!doctype html> <html> <head> </head> <body> <zippy title="Zippy"> Zippy! </zippy> </body> </html> 

 myModule.directive('zippy', function () { return { restrict: 'E', template: '<div><div class="header"></div><div class="content" ng-transclude></div></div>', link: function (scope, el) { el.find('.header').click(function () { el.find('.content').toggle(); }); } } }); 

この例では、ユーザーインターフェイスのコンポーネントであるディレクティブを作成します。作成されたコンポーネント(「zippy」という名前)には、タイトルとコンテンツがあります。タイトルをクリックすると、コンテンツの表示が切り替わります。

最初の例から、DOMツリーは要素の組み合わせであることがわかります。ルートコンポーネントはhtmlで、その直後にネストされた要素head、bodyなどがあります...

2番目の例では、テンプレートディレクティブのプロパティにng-transcludeディレクティブを含むマークアップが含まれていることがわかります。これは、「zippy」ディレクティブ内に別のng-transcludeディレクティブ、つまりディレクティブの構成があることを意味します。理論的には、最終ノードに到達するまでコンポーネントを無限にネストできます。

通訳


インタープリター(インタープリター)-言語で式を定義する方法を示す動作設計パターン。主なアイデアは、各文字(端末または非端末)を特殊なプログラミング言語に分類することです。式の構文ツリー(構成テンプレートの例)は、式の分析(解釈)に使用されます。

画像

$ parseを使用して、AngularJSはDSL(ドメイン固有言語)インタープリターの独自の実装を提供します。DSLを使用すると、JavaScriptが簡素化および変更されます。AngularJSとJavaScriptの式の主な違いは、AngularJSの式:

$ parse内では、2つの主要コンポーネントが定義されています。
 //       var Lexer; //       var Parser; 


式が受信されると、トークンに分割されてキャッシュされます(パフォーマンスの問題のため)。
AngularJS DSLの端末式は、次のように定義されています。

 var OPERATORS = { /* jshint bitwise : false */ 'null':function(){return null;}, 'true':function(){return true;}, 'false':function(){return false;}, undefined:noop, '+':function(self, locals, a,b){ //... }, '*':function(self, locals, a,b){return a(self, locals)*b(self, locals);}, '/':function(self, locals, a,b){return a(self, locals)/b(self, locals);}, '%':function(self, locals, a,b){return a(self, locals)%b(self, locals);}, '^':function(self, locals, a,b){return a(self, locals)^b(self, locals);}, '=':noop, '===':function(self, locals, a, b){return a(self, locals)===b(self, locals);}, '!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);}, '==':function(self, locals, a,b){return a(self, locals)==b(self, locals);}, '!=':function(self, locals, a,b){return a(self, locals)!=b(self, locals);}, '<':function(self, locals, a,b){return a(self, locals)<b(self, locals);}, '>':function(self, locals, a,b){return a(self, locals)>b(self, locals);}, '<=':function(self, locals, a,b){return a(self, locals)<=b(self, locals);}, '>=':function(self, locals, a,b){return a(self, locals)>=b(self, locals);}, '&&':function(self, locals, a,b){return a(self, locals)&&b(self, locals);}, '||':function(self, locals, a,b){return a(self, locals)||b(self, locals);}, '&':function(self, locals, a,b){return a(self, locals)&b(self, locals);}, '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, '!':function(self, locals, a){return !a(self, locals);} }; 

各終端記号に関連付けられた各関数は、AbstractExpressionインターフェイス(抽象式)の実装として表すことができます。
クライアントは、特定のコンテキスト(特定の$スコープ)で受信した式を解釈します。
いくつかの簡単なAngularJS式:
 // toUpperCase filter is applied to the result of the expression // (foo) ? bar : baz (foo) ? bar : baz | toUpperCase 


テンプレートビュー


HTMLページにマーカーを埋め込むことにより、データをHTMLに変換します。

画像

ページを動的に表示することは、それほど簡単な作業ではありません。これは、多くの文字列の連結、操作、および問題が原因です。動的ページを作成する最も簡単な方法は、マークアップを記述し、特定のコンテキストで処理されるいくつかの式をその中に含めることです。その結果、テンプレート全体が最終的な形式に変換されます。私たちの場合、この形式はHTML(またはDOM)になります。これがまさにテンプレートエンジンの動作です。DSLを取得し、適切なコンテキストで処理してから、最終的な形式に変換します。

テンプレートはバックエンドで非常に頻繁に使用されます。たとえば、HTMLにPHPコードを埋め込み、動的ページを作成できます。また、RubyでSmartyまたはeRubyを使用して、静的ページにRubyコードを埋め込むこともできます。

JavaScriptには、mustache.js、ハンドルバーなどの多くのテンプレートエンジンがあります。それらのほとんどは、テンプレートを文字列として使用します。テンプレートはさまざまな方法で保存できます。

例:

 <script type="template/mustache"> <h2>Names</h2> {{#names}} <strong>{{name}}</strong> {{/names}} </script> 


テンプレートエンジンは、文字列を結果のコンテキストと組み合わせて、DOM要素に変換します。したがって、マークアップに組み込まれているすべての式が分析され、それらの値に置き換えられます。

たとえば、オブジェクトのコンテキストで上記のテンプレートを処理すると、{names:['foo'、 'bar'、 'baz']}が得られます。

 <h2>Names</h2> <strong>foo</strong> <strong>bar</strong> <strong>baz</strong> 

実際、AngularJSのテンプレートはプレーンHTMLです。ほとんどのテンプレートのように、それらは中間形式ではありません。AngularJSコンパイラは、DOMツリーをバイパスして既知のディレクティブ(要素、属性、クラス、またはコメント)を見つけるために何をしますか?AngularJSは、これらのディレクティブのいずれかを検出すると、それに関連付けられたロジックを呼び出し、現在の$スコープのコンテキストにさまざまな式の定義を含めることができます。

例:

 <ul ng-repeat="name in names"> <li>{{name}}</li> </ul> 

スコープコンテキストで:

 $scope.names = ['foo', 'bar', 'baz']; 


上記と同じ結果が得られます。主な違いは、テンプレートがscriptタグ内にないことです。ここではHTMLのみです。

範囲


オブザーバー


オブザーバーパターンは、サブジェクトと呼ばれるオブジェクトがオブザーバーと呼ばれる依存関係のリストを保存し、通常はメソッドの1つを呼び出すことで状態に変化があった場合に通知する動作設計パターンです。主に、分散イベント処理システムの実装に使用されます。

画像

AngularJSアプリケーションには、スコープ間で相互作用する2つの主な方法があります。

まず、子スコープから親スコープのメソッドを呼び出します。前述のとおり、子スコープは親のプロトタイプを継承するため、これが可能です(スコープを参照)。これにより、子から親への一方向の対話が可能になります。子スコープのメソッドを呼び出すか、親スコープでのイベントの発生を通知する必要がある場合があります。 AngularJSは、これを可能にする組み込みのオブザーバーパターンを提供します。

オブザーバーパターンの2番目の可能な使用法は、複数のスコープがイベントにサブスクライブされる場合ですが、それが発生するスコープはそれについて何も知りません。これにより、接続の範囲を縮小できます。他の範囲については何も知らないようにする必要があります。

AngularJSの各スコープには、パブリックメソッド$ on、$ emit、および$ broadcastがあります。$ onメソッドは、イベント名とコールバック関数を引数として受け取ります。この関数は、オブザーバー(オブザーバーインターフェイスを実装するオブジェクト)として表すことができます(JavaScriptでは、すべての関数は最初のクラスであるため、通知メソッドの実装のみを提供できます)。

 function ExampleCtrl($scope) { $scope.$on('event-name', function handler() { //body }); } 

したがって、現在のスコープはevent-nameイベントにサブスクライブします。親スコープまたは子スコープのいずれかで発生すると、ハンドラーが呼び出されます。

$ emitおよび$ broadcastメソッドは、それぞれ継承チェーンの上下でイベントをトリガーするために使用されます。例:

 function ExampleCtrl($scope) { $scope.$emit('event-name', { foo: 'bar' }); } 

上記の例では、スコープはすべてのスコープに対してイベント名イベントを生成します。これは、event-nameイベントにサブスクライブするすべての親スコープが通知され、そのハンドラーが呼び出されることを意味します。

$ broadcastメソッドが呼び出されたときにも同じことが起こります。唯一の違いは、イベントがすべての子スコープに対して渡されることです。各スコープは、複数のコールバック関数を使用して任意のイベントをサブスクライブできます(つまり、複数のオブザーバーをこのイベントに関連付けることができます)。

JavaScriptコミュニティでは、このテンプレートはパブリッシュ/サブスクライブとして知られています。

責任の連鎖


Chain of Responsibilities(行動連鎖)-行動デザインパターンは、チームオブジェクトと一連の処理オブジェクト(ハンドラー)で構成されます。各ハンドラーには、処理できるコマンドのタイプを決定するロジックが含まれています。次に、コマンドはチェーン内の次のハンドラーに移動します。テンプレートには、このチェーンの最後に新しいハンドラーを追加するメカニズムも含まれています。

画像

上記のように、スコープはスコープチェーンと呼ばれる階層を形成します。これらのスコープの一部は分離されています。つまり、親スコープのプロトタイプを継承しませんが、$ parentプロパティを介してプロトタイプに関連付けられます。

$ emitまたは$ broadcastを呼び出した後、スコープチェーンに沿って(呼び出されるメソッドに応じて上下に)移動を開始するイベントが発生し、イベントバスとして、より正確には職務のチェーンとして表すことができます。後続の各スコープは次のことができます。

以下の例では、ChildCtrlはスコープチェーンを上に伝播するイベントを生成します。ここで、各親スコープ(ParentCtrlおよびMainCtrl)は、コンソールに書き込むことでイベントを処理する必要があります:「foo received」。スコープのいずれかが最終受信者である場合、イベントオブジェクトでstopPropagationメソッドを呼び出すことができます。

 myModule.controller('MainCtrl', function ($scope) { $scope.$on('foo', function () { console.log('foo received'); }); }); myModule.controller('ParentCtrl', function ($scope) { $scope.$on('foo', function (e) { console.log('foo received'); }); }); myModule.controller('ChildCtrl', function ($scope) { $scope.$emit('foo'); }); 

上記のUML図のハンドラーは、コントローラーに追加されたさまざまなスコープです。

コマンド


コマンドは、オブジェクトを使用して必要なすべての情報をカプセル化し、しばらくしてからメソッドを呼び出すビヘイビアーデザインパターンです。この情報には、メソッドの名前、メソッドが属するオブジェクト、およびメソッドパラメーターの値が含まれます。

画像

AngularJSでは、コマンドパターンを使用して、データバインディングの実装を記述できます。
モデルをビューにリンクする場合、ng-bindディレクティブ(一方向データバインディング用)およびng-model(双方向データバインディング用)を使用できます。たとえば、モデルのすべての変更をビューに表示する場合:

 <span ng-bind="foo"></span> 

fooの値を変更するたびに、spanタグ内のテキストも変更されます。より複雑な式を使用することもできます。

 <span ng-bind="foo + ' ' + bar | uppercase"></span> 

上記の例では、spanタグの値は大文字のfooとbarの値の合計になります。内部で何が起こっているのですか?
各スコープには$ watchメソッドがあります。AngularJSコンパイラがng-bindディレクティブを見つけると、式foo + '' + bar |の新しいオブザーバーを作成します。大文字、($スコープ。$ watch( "foo + '' + bar | uppercase"、function(){/ * body * /});)。コールバック関数は、式の値が変更されるたびに呼び出されます。この場合、コールバック関数はspanタグの値を更新します。

$ watch実装の最初の数行は次のとおりです。

 $watch: function(watchExp, listener, objectEquality) { var scope = this, get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; //... 

ウォッチャーはコマンドとして想像できます。コマンド式は、$ダイジェストループごとに評価されます。AngularJSは式の変更を検出すると、リスナー関数を呼び出します。Watcherには、コマンドの実行を監視してリスナー(実際の受信者)に委任するために必要なすべての情報が含まれています。$スコープをClientとして、$ダイジェストサイクルをInvokerチームとして表すことができます。

コントローラー


ページコントローラー


サイト上の特定のページまたはアクティビティに対する要求を処理するオブジェクト。マーティン・ファウラー。

画像

ページあたり4つのコントローラーによると:
ページコントローラーテンプレートは、受信したページからのデータ入力を許可し、モデルに対して要求されたアクションを実行し、結果のページの正しい表示を決定します。ビューコードの残りの部分からのディスパッチロジックの分離。

ページ上に大量のコードが重複しているため(たとえば、フッター、ヘッダー、ユーザーのセッションを処理するコード)、コントローラーは階層を形成できます。 AngularJSには、スコープが制限されたコントローラーがあります。これらは、$ routeまたは$ stateの責任であり、ディレクティブng-view / ui-viewが表示を担当するため、ユーザーリクエストを受け入れません。

ページコントローラーと同様に、AngularJSコントローラーはユーザーとのやり取りを担当し、モデルの更新を提供します。これらのモデルは、スコープに追加された後、表示から保護されません;ビューに含まれるすべてのメソッドは、最終的にユーザーアクション(スコープメソッド)になります。ページコントローラーとAngularJSコントローラーのもう1つの類似点は、それらが形成する階層です。スコープ階層はこれに対応します。したがって、簡単なアクションでベースコントローラーを分離できます。

AngularJSコントローラーは、ASP.NET WebFormsと非常によく似ています。その役割はほぼ同じです。複数のコントローラー間の階層の例を次に示します。

 <!doctype html> <html> <head> </head> <body ng-controller="MainCtrl"> <div ng-controller="ChildCtrl"> <span>{{user.name}}</span> <button ng-click="click()">Click</button> </div> </body> </html> 

 function MainCtrl($scope, $location, User) { if (!User.isAuthenticated()) { $location.path('/unauthenticated'); } } function ChildCtrl($scope, User) { $scope.click = function () { alert('You clicked me!'); }; $scope.user = User.get(0); } 

この例は、ベースコントローラーを使用してロジックを再利用する最も簡単な方法を示していますが、実稼働アプリケーションでは、コントローラーに承認ロジックを配置することはお勧めしません。さまざまなルートへのアクセスは、より高い抽象レベルで定義できます。
ChildCtrは、モデルをビューに表示し、「クリック」というボタンをクリックするなどのアクションを処理します。

その他


モジュールパターン


実際、これは「4人組」のデザインパターンではなく、「エンタープライズアプリケーションアーキテクチャのパターン」の1つでもありません。これは、カプセル化を提供することを目的とする従来のJavaScriptデザインパターンです。
モジュールテンプレートを使用すると、機能と語彙の範囲に基づいてデータプライバシーを取得できます。各モジュールには、ローカルスコープで非表示になっている0個以上のプライベートメソッドを含めることができます。

この関数は、このモジュールのパブリックAPIを提供するオブジェクトを返します。

 var Page = (function () { var title; function setTitle(t) { document.title = t; title = t; } function getTitle() { return title; } return { setTitle: setTitle, getTitle: getTitle }; }()); 

上記の例では、IIFE(Immediately-Invoked Function Expressionは、すぐに呼び出される関数の式です)。呼び出し後、2つのメソッド(setTitleおよびgetTitle)でオブジェクトを返します。返されたオブジェクトは、Page変数に割り当てられます。この場合、Pageオブジェクトのユーザーは、IIFEのローカルスコープ内で定義されている変数titleに直接アクセスできません。

モジュールテンプレートは、AngularJSでサービスを定義するときに非常に便利です。このテンプレートを使用すると、プライバシーを実現できます(実際に実現できます)。

 app.factory('foo', function () { function privateMember() { //body... } function publicMember() { //body... privateMember(); //body } return { publicMember: publicMember }; }); 

fooを他のコンポーネントに追加すると、プライベートメソッドを使用できなくなり、パブリックメソッドのみが使用できるようになります。このソリューションは、特に再利用のためにライブラリを作成する場合に非常に強力です。

データマッパー


データマッパーテンプレートは、永続的なデータストア(多くの場合、リレーショナルデータベース)とメモリ内データの間で双方向のデータ転送を実行するデータアクセスレイヤーです。このテンプレートの目的は、互いに独立して、永続データの表現をメモリに保存することです。

画像

既に述べたように、Data Mapperは、永続データストアとメモリ内のデータ間の双方向データ転送に使用されます。通常、AngularJSアプリケーションは、任意のサーバー言語(Ruby、PHP、Java、JavaScriptなど)で記述されたサーバーAPIと対話します。

RESTful APIがある場合、service $リソースはActive Recordのようなスタイルでサーバーと対話するのに役立ちます。一部のアプリケーションは、フロントエンドで使用したい最適な形式でサーバーからデータを返しませんが。

たとえば、各ユーザーが次のようなアプリケーションを持っているとしましょう:

そして、次のメソッドを持つAPI:

1つの解決策は、1つ目の方法と2つ目の方法の2つの異なるサービスを使用することです。おそらく、より適切な解決策は、Userという1つのサービスを使用することです。Userを要求すると、ユーザーの友人が読み込まれます。

 app.factory('User', function ($q) { function User(name, address, friends) { this.name = name; this.address = address; this.friends = friends; } User.get = function (params) { var user = $http.get('/user/' + params.id), friends = $http.get('/friends/' + params.id); $q.all([user, friends]).then(function (user, friends) { return new User(user.name, user.address, friends); }); }; return User; }); 


したがって、SPA要件に従ってAPIに適応する擬似データマッパーを作成しました。
Userは次のように使用できます。

 function MainCtrl($scope, User) { User.get({ id: 1 }).then(function (data) { $scope.user = data; }); } 

対応するテンプレート:

 <div> <div> Name: {{user.name}} </div> <div> Address: {{user.address}} </div> <div> Friends with ids: <ul> <li ng-repeat="friend in user.friends">{{friend}}</li> </ul> </div> </div> 


参照資料


  1. ウィキペディア設計パターンのすべての簡単な説明のソースはウィキペディアです。
  2. AngularJS' documentation
  3. AngularJS' git repository
  4. Page Controller
  5. Patterns of Enterprise Application Architecture (P of EAA)
  6. Using Dependancy Injection to Avoid Singletons
  7. Why would one use the Publish/Subscribe pattern (in JS/jQuery)?

Source: https://habr.com/ru/post/J250149/


All Articles