Angular js はまりどころ
-
Upload
ayumi-goto -
Category
Technology
-
view
8.241 -
download
2
description
Transcript of Angular js はまりどころ
AngularJSはまりどころ
2014/9/18
おめでとうございます。
自己紹介
後藤歩 (@balmychan)
株式会社オロ
CakePHP, Rails, iOS, AndroidなどのWeb系、アプリ系の受託開発
家族向け写真共有サービス nicori の立ち上げ、開発
クラウド管理会計 ZAC Enterprise の開発
開発対象概要
開発対象Zac Enterprise(管理会計システム)
開発体制日本 3~4名海外(ベトナム) 1名※将来的には、 20~30名のエンジニアが開発する想定
技術構成AngularJS + TypeScript + .NET
AngularJSの構成
モジュールルーティング( ui.router)は使用せず、多言語対応のための angular-translateと、 directiveの共通コンポーネント化、 serviceのビジネスロジックの共通化、実装方式の共通化を図るために導入
ディレクティブ約 40個(テンプレート代わりや、新しいインターフェース)
サービス約 31個(サーバーサイドからのデータ取得や共通処理をまとめる)
フィルター約 10個
導入のきっかけ
コード量を減らしたい → data binding
UIを統一したい → directive
クライアント側の開発を、多人数開発で効率よく行いたい → フルスタック
検討を開始したのは、 2013年 10月ごろ
導入前の大きな懸念
パフォーマンスは問題ないか?
業務システムでは、大量のデータを同時に表示することは多い
一部画面では大量のリスト&計算処理など、クライアント側で処理する重い処理もある
※それまでは、 jQuery or pure JS
いくつか、はまった事例を紹介します
はまりの分類
はまるポイントは、大きく分類して下記があります
・スコープ・パフォーマンス・フィルター・バリデーション
・ DI・ディレクティブ・サービス
・セキュリティ・ angular-translate
・ altJS( TypeScript)・ 1.3への移行
パフォーマンス関連ではまったところ
パフォーマンス
これは有名な話ですが、 data-bindingは 2,000を超えると重くなるといわれている
特に、 angular-translateを使っていると、ラベル部分にも data-bindingが使われるので、固定ラベルにも data-bindingが使われ、数が多くなっていく
one-time binding
下記のように、 AngularJS 1.3から使用できる one-time bindingを使うと、余計な data-bindingを初回のみにすることができます。これによって大量のリストも表示が可能になりました。
::を付けるだけ
<span>{{ ::item.title }}</span><span ng-bind="item.title></span>
<span>{{ ::('message' | translate) }}</span>
※angular-translateで one-time bindingを使う場合は、 filterにする
one-time binding
filterの結果や、 ng-class、配列にも指定できるため、とにかくリアルタイムに変更しない項目はすべてこれを使用しています
<span>{{ ::(item.date | date | convert) }}</span>
<select> <option ng-repeat="option in ::options">{{ ::option.title }}</option></select>
<div ng-class="::{ normal: type == 0, warning: type == 1, danger: type == 2 }"></div>
※1.2までは、 angular-onceや bindonceなどのモジュールを使用する
使えるところは使う
過剰な directive
大量の data-bindingにも関連しますが、リストで表示する HTMLに directiveを使い過ぎると、ページング処理に重くなります。また、 directiveの linkで重い処理(大量の DOM処理や複雑な計算)を行うと、ページング時に生成に時間がかかり、カクカクしだします。
可能な限り linkは軽くする、 directiveを使い過ぎない
※IEだとまた重い!!
<hoge-item> <hoge-header> <hoge-title>hoge</hoge-title> <hoge-button>Button1</hoge-button> <hoge-button>Button2</hoge-button> </hoge-header></hoge-item>
スコープ関連ではまったところ
スコープ
プリミティブな型の data bindingscopeが true(継承)の directive内で inputにプリミティブな型を ng-modelで bindingすると、プロトタイプ継承の関係で想定とは違う動作をすることがある。
<body ng-controller="myController"> <div>{{ title | json }}</div> <hoge-item> <input type="text" ng-model="title"> </hoge-item></body>
<body ng-controller="myController"> <div>{{ obj.title | json }}</div> <hoge-item> <!-- このディレクティブは scope=true --> <input type="text" ng-model="obj.title"> </hoge-item></body>
このパターンは titleが表示されない(myControllerで参照できない)
これは動作する
title = "hoge" ← Set
obj.title = "hoge" ← Set
objが無い!ので上位スコープを探索
このスコープからは参照できない
ディレクティブ関連ではまったところ
ng-transcludeの scopeは falseだが、内部で $scope.$new(false)が呼ばれているng-transcludeで使用する HTMLに、プリミティブな値を使うと思わぬところで動作しない※先ほどの scopeの問題と同様
このパターンは、 titleが表示されない
app.directive('hogeArea', function() { return { restrict: "EA", template: "<div ng-transclude></div>", transclude: true };});
<hoge-area> <input type="text" name="title" ng-model="title"></hoge-area><div>{{ title }}</div>
ディレクティブ
このように、 scopeを指定して transcludeすれば OK
app.directive('hogeItem', function() { return { restrict: "EA", template: "<div ng-transclude></div>", transclude: true, link: function(scope, element, attrs, ctrl, transclude) { if (transclude) { transclude(scope, function(clone) { element.find('div').append(clone); } } } };});
下記の issueで議論されていて、 <div ng-transclude="parent, sibling, child"></div>で指定できるようになるかも。https://github.com/angular/angular.js/issues/8609
input系のカスタムディレクティブを作るとき独自に入力系ディレクティブを作る際、 $viewValue, $modelValue, $renderをしっかり理解しないとはまる
たとえば、独自の selectタグのようなディレクティブを作りたい。まずは勢いとノリで実装してみるlink: function(scope, element, attrs, ngModelCtrl) { // この inputは初期値を設定したいが、 ng-repeatで elementが未生成だから遅延させよう $timeout(function() { ngModelCtrl.$setViewValue('initializeValue'); select(newValue); }); // ngModelが変わったら、選択肢を変更しよう scope.$watch("ngModel", function(newValue) { select(newValue); });}
}無限に選択肢が変わる現象が発生・・・
ディレクティブ
ngModelCtrl.$viewValueは画面に表示するためのフォーマット後の値( ngModelの値が、 $formattersを全て通った後)ngModelCtrl.$modelValueは内部で保持する値(画面入力値が、 $parsersを全て通った後)ngModelCtrl.$renderは、 ng-model="hoge"に指定された値が変更された時に呼ばれる、 DOM操作関連の処理を入れる
link: function(scope, element, attrs, ngModelCtrl) { ngModelCtrl.$render = function() { var value = ngModelCtrl.$modelValue || ngModelCtrl.$viewValue; // 値無い場合(初期値が無いのも含む)の処理 if (angular.isUndefined(value)) { ngModelCtrl.$setViewValue("initialize Value"); ngModelCtrl.$render(); return; }
// ここで、 valueを元に、 DOMを操作する処理を入れる element.find("div").addClass("hoge"); };}
良くあるフォームのリセットの処理をどうするか
link: function(scope, element, attrs, ngModelCtrl) { var beforePristing; scope.$watch(function() { return beforePristine = ngModelCtrl.$pristine; }, function() { if (beforePristine == false && ngModelCtrl.$pristine) { // リセット処理 } };}
ディレクティブ
こんなこともしてましたがいまいち・・・。みなさんどうしてるんでしょうか、教えてください
DI関連ではまったところ
Circular Dependency errorが発生
DI
app.directive('hogeDirective', ['serviceA', 'serviceB', function(serviceA, serviceB) { return { link: function() { serviceA.method(); serviceB.method(); } }}]);
app.service('serviceA', ['serviceB', function(serviceB) { this.method = function() { alert("serviceA method!"); };}]);
app.service('serviceB', ['serviceA', function(serviceA) { this.method = function() { serviceA.method(); };});
あるディレクティブでサービス Aを使い、そこでサービス Bを使ってたらそいつがサービス Aを使ってた
Circular Dependency errorが発生するhttps://docs.angularjs.org/error/$injector/cdep?p0=serviceA%20%3C-%20serviceB%20%3C-%20serviceA%20%3C-%20hogeInputDirective
DI
app.directive('hogeDirective', ['serviceA', 'serviceB', function(serviceA, serviceB) { link: function() { serviceA.method(); serviceB.method(); }}]);
app.service('serviceA', ['serviceB', function(serviceB) { this.method = function() { alert("serviceA method!") };}]);
app.service('serviceB', ['$injector', function($injector) { this.method = function() { $injector.invoke(['serviceA', function(serviceA) { serviceA.method(); }]); };}]);
下記で解決
$injectorサービスを使用する
angular-translate関連ではまったところ
angular-translate
app.controller("myController", ['$translate', function($translate) { $translate("APP_TITLE").then(function(value) { // タイトルを加工 scope.title = value; });});
serviceとしても使用可能だが、 promiseが返ってくるので thenが入って見づらい
多言語対応用のモジュール。下記の形にしておくと、各言語用の辞書データから読みだして、表示される(動的に言語変更も簡単)
<div translate="APP_TITLE"></div>
instantを使うと値が返ってくる(読み出しが完了していることが前提)
app.controller("myController", ['$translate', function($translate) { scope.title = $translate.instan("APP_TITLE");});
1.3への移行ではまったところ
AngularJS 1.3
isolated scope同士の directiveを同時に使用するとエラー
<custom-button translate="MESSAGE"></custom-button>
<custom-button>{{ ::('MESSAGE' | translate) }}</custom-button>
下記は、 custom-buttonと translateで isolated scopeを使っているエラーになる
今回は translateが filterでも使用できるので、そちらで回避
AngularJS 1.3
validationの実装方法が変更
ngModelCtrl.$parsers.unshift(function(viewValue) { var valid = (value == "A") ? true : false; ngModelCtrl.$setValidity("validA", valid); return valid ? value : undefined;});
ngModelCtrl.$validators['validA'] = function(modelValue, viewValue) { var value = modelValue || viewValue; return value == "A";}
以前は下記のような形で validationを実装していた( "A"だけ許可する簡易なバリデーション)
1.3からは下記を使用する
この実装の場合、例えば validBを同時に使用していた場合、 validAを評価した時点でその次の parserへの値はundefinedになってしまうため、 validBのバリデーションがうまく動作しなかった
はまったときは
・「 AngularJSリファレンス」を読む・検索
・ angular.jsのソースを読む・ githubで issueも見る・英語に強くなる
ご清聴ありがとうございました。
今度別の機会で、 TypeScript関連について話します!