AngularJS開発者が犯す最も一般的な18の間違い

公開: 2022-03-11

シングルページアプリでは、フロントエンド開発者がより優れたソフトウェアエンジニアになる必要があります。 CSSとHTMLはもはや最大の懸念事項ではなく、実際、もはや1つの懸念事項だけではありません。 フロントエンド開発者は、XHR、アプリケーションロジック(モデル、ビュー、コントローラー)、パフォーマンス、アニメーション、スタイル、構造、SEO、および外部サービスとの統合を処理する必要があります。 これらすべてを組み合わせることで得られる結果は、常に優先されるべきユーザーエクスペリエンス(UX)です。

AngularJSは非常に強力なフレームワークです。 これは、GitHubで3番目にスターが付けられたリポジトリです。 使い始めるのは難しいことではありませんが、需要の理解を達成することを目的とした目標です。 AngularJS開発者は、ナビゲーションでリセットされなくなるため、メモリ消費を無視できなくなりました。 これがWeb開発の先駆者です。 抱きしめよう!

よくあるAngularJSの間違い

よくある間違い#1:DOMを介したスコープへのアクセス

本番環境で推奨される最適化の微調整がいくつかあります。 それらの1つは、デバッグ情報を無効にすることです。

DebugInfoEnabledは、デフォルトでtrueに設定され、DOMノードを介したスコープアクセスを可能にする設定です。 JavaScriptコンソールからそれを試してみたい場合は、DOM要素を選択し、次のコマンドでそのスコープにアクセスします。

 angular.element(document.body).scope()

CSSでjQueryを使用していない場合でも役立ちますが、コンソールの外部では使用しないでください。 その理由は、 $compileProvider.debugInfoEnabledがfalseに設定されている場合、DOMノードで.scope()を呼び出すとundefinedが返されるためです。

これは、本番環境で推奨される数少ないオプションの1つです。

本番環境でも、コンソールからスコープにアクセスできることに注意してください。 コンソールからangular.reloadWithDebugInfo()を呼び出すと、アプリがそれを実行します。

よくある間違い#2:そこにドットがない

ng-modelにドットがない場合は、間違っていることを読んだことがあるでしょう。 継承に関しては、そのステートメントはしばしば真実です。 スコープには、JavaScriptに典型的な継承のプロトタイプモデルがあり、ネストされたスコープはAngularJSに共通です。 多くのディレクティブは、 ngRepeatngIfngControllerなどの子スコープを作成します。 モデルを解決するとき、ルックアップは現在のスコープで開始され、 $rootScopeに至るまですべての親スコープを通過します。

ただし、新しい値を設定する場合、何が起こるかは、変更するモデル(変数)の種類によって異なります。 モデルがプリミティブの場合、子スコープは新しいモデルを作成するだけです。 ただし、変更がモデルオブジェクトのプロパティに対するものである場合、親スコープを検索すると、参照されているオブジェクトが検索され、その実際のプロパティが変更されます。 新しいモデルは現在のスコープに設定されないため、マスキングは発生しません。

 function MainController($scope) { $scope.foo = 1; $scope.bar = {innerProperty: 2}; } angular.module('myApp', []) .controller('MainController', MainController);
 <div ng-controller="MainController"> <p>OUTER SCOPE:</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <div ng-if="foo"> <!— ng-if creates a new scope —> <p>INNER SCOPE</p> <p>{{ foo }}</p> <p>{{ bar.innerProperty }}</p> <button ng-click="foo = 2">Set primitive</button> <button ng-click="bar.innerProperty = 3">Mutate object</button> </div> </div>

「プリミティブの設定」というラベルの付いたボタンをクリックすると、内部スコープのfooが2に設定されますが、外部スコープのfooは変更されません。

「オブジェクトの変更」というラベルの付いたボタンをクリックすると、親スコープからbarプロパティが変更されます。 内部スコープには変数がないため、シャドウイングは発生せず、barの表示値は両方のスコープで3になります。

これを行う別の方法は、親スコープとルートスコープがすべてのスコープから参照されるという事実を活用することです。 $parentオブジェクトと$rootオブジェクトを使用して、ビューから直接親スコープと$rootScopeにそれぞれアクセスできます。 これは強力な方法かもしれませんが、ストリームの特定のスコープをターゲットにすることに問題があるため、私はそれが好きではありません。 スコープに固有のプロパティを設定してアクセスする別の方法がありますcontrollerAs構文を使用します。

よくある間違い#3:controllerAs構文を使用しない

注入された$scopeの代わりにコントローラーオブジェクトを使用するようにモデルを割り当てるための代替の最も効率的な方法。 スコープを挿入する代わりに、次のようなモデルを定義できます。

 function MainController($scope) { this.foo = 1; var that = this; var setBar = function () { // that.bar = {someProperty: 2}; this.bar = {someProperty: 2}; }; setBar.call(this); // there are other conventions: // var MC = this; // setBar.call(this); when using 'this' inside setBar() }
 <div> <p>OUTER SCOPE:</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <div ng-if="test1"> <p>INNER SCOPE</p> <p>{{ MC.foo }}</p> <p>{{ MC.bar.someProperty }}</p> <button ng-click="MC.foo = 3">Change MC.foo</button> <button ng-click="MC.bar.someProperty = 5">Change MC.bar.someProperty</button> </div> </div>

これははるかに混乱が少ないです。 特に、ネストされた状態の場合のように、ネストされたスコープが多数ある場合。

controllerAs構文にはさらに多くのものがあります。

よくある間違い#4:controllerAs構文を十分に活用していない

コントローラオブジェクトがどのように公開されるかについて、いくつかの注意点があります。 これは基本的に、通常のモデルと同様に、コントローラーのスコープに設定されたオブジェクトです。

コントローラオブジェクトのプロパティを監視する必要がある場合は、関数を監視できますが、必須ではありません。 次に例を示します。

 function MainController($scope) { this.title = 'Some title'; $scope.$watch(angular.bind(this, function () { return this.title; }), function (newVal, oldVal) { // handle changes }); }

実行する方が簡単です。

 function MainController($scope) { this.title = 'Some title'; $scope.$watch('MC.title', function (newVal, oldVal) { // handle changes }); }

つまり、スコープチェーンの下流でも、子コントローラーからMCにアクセスできます。

 function NestedController($scope) { if ($scope.MC && $scope.MC.title === 'Some title') { $scope.MC.title = 'New title'; } }

ただし、それを実行できるようにするには、controllerAに使用する頭字語と一致している必要があります。 それを設定する方法は少なくとも3つあります。 あなたはすでに最初のものを見ました:

 <div ng-controller="MainController as MC"> … </div>

ただし、 ui-routerを使用する場合、そのようにコントローラーを指定するとエラーが発生しやすくなります。 状態の場合、コントローラーは状態構成で指定する必要があります。

 angular.module('myApp', []) .config(function ($stateProvider) { $stateProvider .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/path/to/template.html' }) }). controller('MainController', function () { … });

注釈を付ける別の方法があります。

 (…) .state('main', { url: '/', controller: 'MainController', controllerAs: 'MC', templateUrl: '/path/to/template.html' })

ディレクティブでも同じことができます。

 function AnotherController() { this.text = 'abc'; } function testForToptal() { return { controller: 'AnotherController as AC', template: '<p>{{ AC.text }}</p>' }; } angular.module('myApp', []) .controller('AnotherController', AnotherController) .directive('testForToptal', testForToptal);

あまり簡潔ではありませんが、注釈を付ける他の方法も有効です。

 function testForToptal() { return { controller: 'AnotherController', controllerAs: 'AC', template: '<p>{{ AC.text }}</p>' }; }

よくある間違い#5:UIで名前付きビューを使用しない-ROUTERForPower」

AngularJSの事実上のルーティングソリューションは、これまでui-routerでした。 しばらく前にコアから削除されたngRouteモジュールは、より高度なルーティングには基本的すぎました。

途中で新しいNgRouterがありますが、作者はまだそれを生産するには時期尚早だと考えています。 私がこれを書いているとき、安定したAngularは1.3.15であり、 ui-routerは揺れています。

主な理由:

  • 素晴らしい状態の入れ子
  • ルートの抽象化
  • オプションおよび必須パラメーター

ここでは、AngularJSエラーを回避するための状態のネストについて説明します。

これは、複雑でありながら標準的なユースケースと考えてください。 ホームページビューと商品ビューのアプリがあります。 製品ビューには、イントロ、ウィジェット、コンテンツの3つのセクションがあります。 状態を切り替えるときにウィジェットを保持し、リロードしないようにします。 ただし、コンテンツはリロードする必要があります。

次のHTML製品インデックスページ構造を検討してください。

 <body> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <h2>SOME PRODUCT SPECIFIC INTRO</h2> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <!-- some widget, which should never reload --> </section> </div> <div class="col-xs-9"> <section class="content"> <div class="product-content"> <h2>Product title</h2> <span>Context-specific content</span> </div> </section> </div> </div> </div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer> </body>

これはHTMLコーダーから取得できるものであり、ファイルと状態に分割する必要があります。 私は通常、必要に応じてグローバルデータを保持する抽象MAIN状態があるという規則に従います。 $rootScopeの代わりにそれを使用してください。 メイン状態は、すべてのページで必要な静的HTMLも保持します。 index.htmlをクリーンに保ちます。

 <!— index.html —> <body> <div ui-view></div> </body>
 <!— main.html —> <header> <!-- SOME STATIC HEADER CONTENT --> </header> <section class="main"> <div ui-view></div> </section> <footer> <!-- SOME STATIC HEADER CONTENT --> </footer>

次に、製品インデックスページを見てみましょう。

 <div class="page-content"> <div class="row"> <div class="col-xs-12"> <section class="intro"> <div ui-view="intro"></div> </section> </div> </div> <div class="row"> <div class="col-xs-3"> <section class="widget"> <div ui-view="widget"></div> </section> </div> <div class="col-xs-9"> <section class="content"> <div ui-view="content"></div> </section> </div> </div> </div>

ご覧のとおり、製品インデックスページには3つの名前付きビューがあります。 1つはイントロ用、もう1つはウィジェット用、もう1つは製品用です。 スペックを満たしています! それでは、ルーティングを設定しましょう。

 function config($stateProvider) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { abstract: true, url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html' }) // A SIMPLE HOMEPAGE .state('main.homepage', { url: '', controller: 'HomepageController as HC', templateUrl: '/routing-demo/homepage.html' }) // THE ABOVE IS ALL GOOD, HERE IS TROUBLE // A COMPLEX PRODUCT PAGE .state('main.product', { abstract: true, url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'widget': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }); } angular.module('articleApp', [ 'ui.router' ]) .config(config);

それが最初のアプローチです。 では、 main.product.indexmain.product.detailsを切り替えるとどうなりますか? コンテンツとウィジェットはリロードされますが、コンテンツをリロードするだけです。 これには問題があり、開発者は実際にその機能だけをサポートするルーターを作成しました。 この名前の1つは、スティッキービューでした。 幸い、 ui-routerは、絶対的な名前付きビューターゲティングを使用して、すぐにそれをサポートします。

 // A COMPLEX PRODUCT PAGE // WITH NO MORE TROUBLE .state('main.product', { abstract: true, url: ':id', views: { // TARGETING THE UNNAMED VIEW IN MAIN.HTML '@main': { controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html' }, // TARGETING THE WIDGET VIEW IN PRODUCT.HTML // BY DEFINING A CHILD VIEW ALREADY HERE, WE ENSURE IT DOES NOT RELOAD ON CHILD STATE CHANGE '[email protected]': { controller: 'WidgetController as PWC', templateUrl: '/routing-demo/widget.html' } } }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } }) // PRODUCT DETAILS SUBSTATE .state('main.product.details', { url: '/details', views: { 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } } });

状態定義を親ビューに移動することで、これも抽象的であり、通常はその子の兄弟に影響を与えるURLを切り替えるときに、子ビューが再ロードされないようにすることができます。 もちろん、ウィジェットは単純なディレクティブでもかまいません。 しかし、要点は、それは別の複雑なネストされた状態である可能性もあるということです。

$urlRouterProvider.deferIntercept()を使用してこれを行う別の方法がありますが、実際には状態構成を使用する方が優れていると思います。 ルートの傍受に興味がある場合は、StackOverflowに関する小さなチュートリアルを作成しました。

よくある間違い#6:匿名関数を使用してAngularWorldのすべてを宣言する

この間違いは口径が軽いものであり、AngularJSエラーメッセージを回避するよりもスタイルの問題です。 以前、私が匿名関数をAngularInternalの宣言に渡すことはめったにないことに気づいたかもしれません。 私は通常、最初に関数を定義してから、それを渡します。

これは単なる機能以上のものです。 このアプローチは、スタイルガイド、特にAirbnbとToddMottoを読んで得たものです。 いくつかの利点があり、欠点はほとんどないと思います。

まず、関数とオブジェクトが変数に割り当てられている場合、それらを操作および変更するのがはるかに簡単になります。 第二に、コードはよりクリーンで、ファイルに簡単に分割できます。 それは保守性を意味します。 グローバル名前空間を汚染したくない場合は、すべてのファイルをIIFEでラップします。 3番目の理由はテスト容易性です。 この例を考えてみましょう。

 'use strict'; function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE publicMethod1('someArgument'); }; // IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE return { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; } angular.module('app', []) .factory('yoda', yoda);

これで、 publicMethod1をモックできますが、公開されているので、なぜそれを行う必要があるのでしょうか。 既存の方法をスパイするだけの方が簡単ではないでしょうか。 ただし、このメソッドは実際には別の関数であり、薄いラッパーです。 このアプローチを見てください:

 function yoda() { var privateMethod = function () { // this function is not exposed }; var publicMethod1 = function () { // this function is exposed, but it's internals are not exposed // some logic... }; var publicMethod2 = function (arg) { // the below call cannot be spied on publicMethod1('someArgument'); // BUT THIS ONE CAN! hostObject.publicMethod1('aBetterArgument'); }; var hostObject = { publicMethod1: function () { return publicMethod1(); }, publicMethod2: function (arg) { return publicMethod2(arg); } }; return hostObject; }

事実上、コードはより再利用可能で慣用的であるため、これはスタイルだけではありません。 開発者はより表現力を得る。 すべてのコードを自己完結型のブロックに分割すると、簡単になります。

よくある間違い#7:ワーカーを使用してAngularAKAで大量の処理を行う

シナリオによっては、一連のフィルター、デコレーター、そして最後に並べ替えアルゴリズムを通過させることにより、複雑なオブジェクトの大規模な配列を処理する必要がある場合があります。 ユースケースの1つは、アプリをオフラインで動作させる必要がある場合、またはデータ表示のパフォーマンスが重要な場合です。 また、JavaScriptはシングルスレッドであるため、ブラウザーをフリーズするのは比較的簡単です。

Webワーカーでも簡単に回避できます。 特にAngularJS用にそれを処理する人気のあるライブラリはないようです。 ただし、実装は簡単なので、最善の方法かもしれません。

まず、サービスを設定しましょう。

 function scoringService($q) { var scoreItems = function (items, weights) { var deferred = $q.defer(); var worker = new Worker('/worker-demo/scoring.worker.js'); var orders = { items: items, weights: weights }; worker.postMessage(orders); worker.onmessage = function (e) { if (e.data && e.data.ready) { deferred.resolve(e.data.items); } }; return deferred.promise; }; var hostObject = { scoreItems: function (items, weights) { return scoreItems(items, weights); } }; return hostObject; } angular.module('app.worker') .factory('scoringService', scoringService);

さて、労働者:

 'use strict'; function scoringFunction(items, weights) { var itemsArray = []; for (var i = 0; i < items.length; i++) { // some heavy processing // itemsArray is populated, etc. } itemsArray.sort(function (a, b) { if (a.sum > b.sum) { return -1; } else if (a.sum < b.sum) { return 1; } else { return 0; } }); return itemsArray; } self.addEventListener('message', function (e) { var reply = { ready: true }; if (e.data && e.data.items && e.data.items.length) { reply.items = scoringFunction(e.data.items, e.data.weights); } self.postMessage(reply); }, false);

ここで、通常どおりサービスを挿入し、promiseを返すサービスメソッドと同じようにscoringService.scoreItems()を処理します。 重い処理は別のスレッドで実行され、UXに害はありません。

注意すべき点:

  • スポーンするワーカーの数に関する一般的なルールはないようです。 一部の開発者は8が適切な数値であると主張していますが、オンライン計算機を使用して自分に合ったものにしてください
  • 古いブラウザとの互換性を確認してください
  • サービスからワーカーに番号0を渡すときに問題が発生します。 渡されたプロパティに.toString()を適用しましたが、正しく機能しました。

よくある間違い#8:使いすぎと誤解による解決

解決すると、ビューのロードに余分な時間が追加されます。 フロントエンドアプリの高性能が私たちの主な目標だと思います。 アプリがAPIからのデータを待機している間に、ビューの一部をレンダリングすることは問題ではありません。

この設定を検討してください。

 function resolve(index, timeout) { return { data: function($q, $timeout) { var deferred = $q.defer(); $timeout(function () { deferred.resolve(console.log('Data resolve called ' + index)); }, timeout); return deferred.promise; } }; } function configResolves($stateProvide) { $stateProvider // MAIN ABSTRACT STATE, ALWAYS ON .state('main', { url: '/', controller: 'MainController as MC', templateUrl: '/routing-demo/main.html', resolve: resolve(1, 1597) }) // A COMPLEX PRODUCT PAGE .state('main.product', { url: ':id', controller: 'ProductController as PC', templateUrl: '/routing-demo/product.html', resolve: resolve(2, 2584) }) // PRODUCT DEFAULT SUBSTATE .state('main.product.index', { url: '', views: { 'intro': { controller: 'IntroController as PIC', templateUrl: '/routing-demo/intro.html' }, 'content': { controller: 'ContentController as PCC', templateUrl: '/routing-demo/content.html' } }, resolve: resolve(3, 987) }); }

コンソール出力は次のようになります。

 Data resolve called 3 Data resolve called 1 Data resolve called 2 Main Controller executed Product Controller executed Intro Controller executed

これは基本的に次のことを意味します。

  • 解決は非同期で実行されます
  • 実行の順序に依存することはできません(または少なくともかなり柔軟にする必要があります)
  • すべての状態は、抽象的でなくても、すべての解決策が実行されるまでブロックされます。

これは、ユーザーが出力を表示する前に、すべての依存関係を待つ必要があることを意味します。 そのデータが必要です、確かに、大丈夫です。 ビューの前に絶対に必要な場合は、 .run()ブロックに入れてください。 それ以外の場合は、コントローラーからサービスを呼び出して、ハーフロード状態を適切に処理します。 進行中の作業を確認すること(およびコントローラーはすでに実行されているため、実際には進行中です)は、アプリを停止させるよりも優れています。

よくある間違い#9:アプリを最適化しない-3つの例

a)モデルへのスライダーの取り付けなど、ダイジェストループが多すぎる原因

これはAngularJSエラーを引き起こす可能性のある一般的な問題ですが、スライダーの例で説明します。 拡張機能が必要だったので、このスライダーライブラリである角度範囲スライダーを使用していました。 そのディレクティブには、最小バージョンで次の構文があります。

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.price" > </div> </body>

コントローラ内の次のコードを検討してください。

 this.maxPrice = '100'; this.price = '55'; $scope.$watch('MC.price', function (newVal) { if (newVal || newVal === 0) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } });

そのため、動作が遅くなります。 カジュアルな解決策は、入力にタイムアウトを設定することです。 しかし、それが常に便利であるとは限りません。また、すべての場合に実際のモデル変更を遅らせたくない場合もあります。

したがって、タイムアウト時に作業モデルを変更するためにバインドされた一時モデルを追加します。

 <body ng-controller="MainController as MC"> <div range-slider min="0" max="MC.maxPrice" pin-handle="min" model-max="MC.priceTemporary" > </div> </body>

およびコントローラー内:

 this.maxPrice = '100'; this.price = '55'; this.priceTemporary = '55'; $scope.$watch('MC.price', function (newVal) { if (!isNaN(newVal)) { for (var i = 0; i < 987; i++) { console.log('ALL YOUR BASE ARE BELONG TO US'); } } }); var timeoutInstance; $scope.$watch('MC.priceTemporary', function (newVal) { if (!isNaN(newVal)) { if (timeoutInstance) { $timeout.cancel(timeoutInstance); } timeoutInstance = $timeout(function () { $scope.MC.price = newVal; }, 144); } });

b)$applyAsyncを使用しない

AngularJSには、 $digest()を呼び出すためのポーリングメカニズムがありません。 これは、コードを評価して後でダイジェストを呼び出すディレクティブ( ng-clickinputなど)、サービス( $timeout$http )、およびメソッド( $watch )を使用するためにのみ実行されます。

.$applyAsync()が行うことは、式の解決を次の$digest()サイクルまで遅らせることです。これは、0タイムアウト(実際には約10ms)の後にトリガーされます。

現在、 applyAsyncを使用する方法は2つあります。 $httpリクエストの自動化された方法と、残りの手動の方法。

ほぼ同時に返されるすべてのhttpリクエストを1つのダイジェストで解決するには、次のようにします。

 mymodule.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });

手動による方法は、実際にどのように機能するかを示しています。 バニラJSイベントリスナーまたは.click() 、またはその他の外部ライブラリへのコールバックで実行される関数について考えてみます。 モデルを実行して変更した後、まだ$apply()でラップしていない場合は、 $scope.$root.$digest()$rootScope.$digest() )、または少なくとも$scope.$digest()を呼び出す必要があります。 $scope.$digest() 。 それ以外の場合、変更は表示されません。

1つのフローでこれを複数回実行すると、実行が遅くなる可能性があります。 代わりに、式で$scope.$applyAsync()を呼び出すことを検討してください。 それらすべてに対して1つのダイジェストサイクルのみを呼び出すように設定されます。

c)画像の大量処理を行う

パフォーマンスが低下した場合は、Chromeデベロッパーツールのタイムラインを使用して理由を調査できます。 このツールについては、間違い#17で詳しく説明します。 記録後にタイムライングラフが緑色で表示される場合は、パフォーマンスの問題が画像の処理に関連している可能性があります。 これは厳密にはAngularJSとは関係ありませんが、AngularJSのパフォーマンスの問題(グラフではほとんど黄色)に加えて発生する可能性があります。 フロントエンドエンジニアとして、私たちは完全なエンドプロジェクトについて考える必要があります。

評価する時間を取ってください:

  • 視差を使用していますか?
  • コンテンツの複数のレイヤーが互いに重なり合っていますか?
  • 画像を動かしますか?
  • 画像を拡大縮小しますか(背景サイズなど)?
  • 画像のサイズをループで変更しますか?サイズ変更時にダイジェストループが発生する可能性がありますか?

上記の少なくとも3つに「はい」と答えた場合は、緩和することを検討してください。 おそらく、さまざまな画像サイズを提供でき、サイズ変更はまったくできません。 たぶん、「transform:translateZ(0)」強制GPU処理ハックを追加することができます。 または、ハンドラーにrequestAnimationFrameを使用します。

よくある間違い#10:jQueryingIt-切り離されたDOMツリー

AngularJSでjQueryを使用することは推奨されておらず、避けるべきだとよく耳にします。 これらのステートメントの背後にある理由を理解することが不可欠です。 私が見る限り、少なくとも3つの理由がありますが、実際のブロッカーではありません。

理由1: jQueryコードを実行するときは、自分で$digest()を呼び出す必要があります。 多くの場合、AngularJS用に調整されたAngularJSソリューションがあり、jQuery(ng-clickやイベントシステムなど)よりもAngular内でより適切に使用できます。

理由2:アプリの構築についての考え方。 ナビゲート時にリロードするJavaScriptをWebサイトに追加している場合は、メモリ消費についてあまり心配する必要はありません。 シングルページアプリでは、心配する必要があります。 クリーンアップしないと、アプリに数分以上費やすユーザーでパフォーマンスの問題が増大する可能性があります。

理由3:クリーンアップは、実際には実行および分析するのが最も簡単なことではありません。 (ブラウザーで)スクリプトからガベージコレクターを呼び出す方法はありません。 DOMツリーが切り離されてしまう可能性があります。 例を作成しました(jQueryはindex.htmlにロードされます):

 <section> <test-for-toptal></test-for-toptal> <button ng-click="MC.removeDirective()">remove directive</button> </section>
 function MainController($rootScope, $scope) { this.removeDirective = function () { $rootScope.$emit('destroyDirective'); }; } function testForToptal($rootScope, $timeout) { return { link: function (scope, element, attributes) { var destroyListener = $rootScope.$on('destroyDirective', function () { scope.$destroy(); }); // adding a timeout for the DOM to get ready $timeout(function () { scope.toBeDetached = element.find('p'); }); scope.$on('$destroy', function () { destroyListener(); element.remove(); }); }, template: '<div><p>I AM DIRECTIVE</p></div>' }; } angular.module('app', []) .controller('MainController', MainController) .directive('testForToptal', testForToptal);

これは、テキストを出力する単純なディレクティブです。 その下にボタンがあり、ディレクティブを手動で破棄します。

したがって、ディレクティブが削除されても、scope.toBeDetachedにDOMツリーへの参照が残ります。 Chrome開発ツールでは、[プロファイル]タブにアクセスしてから[ヒープスナップショットを作成]タブにアクセスすると、次のように出力されます。

あなたは少数で生きることができます、しかしあなたがトンを持っているならばそれは悪いです。 特に、例のように何らかの理由でスコープに保存する場合は特にそうです。 DOM全体がすべてのダイジェストで評価されます。 問題のある切り離されたDOMツリーは、4つのノードを持つものです。 では、これをどのように解決できますか?

 scope.$on('$destroy', function () { // setting this model to null // will solve the problem. scope.toBeDetached = null; destroyListener(); element.remove(); });

4つのエントリを持つ切り離されたDOMツリーが削除されます!

この例では、ディレクティブは同じスコープを使用し、DOM要素をスコープに格納します。 そのようにそれを示すのは私にとってより簡単でした。 変数に格納できるため、必ずしもそれほど悪くなるとは限りません。 ただし、その変数または同じ関数スコープの他の変数を参照していたクロージャが存在する場合は、それでもメモリを消費します。

よくある間違い#11:孤立したスコープの使いすぎ

単一の場所で使用されることがわかっているディレクティブが必要な場合、または使用されている環境と競合することが予想されない場合は常に、分離スコープを使用する必要はありません。 最近、再利用可能なコンポーネントを作成する傾向がありますが、コア角度ディレクティブは分離スコープをまったく使用しないことをご存知ですか?

主な理由は2つあります。要素に2つの分離されたスコープディレクティブを適用できないことと、ネスト/継承/イベント処理で問題が発生する可能性があることです。 特にトランスクルージョンに関しては、効果はあなたが期待するものではないかもしれません。

したがって、これは失敗します。

 <p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"></p>

また、ディレクティブを1つだけ使用した場合でも、isolatedScopeモデルもisolatedScopeDirectiveでブロードキャストされたイベントもAnotherControllerで使用できないことに気付くでしょう。 悲しいことに、トランスクルージョンマジックを柔軟に使用して機能させることができますが、ほとんどのユースケースでは、分離する必要はありません。

 <p isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux"> <div ng-controller="AnotherController"> … the isolated scope is not available here, look: {{ isolatedModel }} </div> </p>

だから、今2つの質問:

  1. 同じスコープのディレクティブで親スコープモデルをどのように処理できますか?
  2. 新しいモデル値をどのようにインスタンス化できますか?

2つの方法があり、どちらも属性に値を渡します。 このMainControllerについて考えてみましょう。

 function MainController($interval) { this.foo = { bar: 1 }; this.baz = 1; var that = this; $interval(function () { that.foo.bar++; }, 144); $interval(function () { that.baz++; }, 144); this.quux = [1,2,3]; }

これはこのビューを制御します:

 <body ng-controller="MainController as MC"> <div class="cyan-surface"> <h1>Attributes test</h1> <test-directive watch-attribute="MC.foo" observe-attribute="current index: {{ MC.baz }}"></test-directive> </div> </body>

「watch-attribute」は補間されていないことに注意してください。 JSの魔法のおかげで、すべてうまくいきます。 ディレクティブの定義は次のとおりです。

 function testDirective() { var postLink = function (scope, element, attrs) { scope.$watch(attrs.watchAttribute, function (newVal) { if (newVal) { // take a look in the console // we can't use the attribute directly console.log(attrs.watchAttribute); // the newVal is evaluated, and it can be used scope.modifiedFooBar = newVal.bar * 10; } }, true); attrs.$observe('observeAttribute', function (newVal) { scope.observed = newVal; }); }; return { link: postLink, templateUrl: '/attributes-demo/test-directive.html' }; }

attrs.watchAttributeが引用符なしでscope.$watch()に渡されることに注意してください。 つまり、実際に$ watchに渡されたのは、文字列MC.fooた。 ただし、 $watch()に渡された文字列はスコープに対して評価され、 MC.fooがスコープで使用できるため、機能します。 これは、AngularJSコアディレクティブで属性を監視する最も一般的な方法でもあります。

テンプレートについてはgithubのコードを参照し、さらにすばらしいものについては$parse$evalを調べてください。

よくある間違い#12:自分の後でクリーンアップしない-ウォッチャー、間隔、タイムアウト、変数

AngularJSはあなたに代わっていくつかの作業を行いますが、すべてではありません。 以下は手動でクリーンアップする必要があります。

  • 現在のスコープにバインドされていない(たとえば、$ rootScopeにバインドされている)ウォッチャー
  • 間隔
  • タイムアウト
  • ディレクティブでDOMを参照する変数
  • 危険なjQueryプラグイン、たとえばJavaScriptの$destroyイベントに反応するハンドラーを持たないプラグイン

これを手動で行わないと、予期しない動作やメモリリークが発生します。 さらに悪いことに、これらはすぐには表示されませんが、最終的には忍び寄ります。 マーフィーの法則。

驚くべきことに、AngularJSはこれらすべてに対処するための便利な方法を提供します。

 function cleanMeUp($interval, $rootScope, $timeout) { var postLink = function (scope, element, attrs) { var rootModelListener = $rootScope.$watch('someModel', function () { // do something }); var myInterval = $interval(function () { // do something in intervals }, 2584); var myTimeout = $timeout(function () { // defer some action here }, 1597); scope.domElement = element; $timeout(function () { // calling $destroy manually for testing purposes scope.$destroy(); }, 987); // here is where the cleanup happens scope.$on('$destroy', function () { // disable the listener rootModelListener(); // cancel the interval and timeout $interval.cancel(myInterval); $timeout.cancel(myTimeout); // nullify the DOM-bound model scope.domElement = null; }); element.on('$destroy', function () { // this is a jQuery event // clean up all vanilla JavaScript / jQuery artifacts here // respectful jQuery plugins have $destroy handlers, // that is the reason why this event is emitted... // follow the standards. }); };

jQueryの$destroyイベントに注目してください。 AngularJSと同じように呼ばれますが、個別に処理されます。 スコープ$watchersはjQueryイベントに反応しません。

よくある間違い#13:ウォッチャーの数が多すぎる

これは今では非常に簡単なはずです。 ここで理解すべきことが1つあります。 $digest()です。 バインディング{{ model }}ごとに、AngularJSはウォッチャーを作成します。 すべてのダイジェストフェーズで、そのような各バインディングが評価され、前の値と比較されます。 これはダーティチェックと呼ばれ、$digestが行うことです。 最後のチェック以降に値が変更された場合、ウォッチャーコールバックが発生します。 If that watcher callback modifies a model ($scope variable), a new $digest cycle is fired (up to a maximum of 10) when an exception is thrown.

Browsers don't have problems even with thousands of bindings, unless the expressions are complex. The common answer for “how many watchers are ok to have” is 2000.

So, how can we limit the number of watchers? By not watching scope models when we don't expect them to change. It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.

 <li ng-repeat="item in ::vastArray">{{ ::item.velocity }}</li>

After vastArray and item.velocity are evaluated once, they will never change again. You can still apply filters to the array, they will work just fine. It is just that the array itself will not be evaluated. In many cases, that is a win.

Common Mistake #14: Misunderstanding The Digest

This AngularJS error was already partly covered in mistakes 9.b and in 13. This is a more thorough explanation. AngularJS updates DOM as a result of callback functions to watchers. Every binding, that is the directive {{ someModel }} sets up watchers, but watchers are also set for many other directives like ng-if and ng-repeat . Just take a look at the source code, it is very readable. Watchers can also be set manually, and you have probably done that at least a few times yourself.

$watch() ers are bound to scopes. $Watchers can take strings, which are evaluated against the scope that the $watch() was bound to. They can also evaluate functions. And they also take callbacks. So, when $rootScope.$digest() is called, all the registered models (that is $scope variables) are evaluated and compared against their previous values. If the values don't match, the callback to the $watch() is executed.

It is important to understand that even though a model's value was changed, the callback does not fire until the next digest phase. It is called a “phase” for a reason - it can consist of several digest cycles. If only a watcher changes a scope model, another digest cycle is executed.

But $digest() is not polled for . It is called from core directives, services, methods, etc. If you change a model from a custom function that does not call .$apply , .$applyAsync , .$evalAsync , or anything else that eventually calls $digest() , the bindings will not be updated.

By the way, the source code for $digest() is actually quite complex. It is nevertheless worth reading, as the hilarious warnings make up for it.

Common Mistake #15: Not Relying On Automation, Or Relying On It Too Much

If you follow the trends within front end development and are a bit lazy - like me - then you probably try to not do everything by hand. Keeping track of all your dependencies, processing sets of files in different ways, reloading the browser after every file save - there is a lot more to developing than just coding.

So you may be using bower, and maybe npm depending on how you serve your app. There is a chance that you may be using grunt, gulp, or brunch. Or bash, which also is cool. In fact, you may have started your latest project with some Yeoman generator!

This leads to the question: do you understand the whole process of what your infrastructure really does? Do you need what you have, especially if you just spent hours trying to fix your connect webserver livereload functionality?

Take a second to assess what you need. All those tools are only here to aid you, there is no other reward for using them. The more experienced developers I talk to tend to simplify things.

Common Mistake #16: Not Running The Unit Tests In TDD Mode

Tests will not make your code free of AngularJS error messages. What they will do is assure that your team doesn't run into regression issues all the time.

I am writing specifically about unit tests here, not because I feel they are more important than e2e tests, but because they execute much faster. I must admit that the process I am about to describe is a very pleasurable one.

Test Driven Development as an implementation for eg gulp-karma runner, basically runs all your unit tests on every file save. My favorite way to write tests is, I just write empty assurances first:

 describe('some module', function () { it('should call the name-it service…', function () { // leave this empty for now }); ... });

After that, I write or refactor the actual code, then I come back to the tests and fill in the assurances with actual test code.

Having a TDD task running in a terminal speeds up the process by about 100%. Unit tests execute in a matter of a few seconds, even if you have a lot of them. Just save the test file and the runner will pick it up, evaluate your tests, and provide feedback instantly.

With e2e tests, the process is much slower. My advice - split e2e tests up into test suites and just run one at a time. Protractor has support for them, and below is the code I use for my test tasks (I like gulp).

 'use strict'; var gulp = require('gulp'); var args = require('yargs').argv; var browserSync = require('browser-sync'); var karma = require('gulp-karma'); var protractor = require('gulp-protractor').protractor; var webdriverUpdate = require('gulp-protractor').webdriver_update; function test() { // Be sure to return the stream // NOTE: Using the fake './foobar' so as to run the files // listed in karma.conf.js INSTEAD of what was passed to // gulp.src ! return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'run' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); this.emit('end'); //instead of erroring the stream, end it }); } function tdd() { return gulp.src('./foobar') .pipe(karma({ configFile: 'test/karma.conf.js', action: 'start' })) .on('error', function(err) { // Make sure failed tests cause gulp to exit non-zero // console.log(err); // this.emit('end'); // not ending the stream here }); } function runProtractor () { var argument = args.suite || 'all'; // NOTE: Using the fake './foobar' so as to run the files // listed in protractor.conf.js, instead of what was passed to // gulp.src return gulp.src('./foobar') .pipe(protractor({ configFile: 'test/protractor.conf.js', args: ['--suite', argument] })) .on('error', function (err) { // Make sure failed tests cause gulp to exit non-zero throw err; }) .on('end', function () { // Close browser sync server browserSync.exit(); }); } gulp.task('tdd', tdd); gulp.task('test', test); gulp.task('test-e2e', ['webdriver-update'], runProtractor); gulp.task('webdriver-update', webdriverUpdate);

Common Mistake #17: Not Using The Available Tools

A - chrome breakpoints

Chrome dev tools allow you to point at a specific place in any of the files loaded into the browser, pause code execution at that point, and let you interact with all the variables available from that point. That is a lot! That functionality does not require you to add any code at all, everything happens in the dev tools.

Not only you get access to all the variables, you also see the call stack, print stack traces, and more. You can even configure it to work with minified files. Read about it here.

There are other ways you can get similar run-time access, eg by adding console.log() calls. But breakpoints are more sophisticated.

AngularJS also allows you to access scope through DOM elements (as long as debugInfo is enabled), and inject available services through the console. Consider the following in the console:

 $(document.body).scope().$root

or point at an element in the inspector, and then:

 $($0).scope()

Even if debugInfo is not enabled, you can do:

 angular.reloadWithDebugInfo()

And have it available after reload:

To inject and interact with a service from the console, try:

 var injector = $(document.body).injector(); var someService = injector.get('someService');

B - chrome timeline

Another great tool that comes with dev tools is the timeline. That will allow you to record and analyse your app's live performance as you are using it. The output shows, among others, memory usage, frame rate, and the dissection of the different processes that occupy the CPU: loading, scripting, rendering, and painting.

If you experience that your app's performance degrades, you will most likely be able to find the cause for that through the timeline tab. Just record your actions which led to performance issues and see what happens. Too many watchers? You will see yellow bars taking a lot of space. Memory leaks? You can see how much memory was consumed over time on a graph.

A detailed description: https://developer.chrome.com/devtools/docs/timeline

C - inspecting apps remotely on iOS and Android

If you are developing a hybrid app or a responsive web app, you can access your device's console, DOM tree, and all other tools available either through Chrome or Safari dev tools. That includes the WebView and UIWebView.

まず、ホスト0.0.0.0でWebサーバーを起動して、ローカルネットワークからアクセスできるようにします。 設定でWebインスペクターを有効にします。 次に、デバイスをデスクトップに接続し、通常の「localhost」の代わりにマシンのIPを使用して、ローカル開発ページにアクセスします。 これで、デスクトップのブラウザからデバイスを利用できるようになります。

AndroidとiOSの詳細な手順は、次のとおりです。非公式のガイドは、Googleから簡単に見つけることができます。

私は最近、browserSyncでいくつかの素晴らしい経験をしました。 これはlivereloadと同じように機能しますが、browserSyncを介して同じページを表示しているすべてのブラウザーを実際に同期します。 これには、スクロールやボタンのクリックなどのユーザー操作が含まれます。デスクトップからiPadのページを制御しながら、iOSアプリのログ出力を見ていました。 それはうまくいきました!

よくある間違い#18:NG-INITの例でソースコードを読み取らない

Ng-initは、その音からすると、 ng-ifng-repeatに似ているはずですよね? なぜそれを使用すべきではないというコメントがドキュメントにあるのか疑問に思ったことはありますか? 意外だった私見! ディレクティブがモデルを初期化することを期待します。 それはそれが行うことでもありますが…それは別の方法で実装されます。つまり、属性値を監視しません。 AngularJSのソースコードを閲覧する必要はありません。それをお届けします。

 var ngInitDirective = ngDirective({ priority: 450, compile: function() { return { pre: function(scope, element, attrs) { scope.$eval(attrs.ngInit); } }; } });

あなたが期待するよりも少ないですか? 厄介なディレクティブ構文以外に、かなり読みやすいですね。 6行目はそれがすべてであるものです。

ng-showと比較してください。

 var ngShowDirective = ['$animate', function($animate) { return { restrict: 'A', multiElement: true, link: function(scope, element, attr) { scope.$watch(attr.ngShow, function ngShowWatchAction(value) { // we're adding a temporary, animation-specific class for ng-hide since this way // we can control when the element is actually displayed on screen without having // to have a global/greedy CSS selector that breaks when other animations are run. // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { tempClasses: NG_HIDE_IN_PROGRESS_CLASS }); }); } }; }];

繰り返しますが、6行目です。 そこに$watchがあり、それがこのディレクティブを動的にします。 AngularJSソースコードでは、すべてのコードの大部分は、最初からほとんど読めるコードを説明するコメントです。 AngularJSについて学ぶのに最適な方法だと思います。

結論

最も一般的なAngularJSの間違いをカバーするこのガイドは、他のガイドのほぼ2倍の長さです。 当然そのようになりました。 高品質のJavaScriptフロントエンドエンジニアに対する需要は非常に高いです。 AngularJSは現在非常にホットであり、数年間、最も人気のある開発ツールの中で安定した位置を占めています。 AngularJS 2.0が開発中であるため、今後数年間はAngularJS2.0が主流になるでしょう。

フロントエンド開発の優れている点は、非常にやりがいがあることです。 私たちの仕事は即座に目に見え、人々は私たちが提供する製品と直接対話します。 JavaScriptの学習に費やした時間、そしてJavaScript言語に焦点を当てるべきだと私は信じていますが、これは非常に良い投資です。 それはインターネットの言語です。 競争は超激しいです! 私たちの焦点は1つあります。それは、ユーザーエクスペリエンスです。 成功するには、すべてをカバーする必要があります。

これらの例で使用されているソースコードは、GitHubからダウンロードできます。 気軽にダウンロードして自分のものにしてください。

私が最も刺激を受けた4人の出版開発者にクレジットを与えたかったのです。

  • ベン・ネーデル
  • トッドモットー
  • パスカル・プレヒト
  • サンディープパンダ

また、FreeNode #angularjsおよび#javascriptチャネルのすべての素晴らしい人々に、多くの優れた会話と継続的なサポートに感謝したいと思います。

そして最後に、常に覚えておいてください:

 // when in doubt, comment it out! :)