Created
March 25, 2020 16:44
-
-
Save okunokentaro/a5d083ed6a5a39413eea49102fab4a88 to your computer and use it in GitHub Desktop.
AngularJSモダンプラクティス
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2015/05/22 にQiitaに投稿した記事のアーカイブです | |
--- | |
こんにちは、@armorik83です。私のAngularJS歴は2年弱で、これまでAngularJSに関する記事はQiitaにたくさん書いてきました。例えば次のような記事です。 | |
- [AngularJSアンチパターン集](http://qiita.com/armorik83/items/b00818ecaf2e93734b36) 2014.9 | |
- [ここらでDirective Scopeの@=&をまとめておきたいと思う](http://qiita.com/armorik83/items/72f12cb3a6f040fb8364) 2014.9 | |
- [TypeScriptで書くAngularJSのMVC](http://qiita.com/armorik83/items/0778d757c46e953f3fdf) 2014.2 | |
- [AngularJS Directiveの処理順を網羅してみた](http://qiita.com/armorik83/items/38fe685cc76163c7e8ce) 2014.12 | |
他にもニッチなものやイマイチだったものも含めてけっこうな数となってきました。また、こういった記事の縁で勉強会でも登壇させて頂きました。 | |
- [モダンAngularJS](https://speakerdeck.com/armorik83/modanangularjs-at-gdgzhong-guo-2014-dot-12-dot-6) 2014.12 GDG中国 | |
- [TypeScriptで書くAngularJS](https://speakerdeck.com/armorik83/typescriptdeshu-kuangularjs-at-gdgshen-hu-2014-dot-8-23) 2014.8 GDG神戸 | |
- [イベント駆動AngularJS](https://speakerdeck.com/armorik83/jin-karashu-kuangular-2-dot-0) 2015.4 GDG神戸 | |
たくさん書いてくると、古い記事は粗が目立ち、そして世界の技術からみても鮮度が落ちていくため、どうしても有用性が失われていきます。そこで今回改めてこれまでのけじめとして、そして控えているAngular 2を捉えながら2015年にAngularJS 1.xをどう書いていくべきか、まとめていきます。 | |
**おことわり**: 客観的に見てベストプラクティスと呼ぶには個人的な好みが含まれすぎているため、モダンプラクティスと称しています。以下の書き方や使い方は、AngularJSが公式に強制または推奨しているものではなく、個人的にAngular 2を考慮して提案しているものです。ご了承ください。 | |
【追記2015/6/30】Angular 2も刻々と成長しており、全容が明らかになっていくにつれ本稿と相容れない解釈も生まれてきます。本稿は筆者による初版公開時点での考えをまとめたものであり、いかなる時も全ての局面で有用だとは限らないことをご了承願います。 | |
【追記2016/2/8】1,000ストックありがとうございます!このたびAngularJS 1.5がリリースされました。モダンプラクティスを守っていたら1.5への移行はすぐです。今後リリースされる2.0も見越して、AngularJS 1系も最新を保てるよう心がけましょう。@laco0416氏の[その使い方はもう古いかも?AngularJS老化チェック(ディレクティブ篇)](http://qiita.com/laco0416/items/edfa917583af4593ad6c)で、あなたのプロジェクトでの書き方がどれくらい古いか診断できるようです。 | |
**執筆時点**: 本稿は2015年5月に執筆しています。 | |
- AngularJS: `1.3.15`、1.4に言及する時は`1.4.0-rc.2` | |
- Angular 2: `2.0.0-alpha.24` | |
- Babel: 最新版 | |
- TypeScript: `1.5.0-beta` | |
# §0 イントロダクション | |
## 現状の何に問題があるのか | |
ヒトコトで言うと、[Angular 2](https://angular.io/)のリリースが控えている、これが障壁とされます。AngularJSは素敵なライブラリです。ただ出るのが少し早すぎたか、開発陣がこのユーザ規模を想定していなかったか、多くの粗を探され多くの後発と比較されました。Angular 2はここに挑戦しており改革に大きな痛みを伴っています。 | |
「AngularJSはAngular 2と互換性がほとんど無いんだろ?」 | |
「今から始めるならReactを…」 | |
「jQueryの方が知ってるし簡単にできる」 | |
有り得る意見です。でもちょっとまって、AngularJSに対して誤解がありませんか。 | |
## 対象読者 | |
この記事は、既にAngularJSをたくさん書いている方々、ちょっと始めてみたけど今のうちに止めた方がいい?と不安になってる方々など、幅広く現在のAngularJSユーザに向けて書いています。ただ、申し訳ありませんが全く使ったことのない方に対する第一歩の手引きとはなっておりません、ご了承ください。 | |
以下の章からは、AngularJSをどう書いていくべきかを具体的に述べます。幅広い力量に向けて対象にしているため、どうしても長めになっております。序文で自分は知っている話題だと感じたら次々と読み飛ばしてもらって構いません。 | |
## オススメ度 | |
各節には、習熟度や利便性などから筆者が独断で表現したオススメ度を表示していますので、ご参考にしてください。 | |
- ★★★: AngularJS習熟度に関わらずお勧めしたい内容 | |
- ★★☆: AngularJSに対して多少の経験があり、困難とも遭遇している方向け | |
- ★☆☆: AngularJSをより使いこなし、最新のJavaScriptの潮流も勘案しておきたい方向け | |
- ☆☆☆: 筆者による挑戦的で実験的な内容、お勧めはしませんが参考までに | |
# §1 Class構文 | |
## ES6が近づいている | |
ES6をご存知でしょうか。ES6のESとは[ECMAScript](http://ja.wikipedia.org/wiki/ECMAScript)の略称で、これは我々がJavaScriptと呼んでいる言語の正式な標準規格のことです。JavaScriptといっても実際にはブラウザ毎に差異があるため、標準化が必要となりました。 | |
ES6は簡単に言うとJavaScriptの次世代規格ということです。 | |
現在の最新ブラウザは全てESのバージョン5をサポートしていますが、[ES6の仕様を全て満たすブラウザはまだ現れていません](https://kangax.github.io/compat-table/es6/)。 | |
- [HELLO, ES6 〜これから迎えるJSのミライ〜](http://yoshiko-pg.github.io/slides/20150425-jsfes/) | |
ES6は、ES5と比べて簡便に記述するための新構文が多く含まれているほか、新しいAPIも複数追加されています。Angular 2はTypeScriptでの使用を前提に開発されており(従わずにES5やCoffeeで書くこともできます)ES6についての知識もぜひ知っておくべきです。 | |
オススメ度: ★★★ | |
### TypeScriptとES6の関係 | |
[TypeScript](http://www.typescriptlang.org/)とはプログラミング言語のひとつで、バージョンは執筆時点で1.5.0-beta[^1]です。TypeScriptはJavaScriptのスーパーセット言語として策定されており、早くから「ES6の構文で書いてブラウザで動作するJavaScriptを生成する」というアプローチを取っていました。Angular 2はこの[TypeScriptで開発され](http://blogs.msdn.com/b/typescript/archive/2015/03/05/angular-2-0-built-on-typescript.aspx)、ユーザがAngular 2を用いる際にもTypeScriptで書けるよう設計されています。 | |
TypeScriptはES6の構文を積極的に採用しているため、ES6の知識とTypeScriptの知識は重複する部分が多いです。そしてもちろん最大の特徴はコンパイラによる静的型検証です。 | |
オススメ度: ★★★ | |
### Babel | |
TypeScriptを使う方法以外に、ES6構文を含むJavaScriptをES5で動くように変換するトランスパイラというものがあります。そのうちのひとつの[Babel](https://babeljs.io/)は開発がとても盛んで、[公式サイト内で気軽に試すこともできる](https://babeljs.io/repl/#?experimental=false&evaluate=false&loose=false&spec=false&playground=true&code=class%20Hello%20%7B%0A%20%20constructor()%20%7B%0A%20%20%20%20this.message%20%3D%20'Hello%20ES6!'%3B%0A%20%20%7D%0A%7D)筆者オススメのトランスパイラです。実際の開発では[gulp](http://gulpjs.com/)などのタスクランナーでBabelの変換処理を自動化して利用します。 | |
このほかに、[traceur-compiler](https://github.com/google/traceur-compiler)というものもあります。 | |
オススメ度: ★★☆ | |
## Class構文の例 | |
### AngularJS + Class構文 | |
次の例は、実際に[業務でAngularJSを用いて書いたコード](https://github.com/likr/interactive-sem)を見本用に整形したものです。Class構文はES6からの大きな特徴で、不慣れな方には別言語に見えるかもしれません。 | |
```js:controller.js | |
export class Controller { | |
constructor($rootScope, $scope, $timeout) { | |
this.subscribe(); | |
} | |
subscribe() { | |
this.disposer = this.disposer || {}; | |
this.disposer.store = Store .addListener(this.storeChangeHandler.bind(this)); | |
this.disposer.renderer = Renderer.addListener(this.rendererChangeHandler.bind(this)); | |
} | |
// snip | |
} | |
``` | |
### Angular 2でのClass構文の使い方 | |
次の例はAngular 2チュートリアルの断片です。公式のチュートリアルが最初からClass構文の使用を想定して紹介しているのです。 | |
```js:angular2-example.js | |
// snip | |
class MyAppComponent { | |
constructor() { | |
this.name = 'Alice'; | |
} | |
} | |
bootstrap(MyAppComponent); | |
``` | |
`@`を使った[Decorators構文](http://qiita.com/armorik83/items/e3a0ce67f569ddc4b432)を省略していることに気付いた読者もおられるでしょうが、今回は触れません。[Angular 2 Annotations](https://angular.io/docs/js/latest/api/annotations/)はalpha版で絶賛開発中なため、また日を改めて紹介します。 | |
## AngularJSでClass構文を導入する | |
### 一番ダメな例 | |
```js | |
var mainCtrl = function($scope) { | |
$scope.user = 'John'; | |
}; | |
``` | |
```html | |
<div ng-controller="mainCtrl"> | |
<p>Hello, {{user}}!</p> | |
</div> | |
``` | |
変数`mainCtrl`に対して無名関数を与えてControllerを定義する例です。最もやってはいけない書き方。AngularJS 1.3以降はそもそも禁止されたので、さすがにこのコードで運用することはありません。しかしこう書くチュートリアルを見てAngularJSを始められる方が今日も残っている以上、ここに記しておきます。当時このようなチュートリアルを許してしまった開発陣はやや残念です。 | |
### $scopeは使わない | |
前項を改善したのが次の例です。よく見る感じになりました。 | |
```js | |
angular.module('myApp', []); | |
angular.module('myApp').controller('mainCtrl', function($scope) { | |
$scope.user = 'John'; | |
}); | |
``` | |
```html | |
<div ng-controller="mainCtrl"> | |
<p>Hello, {{user}}!</p> | |
</div> | |
``` | |
まだ改善箇所があります。minify対策として`['$scope', function($scope){...}]`にしたほうがいい…それも正しいのですが、ここでは『`$scope`自体を使わないこと』がより良い実践でしょう。 | |
理由は2つあります。ひとつはAngularJS 1.xでClass構文を取り入れていくならば、`$scope`を使わないほうが身軽だから。もうひとつは、Angular 2では`$scope`が廃止されるからです。AngularJS 1.3以降を使うならば`$scope`を使わない選択は十分にメジャー、Angular 2も視野に入れるとそろそろ不安材料ですよ。 | |
ご心配なく、`$scope.$watch`や`$scope.$broadcast`については§5で扱います。 | |
### functionを独立させる | |
前項から`$scope`を外したものが次の例です。 | |
```js | |
angular.module('myApp', []); | |
angular.module('myApp').controller('mainCtrl', function() { | |
this.user = 'John'; | |
}); | |
``` | |
```html | |
<div ng-controller="mainCtrl as main"> | |
<p>Hello, {{main.user}}!</p> | |
</div> | |
``` | |
`$scope`は`this`になりました。JavaScriptの`this`はややこしい…そんなことはない、この先にはClass構文が待っています! | |
HTML側では、`ng-controller`は`"mainCtrl as main"`となりました。`as`は`コントローラ名 as コントローラを格納する変数名`というAngularJSの独自構文です。`controllerAs`というAPIも今後出てくるので、あわせて覚えておきましょう。 | |
ここから次の改善として、Class構文を導入しやすくするため`function(){}`を外に出します。 | |
### Class構文を導入する準備が整った | |
`function mainCtrl()`として外に切り出しました。 | |
```js | |
function mainCtrl() { | |
this.user = 'John'; | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').controller('mainCtrl', mainCtrl); | |
``` | |
```html | |
<div ng-controller="mainCtrl as main"> | |
<p>Hello, {{main.user}}!</p> | |
</div> | |
``` | |
`angular`への登録では`.controller('mainCtrl', mainCtrl)`と指定します。 | |
### Class化 | |
あとは`class`構文を用いて一気にいきましょう。Class名は慣習として大文字で始まるので、Controller名を小文字から始めているならば、この際直してもよいでしょう。`Ctrl`は最近だと`Controller`と書く例をよく見かけます。Angular 2の例だと`FooComponent`や`FooCmp`と安定していません。(追記: 先日Angular 2開発者から直々に貰ったサンプルでは`Component`となっていました) | |
非Controller・非Componentと混同したくないため、個人的には`Main`とせず`MainController`とします。ここは好みですので、チーム内規約などに沿って進めてください。 | |
```js | |
class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').controller('MainController', MainController); | |
``` | |
```html | |
<div ng-controller="MainController as main"> | |
<p>Hello, {{main.user}}!</p> | |
</div> | |
``` | |
[実際に動作するサンプル](http://jsfiddle.net/8gvc02ju/)です。 | |
`as`以下は変数名なので、そのままにしています。DIアノテーション(minify対策)は、次の節にて説明します。 | |
## いくつかのメソッドがある場合 | |
複数のメソッドがあった場合にどうClass構文を導入していくか、補足しておきます。 | |
### $scopeにいくつかのメソッドがある例 | |
```js:todoCtrl.js | |
function TodoCtrl($scope, $routeParams, $filter, store) { | |
$scope.newTodo = ''; | |
$scope.editedTodo = null; | |
$scope.addTodo = function () {}; | |
$scope.editTodo = function (todo) {}; | |
$scope.saveEdits = function (todo, event) {}; | |
} | |
``` | |
[TodoMVC](https://github.com/tastejs/todomvc/blob/gh-pages/examples/angularjs/js/controllers/todoCtrl.js)を例に取り上げます。`$scope`に`function`がたくさん生えたよくある構造です。これをES6 Class化していきましょう。 | |
### メソッドとプロパティで分ける | |
まず`$scope`のプロパティに格納されるものが関数型かそれ以外かで分けます。 | |
```js:todoCtrl.js | |
function TodoCtrl($scope, $routeParams, $filter, store) { | |
$scope.newTodo = ''; | |
$scope.editedTodo = null; | |
// 以下は関数型、Classのメソッドとなる | |
$scope.addTodo = function () {}; | |
$scope.editTodo = function (todo) {}; | |
$scope.saveEdits = function (todo, event) {}; | |
} | |
``` | |
### Classを用意する | |
次に`class`を用意します。 | |
```js:todoCtrl.js | |
class TodoController { | |
constructor($scope, $routeParams, $filter, store) { // <- ここに同じものを | |
// それぞれ保持 | |
this.$scope = $scope; | |
this.$routeParams = $routeParams; | |
this.$filter = $filter; | |
this.store = store; | |
} | |
} | |
function TodoCtrl($scope, $routeParams, $filter, store) { // <- 使うService名はconstructorへ | |
$scope.newTodo = ''; | |
$scope.editedTodo = null; | |
$scope.addTodo = function () {}; | |
$scope.editTodo = function (todo) {}; | |
$scope.saveEdits = function (todo, event) {}; | |
} | |
``` | |
`constructor`の引数はDIするService名です。TypeScriptの場合だとconstructorの引数名の前に`public`など[^2]と書くことでthisへの格納を省略できます。ES6ならば律儀に書く必要があります。 | |
### プロパティをconstructorへ | |
```js:todoCtrl.js | |
class TodoController { | |
constructor($scope, $routeParams, $filter, store) { | |
this.$scope = $scope; | |
this.$routeParams = $routeParams; | |
this.$filter = $filter; | |
this.store = store; | |
this.newTodo = ''; // + | |
this.editedTodo = null; // + | |
} | |
} | |
function TodoCtrl($scope, $routeParams, $filter, store) { | |
$scope.addTodo = function () {}; | |
$scope.editTodo = function (todo) {}; | |
$scope.saveEdits = function (todo, event) {}; | |
} | |
``` | |
関数以外の`$scope`プロパティは全て`constructor`に移します。`$scope`は`this`に書き換えます。 | |
TypeScriptだと次のように書くことができます。型情報は省略していますので、本来は型も記述します。 | |
```ts:todoCtrl.ts | |
class TodoController { | |
newTodo = ''; // この辺がちょっと違う | |
editedTodo = null; | |
constructor( | |
public $scope, | |
public $routeParams, // publicと付けるとthis代入を省略できる | |
public $filter, | |
public store | |
) { | |
// noop | |
} | |
} | |
function TodoCtrl($scope, $routeParams, $filter, store) { | |
$scope.addTodo = function () {}; | |
$scope.editTodo = function (todo) {}; | |
$scope.saveEdits = function (todo, event) {}; | |
} | |
``` | |
### $scope関数をメソッドへ | |
最後に`$scope`の各関数をClassのメソッドとして書き換えます。この時、出てくる`$scope`を`this`に置き換えることを忘れずに。 | |
```js:todoCtrl.js | |
class TodoController { | |
constructor($scope, $routeParams, $filter, store) { | |
this.$scope = $scope; | |
this.$routeParams = $routeParams; | |
this.$filter = $filter; | |
this.store = store; | |
this.newTodo = ''; | |
this.editedTodo = null; | |
} | |
addTodo() { | |
// | |
} | |
editTodo(todo) { | |
// | |
} | |
saveEdits(todo, event) { | |
// | |
} | |
} | |
``` | |
ひとつだけ例として`addTodo()`の中身を書いてみます。[元ソースはこちら](https://github.com/tastejs/todomvc/blob/gh-pages/examples/angularjs/js/controllers/todoCtrl.js#L32-L50)。 | |
```js:todoCtrl.js | |
addTodo() { | |
const newTodo = { | |
title: this.newTodo.trim(), | |
completed: false | |
}; | |
if (!newTodo.title) { | |
return; | |
} | |
this.saving = true; | |
store | |
.insert(newTodo) | |
.then(() => { | |
this.newTodo = ''; | |
}) | |
.finally(() => { | |
this.saving = false; | |
}); | |
}; | |
``` | |
要注意ポイントとして、`$scope`を`this`に置き換えるならば、`this`の束縛を揃えるために[アロー関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions)を使う必要があります。ES6の新構文であるアロー関数は単なる[糖衣構文](https://ja.wikipedia.org/wiki/%E7%B3%96%E8%A1%A3%E6%A7%8B%E6%96%87)ではなく`this`の束縛を上のブロックと揃える大事な性質があります。ES6でもTypeScriptでも使え、なにより短くて済むので積極的に採用したい構文です。 | |
変数宣言`var`は、筆者は[`let`#](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let)や[`const`#](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const)に積極的に置き換えています。この2つもES6から導入された新たな仕様です。 | |
### DIアノテーションを記述 | |
よく言及される話題で、ユーザ間では**minify対策**とも呼ばれます。AngularJSでは各Service, Controllerで使う別のServiceを列挙する必要があり、これを欠かすとminify変換時に正常に動作しなくなる問題が存在します。 | |
DIアノテーションを自動で付与してくれるツール[ng-annotate](https://github.com/olov/ng-annotate)を使ってもよいです。ただしAngular 2ではDIの仕組みが変わり機械的な記述がしにくい構造となったため、AngularJS 1.xでも手書きでよいと考えています。Angular 2に関しては[`@Inject`アノテーション](https://angular.io/docs/js/latest/api/di_annotations/Inject-class.html)などを参照してください。 | |
AngularJSにおけるDIアノテーションは、`自身のClass名.$inject`で記述できます。 | |
```js:todoCtrl.js | |
class TodoController { | |
constructor($scope, $routeParams, $filter, store) { | |
this.$scope = $scope; | |
this.$routeParams = $routeParams; | |
this.$filter = $filter; | |
this.store = store; | |
this.newTodo = ''; | |
this.editedTodo = null; | |
TodoController.$inject = ['$scope', '$routeParams', '$filter', 'store']; | |
} | |
addTodo() { | |
// | |
} | |
} | |
``` | |
これで`$scope`に`function`がたくさん生えたControllerをClass化できました。 | |
# §2 Directive指向 | |
本章では、前章でClass構文を導入したControllerからDirectiveに切り替えていく方法について解説します。 | |
Directiveというと「AngularJSの中でも複雑で難しいやつ」とか「入れ子にすると混乱する」などの意見をよく目にしますが、恐れることはありません。これから挙げるAPI以外は**無視しても大丈夫です**。 | |
そして重要なのは、Angular 2はComponent指向で、Angular 2 Component ≒ AngularJS 1 Directiveだということです。`ng-controller`はAngular 2では**廃止されます**。 | |
## 使うAPI | |
ここでいうAPIとはDDO - [Directive Definition Object](https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object)のプロパティのことを指します。使わないAPIの方が多いです。 | |
- `restrict`: 基本的に`E`のみ、稀に`A`、あとは不要 | |
- `templateUrl`: HTMLが1行程度と短ければ`template`プロパティを使ってもよい | |
- `scope`: **Isolate scope**を生成させるため常に`{...}`で記述する | |
- `controller`: `controllerAs`も併記すること | |
- `bindToController`: 1.4.0以降ならば`scope`より扱いの楽なこちらが便利 | |
あとは覚える必要がないです。ここで、AngularJSのDirectiveをフル活用している方からは疑問が上がるかもしれません。 | |
オススメ度: ★★★ | |
### compile, linkはどうするの? | |
**Q.** `compile`, `link`を多用していますが不要ですか? | |
**A.** Angular 2ではこのフック方法が変更されるので、依存しすぎないよう注意してください。`compile`, `prelink`, `postlink`の移行方法は現時点(2015/5)では明示されていませんが、ソースを読む限りAngularJSと同等のAPIは提供されないことが分かっています。Angular 2はDOMの直接書き換えを推奨していないので、こうなっているのかもしれません。 | |
もし`compile`内でjQueryを多用している場合、慎重に置き換える必要があります。jQueryとの付き合い方は§4にて後述します。 | |
オススメ度: ★★☆ | |
### requireはどうするの? | |
**Q.** `require`で親Directive Controllerに依存しています。`link`が廃止されると困りますが、どうすればいいですか? | |
**A.** Angular 2では[`@Parent`#](https://angular.io/docs/js/latest/api/annotations/Parent-class.html)や[`@Ancestor`#](https://angular.io/docs/js/latest/api/annotations/Ancestor-class.html)といった新しいアノテーションが採用されます。このAPIを活用することを念頭において、`link`内ではあまり複雑なことをせず、さっさとControllerに渡すのが賢明です。 | |
筆者の例を掲載します。(TypeScriptで実装していたので、例もTypeScriptです) | |
> https://github.com/likr/interactive-sem/blob/master/app/src/views/dialogs/add-latent-variable.ts#L83-L89 | |
```js | |
static link($scope: Scope, _: any, __: any, controllers: any) { | |
// controllers引数にはrequire配列と同じ順序で格納されている | |
var cwModal = controllers[0]; | |
var self = controllers[1]; | |
$scope.dialog = cwModal.dialog; | |
self.init(); | |
} | |
``` | |
このときはダイアログのプラグインを使っていたので、そのインスタンスを取得する必要がありました。 | |
```js | |
{ | |
controller: Controller, | |
controllerAs: 'Controller', | |
link: link, | |
require: ['^cwModal', directiveName], // <- requireを配列にする | |
restrict: 'E', | |
scope: { | |
locale: '&isemIoLocale' | |
}, | |
templateUrl: app.viewsDir.dialogs + 'add-latent-variable.html' | |
} | |
``` | |
このように`require`を`Array<string>`型の配列にすることで、複数のControllerを指定できます。この時自身のDirective名(ここでは`directiveName`変数に格納済)を与えると自身のControllerが取得できるのです。 | |
Directive Controllerの生成タイミングは`link`実行**前**なので、別途`link`から`init()`を呼んで初期化処理のタイミングを`link`実行後にさせています。`link`関数内とControllerを繋ぐ手段が`$scope` (Isolate scope)なので、`$scope.dialog = cwModal.dialog;`として渡します。 | |
オススメ度: ★☆☆ | |
### restrictはEのみ? | |
**Q.** restrictは`A, C, E, M`の4種とそれらの組み合わせですが、`E`以外は使いませんか? | |
**A.** はい、基本的には`E`しか使わないと覚えてください。`restrict` APIは不必要に混乱を招いたと考えています。 | |
そもそも各所で喧伝されている4種のうち`M`はドキュメントから省かれています(APIとしては現存)。そして`C`の用途ですが、これはIE8以前の非HTML5に対応するためのもので、現代では不必要となっています。 | |
残る`A, E`については、基本的には**タグ名をDirectiveとする**`E`を使えば間違いなく、より応用的にDirectiveを利用する場合のみ属性名をDirectiveとする`A`を考慮してもよいでしょう。`AE`という同時指定はプロジェクト内検索を困難にしチーム内にも混乱をもたらすため、使うべきではありません。 | |
Angular 2では`restrict`は廃止され[`@Component`#](https://angular.io/docs/js/latest/api/annotations/Component-class.html)と[`@Directive`#](https://angular.io/docs/js/latest/api/annotations/Directive-class.html)に分かれ`selector`プロパティで指定するため、明確となっています。 | |
- `E`: タグ名 -> Angular 2では`@Component` | |
- `A`: 属性名 -> Angular 2では`@Directive` | |
- `AE`: 使わない! | |
- `C`: 使わない! | |
オススメ度: ★★★ | |
### replaceは使ってもいいですか? | |
**Q.** Directive自身を`template`と置換してレンダリングする`replace`は使ってもいいですか? | |
**A.** [DEPRECATED](https://github.com/angular/angular.js/blob/bab474aa8b146f6732857c3af1a8b3b010fda8b0/src/ng/compile.js#L300)です、使うべきではありません。 | |
`svg`内では独自タグ名が使えないため`replace`を採用されることがあるかもしれません。廃止は次期メジャーバージョンとされていますが、今のうちからDEPRECATEDだと意識しておくとよいでしょう。`svg`内で自作のDirectiveを適用したい際には、`restrict: A`に設定した上で工夫してください。 | |
オススメ度: ★☆☆ | |
### Isolate scopeとは? | |
**Q.** Isolate scopeとは何ですか? `scope`の指定は`@=&`などの指示子があってややこしいです。 | |
**A.** Isolate scopeとは親子でScopeを共有しない**Directiveの独立したScope**のことです。AngularJS公式の提供が複雑でいまいち理解されていませんが、Isolate scope以外を使うべきではありません。 | |
`scope`プロパティには、自身に無ければ親を参照しにいくという複雑極まりない仕様があり、これはグローバル変数が起こす副作用と混乱に等しい問題でした。筆者は「親子共有問題」と呼んでいます。これは`controllerAs`が普及しプロパティ元が明記されるにつれ緩和しましたが、根本的解決にはなりません。 | |
この問題を解決するにはIsolate scope(分離スコープ)を導入します。これは`scope: {}`のようにObjectリテラルを与えて使います。この時、値の継承方法として`@=&`という3種の指示子が提供されており、これについては[以前まとめました](http://qiita.com/armorik83/items/72f12cb3a6f040fb8364)。結論として筆者は`&`以外不要と考えています。 | |
「`&`はfunction として結び付ける」という解説があり、これではあまりピンときません。そこで「`&`はReadonlyだ」と置き換えれば解釈しやすいでしょう。`&`の対比としてバインド可能な`=`がありますが、これは隠蔽せず共有してしまうので、なるべく使わない方針です。React経験者ならば`props`に近い一方向のフローと言えばイメージしやすいでしょうか。 | |
プロジェクト内のDirectiveをIsolate scopeにするならば、全て統一して用いるのがベターです。レガシーなソースに段階的に導入するならば仕方ないのですが、スクラッチで実装する場合は一律でIsolate scopeにした方が、混乱も少なく、後々の拡張性や差し替えを容易にします。ひとつ注意としてプロパティが一つもないDirectiveの場合は`scope: {}`と空のObjectを与える必要があります。こうしなかった場合、親子共有されるただのScopeが生成されます。 | |
オススメ度: ★★☆ | |
### Scopeの命名は? | |
**Q.** `scope`はHTML属性名もプロパティ名も同名でかまいませんか? | |
**A.** これは注意してください。よく迷うのが`scope`の命名です。`{prop: '&'}`と書けば`<mydir prop="">`のように同名のHTML属性として扱えます。ただし筆者の経験では、プロジェクト内検索の利便性と視認性の両側面から、多少冗長でもプロジェクト毎の接頭辞を付けることをおすすめします。別途、独自プラグインを作成しそのDirectivereの`strict`が`A`だった場合に、その属性名がどのプロジェクト or プラグインに属すのか、混乱が起きやすいのです。 | |
接頭辞を扱うためには、`{prop: '&myProp'}`といったキャメルケース形式で記述し、使用時は`<mydir my-prop="">`と記述します。AngularJSでは`ng`が用いられていますね。この話題も[以前の記事](http://qiita.com/armorik83/items/72f12cb3a6f040fb8364#1-3)で触れています。 | |
Angular 2ではHTML内にAngular独自の構文が導入されるため、判別がつきやすいようになっています。 | |
オススメ度: ★★☆ | |
### bindToControllerは使ってもいいですか? | |
**Q.** `bindToController`を最近よく見かけますが、使ってもいいですか? | |
**A.** はい、積極的に使っていきましょう。ただ個人的には1.3以前で`scope`を使っているなら`bindToController`に急いで置き換える必要はないと考えています。余裕があったらついでに、くらいで。 | |
`bindToController`は`scope`とよく似たAPIですが、Controllerの`$scope`に格納されず、Controllerの`this`に直接格納されるという仕組みです。筆者はこの新APIをAngular 2移行への段階的な導入と解釈しています。HTML側で`{{controller.$scope.prop}}`と冗長に書く必要があったところを`{{controller.prop}}`と書ける点がメリットです。 | |
AngularJS 1.3.0から導入されていたこのAPIは、初期はあまり話題になったと感じていません。1.4.0からは直接`bindToController`にプロパティ`{}`を記述できるようになったため、利便性が向上しました。このAPIの紹介は本稿では煩雑になるため、別の機会にします。 | |
オススメ度: ★★☆ | |
## ng-controllerベースからDirectiveベースへ | |
APIへの理解が整ったところで、`ng-controller`から脱却する手順について解説します。 | |
```js | |
class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').controller('MainController', MainController); | |
``` | |
```html | |
<div ng-controller="MainController as main"> | |
<p>Hello, {{main.user}}!</p> | |
</div> | |
``` | |
§1の例をベースに進めていきましょう。 | |
### Directive Definition Objectの定義 | |
DDO - Directive Definition Objectを返す関数`mainDDO()`を定義します。 | |
```js | |
class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
function mainDDO() { | |
return { | |
restrict: 'E', | |
controller: MainController, // Classを与える | |
controllerAs: 'main', | |
scope: {} // 使わない場合も空ObjectでIsolate scope化 | |
}; | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').directive('main', mainDDO); | |
``` | |
次に`<main>`タグの中身を定義する`template`を記述しましょう。 | |
### Templateの定義 | |
```js | |
class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
function mainDDO() { | |
return { | |
restrict: 'E', | |
controller: MainController, | |
controllerAs: 'main', | |
scope: {}, | |
template: '<p>Hello, {{main.user}}!</p>' // 追加 | |
}; | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').directive('main', mainDDO); | |
``` | |
```html | |
<div ng-controller="MainController as main"> | |
<!-- この<p>をtemplateにする --> | |
<p>Hello, {{main.user}}!</p> | |
</div> | |
``` | |
Isolate scopeを使うので、`template`または`templateUrl`を**必ず**導入する必要があります。Isolate scope利用時は、そのDirective内にHTMLを記述することはできません。つまり、`<main>...いろいろ...</main>`とならず、常に`<main></main>`のみとなります。これによって`transclude`オプションも使用できなくなりますが、`transclude`は挙動がとにかく取っ付きづらく、筆者は一度も活用できたことがないので、気にすることはないでしょう。自作Directiveはタグ内に何も書かず、属性とIsolate scopeをインタフェースとして使うものと覚えるとシンプルです。 | |
筆者は個人的好みで`templateUrl`を使い、2行以上ならば全てHTMLファイルに記述しています。最近はES6の新構文[Template strings](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/template_strings)の有用性も認められてきたので、望むならば`template`に全てのHTMLを記述するスタイルでも構わないです。 | |
### 作成したDirectiveをHTMLに導入 | |
```js | |
class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
function mainDDO() { | |
return { | |
restrict: 'E', | |
controller: MainController, | |
controllerAs: 'main', | |
scope: {}, | |
template: '<p>Hello, {{main.user}}!</p>' | |
}; | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').directive('main', mainDDO); | |
``` | |
```html | |
<main></main> | |
``` | |
HTML側を`<main></main>`にして完成! お疲れ様でした。`ng-controller`から移行するとき、`ng-controller="as"`が未導入で`$scope`が入り混じってる状態だと`<main>`が持つIsolate Scopeとごちゃ混ぜになり**とてもつらい**ので、先に`as`の導入、次にClassへの移行と、順次済ますのが得策です。 | |
### compileやlinkはどこに記述するか | |
`compile`, `link`はAngular 2への移行を阻害するため縮小方向が望ましいですが、いきなり無くすわけにもいきません。筆者はこのケースを次のようにまとめています。 | |
```js | |
export class MainController { | |
constructor() { | |
this.user = 'John'; | |
} | |
} | |
export class MainDefinition { | |
static postLink(scope, iElement, iAttrs) { | |
// ... | |
} | |
static ddo() { | |
return { | |
restrict: 'E', | |
controller: MainController, | |
controllerAs: 'main', | |
link: MainDefinition.postLink, | |
scope: {} | |
}; | |
} | |
} | |
angular.module('myApp').directive('main', MainDefinition.ddo); | |
``` | |
functionを並べずにexport classで括った理由は、まとまった単位のほうがテストで扱いやすかったためです。 | |
## ところでなんでDirectiveがいいの | |
`ng-controller`はAngular 2で廃止されると述べましたが、なぜ筆者が現段階でDirective化を勧めているか補足しておきます。 | |
Directive化促進の理由はいくつかあります。 | |
**1.** テンプレートを小さく保ちたいという動機が大きいです。筆者は長らくJavaScriptよりHTML + PHPを書いてきた背景があり、HTML内のリファクタリングの困難さを痛感していたため、とにかくHTMLの部品は小さく、汎用性を高く、という願いがありました。HTMLの見通しがよくないことは、肥大したHTMLを扱うほど実感されると思います。Bootstrapなどを扱っていて`<div>`が増えすぎることをDirectiveでセマンティックに隠蔽できる利点も大きい。可読性を引き上げ事故を防げるDirective、これを用いない理由はないというのが持論です。 | |
**2.** 次に、Isolate scopeを積極的に使いたい動機です。前述したようにScopeの親子共有問題というのが厄介で、Child scopeの生成されない要素は、親Scopeではなく兄弟のChild scopeを引き継ぐという[不可解な仕様](http://qiita.com/armorik83/items/38fe685cc76163c7e8ce#scope%E3%81%AF%E3%83%9B%E3%83%B3%E3%83%88%E3%82%84%E3%82%84%E3%81%93%E3%81%97%E3%81%84%E3%81%8B%E3%82%89%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%82%88%E3%81%86%E3%81%AA)に悩まされるくらいなら、もう単純明快なIsolate scopeだけ使っていれば混乱しないのでは。 | |
**3.** そして、やはりAngular 2への移行を段階的にしたい思いがあります。Angular 2はComponent指向で、独自タグと`template`を中心にした実装となっていきます。AngularJSとAngular 2の距離を縮めることを考える時、AngularJSのDirective化が整っている方が大幅な設計見直しをせずAngular 2に向けてステップアップできると睨んでいます。 | |
分割は不便、面倒だと感じる方もおられるでしょうが、モダンなComponent指向のフレームワークならば全てこうなっており、ReactのJSXも同様です。汎用性が上がり、差分管理が容易になることで可読性、保守性も高まる。メリットは大きいです。 | |
オススメ度: ★★☆ | |
# §3 ES6 moduleとAngularJS DI | |
本章ではAngularJSの特徴として挙げられるDIを考察し、Angular 2で採用されるES6 moduleについて触れながら、現段階の付き合い方や今後の実践について紹介します。 | |
## AngularJSのDIとは? | |
DIとはDependency Injectionの略語で、日本語では依存性注入です。かの[Martin Fowler](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%83%BB%E3%83%95%E3%82%A1%E3%82%A6%E3%83%A9%E3%83%BC)が提唱しました。ディペンデンシー・インジェクションというと大層な感じですが、積極的に実践すべき設計で、特殊なものだと思わないでください。以下はJavaの事情などは含めず「AngularJSにおける」DIの話です。 | |
### AngularJS DIの例 | |
AngularJS DIの解説によくある`constructor`内で`new`する例です。 | |
```js | |
class RootController { | |
constructor() { | |
this.store = new Store(); | |
this.renderer = new Renderer(); | |
} | |
} | |
``` | |
このコードの問題は、テストの際も本物の`Store`と`Renderer`を必要とする点です。`Store`がもし通信を必要とするモジュールならば、テスト毎に待たなければなりません。ここでモック・テストを行うためには、`RootController`に向かって`MockStore`を「注入」する必要があります。 | |
### DIを導入すると | |
```js | |
class RootController { | |
constructor(store, renderer) { | |
this.store = store; | |
this.renderer = renderer; | |
} | |
} | |
``` | |
```js | |
var rootController = new RootController( | |
new Store(), | |
new Renderer() | |
); | |
var mockRootController = new RootController( | |
new MockStore(), | |
new MockRenderer() | |
); | |
``` | |
`constructor`に引数を設けるとモックへ差し替える余地が生まれました。DI自体はこれだけのことで簡単に実践できます。AngularJSでは`var rootController = new RootController(...)`に相当する処理を内部でうまいことやってくれるので、そのために必要な定義がDIアノテーション、ということになります。自力で準備、実装することを「オレオレDI」と呼ぶこともあります[^3]。 | |
AngularJSはこのDIを当初から提供したことで疎なテストを行いやすくし、[Karma](http://karma-runner.github.io/0.12/index.html)や[Protractor](http://angular.github.io/protractor/#/)の提供をもってテストの重要性を説きました。 | |
オススメ度: ★★★ | |
## AngularJSのCommonJS採用、そしてAngular 2へ | |
### CommonJS, Browserify | |
[Node.js](https://nodejs.org/)ではCommonJSという、1ファイル1モジュールとしてインタフェースや依存性を記述するモジュール管理を採用しています。一方、ブラウザ上ではHTMLに`<script src=""></script>`をいくつも並べるローディングが長らく主流で、`CommonJS`はサーバサイドのものという印象が強めでした。 | |
それが2014年頃から[Browserify](http://browserify.org/)、[webpack](http://webpack.github.io/)を用いてブラウザ上でも`CommonJS`スタイルで記述し、HTMLには生成物である`<script src="bundle.js"></script>`を1行記述するだけというスタイルも現れ、少しずつ浸透しています。複数回の`<script>`ローディングによる通信の遅れを減らせ、[npm](https://www.npmjs.com/), [Bower](http://bower.io/)とパッケージ管理が分散せずに簡潔に済むことが利点だと筆者は認識しています。@teppeis氏の[こちらの記事](http://teppeis.hatenablog.com/entry/2015/05/es6-modules-and-http2)も参考になります。 | |
AngularJSはこれまでCommonJSに対応せず、`<script src="angular.js">`での読み込みとグローバル変数`angular`を使って実装する従来のスタイルでした。それが1.3.14からはCommonJSに正式に対応し、次のようなコードで参照できるよう改良されています。 | |
```js | |
var angular = require('angular'); | |
``` | |
AngularJSには`angular.module('moduleName', ['requires'])`というAngularJS独自のモジュール機構があります。この機構とCommonJS、そしてAngular 2に採用されるES6 moduleとはどのように付き合っていくべきでしょうか、次の節で解説します。 | |
### ES6 module | |
ES6 moduleとはCommonJS module (`require`, `module.exports`)の仕組みに近いモジュール機構で、ES6の**言語レベルで備える**点が最大の特徴です。 | |
```js:commonjs.js | |
var otherModule = require('other-module'); | |
module.exports = function mymodule() { | |
// ... | |
}; | |
``` | |
```js:es6.js | |
import otherModule from 'other-module'; | |
export function mymodule() { | |
// ... | |
}; | |
``` | |
比較すると概ねこのようになります。厳密にはES6には`default`などの仕様が加わるため、全てCommonJSの仕様と一致するわけではありません。TypeScriptでは1.4からES6 moduleの構文で記述が可能となりました。 | |
### AngularJS module | |
AngularJSには`module`や`service`, `directive`, `controller`…とても用語が多く用途も似ているので大変混乱しやすいです。 | |
筆者は次のように考えています。 | |
- `angular.module()`: アプリケーション名、または提供するプラグイン名(まとめてプロジェクト名と称する) | |
- `angular.module().service()`: そのプロジェクト内の肥大化を避けるために適宜分割する単位 | |
AngularJS moduleは粒度がだいぶ大きく、これまでに述べたCommonJS/ES6 moduleとは性質が違うので、混同しないよう注意してください。CommonJS/ES6 moduleに相当するものとしてはAngularJS Serviceが近いです。 | |
## 移行を見据えたAngularJSでの考え方 | |
ここまで、AngularJSを取り巻くいくつかのモジュール機構(AngularJS module, CommonJS module, ES6 module)とAngularJSの特徴であるDIを紹介しました。AngularJSのmoduleが先行していたため、後発の仕組みと食い違い、複雑なものとなってしまいました。 | |
一旦整理し、何をどう扱うか説明しましょう。 | |
### AngularJS moduleはアプリケーション内にひとつ | |
前述のとおり、AngularJS moduleは粒度が大きい単位なので、アプリケーション内でいくつも用いることはアンチパターンだと捉えています。可搬性を考えると、ひとつのアプリケーションにはそのアプリ名を与えた単独のAngularJS moduleで運用すべきです。 | |
他のユーザにAngularJSプラグインを配布する場合は、ここにはプラグイン名が入ります。 | |
オススメ度: ★★★ | |
### 移行に向けての分割 | |
Angular 2はES6 moduleを積極的に活用する設計となっており、今からES6 moduleに関心を持つのは良いことです。ただ、今すぐAngularJSをES6 moduleに書き換えるべきかといえばそうとも限りません。いずれAngular 2で適合するように再度修正する作業は発生します。 | |
ただし、各種Service, Controller, Direcitiveを1ファイル内にひとつにしていくリファクタリングは、早めに意識しておくべきです。1ファイル内にいくつも定義している方はAngular 2での移行コストにES6 module導入の手間も加わるので、早めに段階的に分けて備えましょう。 | |
なお、`angular.module('myApp')`はチェーンにせず毎回書くべきという方針です。筆者の場合、1ファイルに1`angular.module('myApp').*`という方針にしており、1ファイル内にあれこれ定義しません。例外的に複数書くのは`angular.module().run()`と`angular.module().config()`のみです。 | |
オススメ度: ★★☆ | |
### CommonJS + Browserifyスタイルを好む方は | |
AngularJS 1.3.14からCommonJSが採用されたことで、Browserify[^4]の導入が容易になりました。Browserifyに詳しくない方は無理して導入する必要はありません。そして知識や経験がある方ならば、現時点からCommonJS化、Browserify対応を始めることは間違っていません。 | |
筆者はBrowserifyに対応したスタイルでAngularJSを[書いています](https://github.com/likr/interactive-sem/blob/master/app/src/scripts/app.ts#L11)。そして「1ファイル1Service」はそのまま「1ファイル1module[^5]」と置き換えられるので、Service自体を一切使わないようになりました。`angular.module().service()`や`.factory()`を使うことでAngularJS DIを利用できるというメリットは、[proxyquire](http://qiita.com/armorik83/items/46781adda80e9d53f7ee)を使うことで保っています。 | |
オススメ度: ★☆☆ | |
### proxyquireの実験 | |
【追記2015/6/28】更に検証を進めましたがAngularJSとproxyquireは全く相性が良くないのでお勧めしないどころか、止めておいたほうがいいです。素直にKarmaを使ってください。 | |
--- | |
AngularJSのDIはテスト利便性のためという性質が強いので、ここが維持できるならばAngularJSが提供するDIにこだわる必要は無いと筆者は考えました。proxyquireを併用すると、`$inject`の記述を少なくでき、ブラウザのローディングでどうしても実行が遅いKarmaから、高速なNode.js + [Mocha](http://mochajs.org/)に切り替えられる点をメリットと捉え実践してみました。 | |
筆者の[実際に運用したテスト](https://github.com/likr/interactive-sem/blob/master/test/unit/views/network-diagram/root-spec.js#L21-L30)です。実はこのときはまだproxyquireに気付いておらず、自力で似たような機構を実装していたためコード量が増えてしまっています。方向性としては同じです。 | |
このproxyquireで複数のモジュールをモック化しているだけでなく、AngularJS DIを用いる`$rootscope`や`$timeout`もL21-L30でモック化して与えています。例にはありませんが、`$resource`などのモック化も同様に行えます。 | |
ただしこれは尖ったアプローチで、一般的ではないと自覚して試みています。過度にやりすぎると今度はテスト自体が負債となる恐れもあり、筆者自身もまだバランスを確かめている段階です。 | |
オススメ度: ☆☆☆ | |
## Angular 2のDI | |
Angular 2では、ServiceとFactoryの違いに悩まずに済むDI機構や、ES6 moduleと親和性の高い[SystemJS](https://github.com/systemjs/systemjs)というモジュール管理機構を推奨しています。SystemJSはあまり馴染みがないですが「郷に入っては郷に従え」となるのかもしれません。 | |
# §4 jQuery | |
事を荒立てる前に言うならば、jQueryから完全脱却できない話は止むを得ないことだと感じています。そして意見に流されすぎるのもよくありません。主観的には様々な感情論があることでしょうが、それは置いておきます。 | |
## DOM操作に対する時代の変化 | |
仮想DOMの登場はjQuery主流だったWeb制作からすると新たな潮流です。仮想DOMの詳説は別の記事に譲ります。仮想DOMは差分検出を主としているため、jQueryを通じて──もちろん他の手段でもですが──DOMを直接操作すると仮想DOMの情報と実際のDOMに食い違いが発生します。そのため仮想DOMを用いるViewライブラリではjQuery等による手動DOM操作はご法度とされるようになりました。 | |
またjQueryは、もともとはゼロ年代のブラウザ間差異を吸収する目的で普及が進み、jQuery 2系の方針からも分かるように、現代ではレガシーブラウザはサポートされない方向に進んでいます。どうしても意図的に直接HTML要素を操作する場合も、現在は[DOM](https://developer.mozilla.org/ja/docs/DOM) APIが提供する範囲で十分賄えると感じます。ここでどうしてもjQueryの力が必要というときに使えばよいでしょう。 | |
## 負の遺産と嗅覚 | |
**身元の怪しい**jQueryプラグインから脱却できない事情は、負の遺産にしかなりえないと考えており、筆者はまったく賛同しません。これはjQueryに限らず何のライブラリ、フレームワークにおいても同様で、AngularJS向けのプラグインについても言えます。 | |
代表的なAngularJSプラグインに[AngularUI](http://angular-ui.github.io/bootstrap/)があります。これはAngularJS 1.4が控えてる今になってようやく1.3に対応したので、決して早い対応ではありませんでした。このようにプラグインを採り入れることは自らに足枷をはめることと同義になりかねないため、開発コミュニティの体力や活性度、採用することで得られるメリット、負の遺産としないための疎な設計、これらを十分に考慮すべきです。 | |
低品質なプラグインに対する嗅覚は常日頃養いたいものです。 | |
## Angular 2は脱jQuery | |
誤解をされている方がいるようですが、AngularJS 1.xはむしろjQueryとの親和性を意識して設計されました。それは今日になっても変わっていません。AngularJSのロードより前に`$`グローバル変数が存在した場合、自動的にmixinして`angular.element()`で使えるようになっています。 | |
ただし、親和性が高いからといって無頓着に増やし続けると、それはAngular 2への移行を困難にします。Angular 2では逆にjQueryを**推奨していません**。これは前述した仮想DOMの事情に近く、Change Detection([開発者の記事]((http://victorsavkin.com/post/110170125256/change-detection-in-angular-2)))を正しく動作させるためには直接DOMを操作すべきでないからです。Change DetectionはAngular 2の高速化に寄与しており、これは1系から2に移行する最大のメリットとなるでしょう。 | |
§2で`compile`, `link`には依存しすぎないよう注意せよと述べました。これはjQueryを極力控えるべき事情と通じています。 | |
## AngularJSとjQueryの付き合い方 | |
ではjQueryを多用しているAngularJSプロジェクトはどのように乗りこなすべきでしょうか。答えは[`ngIf`#](https://docs.angularjs.org/api/ng/directive/ngIf)や[`ngShow`#](https://docs.angularjs.org/api/ng/directive/ngShow), [`ngHide`#](https://docs.angularjs.org/api/ng/directive/ngHide)の駆使にあります。 | |
多くの場合、ダイナミックなHTML要素の追加には`ngIf`を使用してください。`ngIf`は`boolean`に従いDOMをその都度生成・削除します。どうしてもこの生成パフォーマンスが気になった場合には`ngShow/ngHide`を使えます。これは`display: none`によって要素は残しながら非表示とします。 | |
リッチなUIを実現するとき、しばしばアニメーション効果が望まれます。これは[`ngAnimate`#](https://docs.angularjs.org/api/ngAnimate)と[CSS Transitions](https://developer.mozilla.org/ja/docs/Web/Guide/CSS/Using_CSS_transitions)にて表現可能です。フェードイン・アウトはCSS Transitionsで制御する都合上`ngShow/ngHide`の方が扱いやすいです。ダイアログ、プルダウンメニュー、リストや通知の挿入、削除は`ngAnimate`で十分対応可能です。 | |
申し訳ないのですが筆者はあまり過度のアニメーション装飾を好まないため、これ以上複雑な動きには必然性を感じていません。どうしてもそういった動きを実現するのにjQueryが必要と考えるならば、同時にAngularJSが本当に必要かどうかについても検討できるでしょう。そこでAngularJSが必要ならば、これはCSS Transitionsの可能性を拡げるきっかけにも繋がります。 | |
適材適所と言ってしまえばそれまでですが、世の流れを窺いながら適切に付き合っていきましょう。 | |
オススメ度: ★★☆ | |
# §5 AngularJSの気になるところ | |
本章では、まとまり切らないながら重要である疑問について扱います。 | |
## $scopeをcontrollerAsに置き換えると$watchはどうすれば? | |
**A.** 問題なく使用できます。 | |
§1で`$scope`はControllerの`this`に置き換える手法を紹介しました。そうなると`$scope.$watch`はどう扱えばいいのか疑問が起こります。 | |
これは全く問題がなく、`$scope`を、Controllerのプロパティを生やすためのServiceとして捉えずに「`$scope`は`$watch`を提供するためのServiceである」と認識を改めるとよいでしょう。なので他のService、例えば`$resource`をDIするのと同じ感覚で`$scope`をDIしてください。 | |
```js | |
class MainController { | |
constructor($scope) { | |
this.$scope = $scope; | |
this.user = 'John'; | |
this.$scope.$watch('main.user', (newVal) => { | |
console.log(newVal); | |
}); | |
MainController.$inject = ['$scope']; | |
} | |
} | |
function mainDDO() { | |
return { | |
restrict: 'E', | |
controller: MainController, | |
controllerAs: 'main', | |
scope: {}, | |
template: '<input type="text" ng-model="main.user"><p>Hello, {{main.user}}!</p>' | |
}; | |
} | |
angular.module('myApp', []); | |
angular.module('myApp').directive('main', mainDDO); | |
``` | |
[動作サンプル](http://jsfiddle.net/c9ufb5sg/)です。意外かもしれませんが、`$watch`対象は`$scope`に格納されたプロパティに限っておらず`$watch('ここに書かれた式')`によって解決されています。式はHTMLの`{{main.user}}`と同等のものです。 | |
オススメ度: ★★★ | |
## $broadcast, $emit, $onは使ってもいいですか? | |
**A.** 答えに詰まりますが、Angular 2では別の機構に置き換えられ[互換性のあるAPIは用意されない](http://qiita.com/armorik83/items/d3d93ee1e4454e01d2a3)ため、これを理解した上で用いるなら可です。 | |
StackOverflowを眺めていても気付くところで、`$broadcast`には誤解があるようです。筆者もその一人で、どうやら`$broadcast`を[EventEmitter](https://nodejs.org/api/events.html)と同じように扱うことは開発陣は想定していないようでした。AngularJSとしては、むしろこのAPIを[Mouse Event](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)といったWeb APIに対して使うものと想定していたようで、その証拠にAngular 2に`$broadcast`に替わるものとして用意された[hostListeners](https://angular.io/docs/js/latest/api/annotations/Directive-class.html)の機構は、マウスやウインドウのイベント取得に向けて設計されています。 | |
ではAngularJS内で`$broadcast`をEventEmitter的に使ってはダメかというと、そうではありませんが、推奨されていないと認識すべきです。Angular 2で同等の機構を実現するならば、Reactと同様にEventEmitterを継承して使えばよいようです。 | |
オススメ度: ★★☆ | |
## Filterはどのように扱っていますか? | |
**A.** 内蔵されているもの以外は使っていません。 | |
筆者はあまり自作Filterを量産しようとは考えておらず、極力汎用ライブラリの処理結果をbindさせる手段をとっています。これはAngularJSから他社フレームワークに乗り換える必要が生じた際への対策といえます。汎用ライブラリの例は§6にて扱います。 | |
ところで、Angular 2には[Pipes](https://angular.io/docs/js/latest/api/pipes/)というAPIが備わっており、これがAngularJSでいうFilterに相当します。Angular 2のPipesに関する情報は@laco0416氏の[こちらの記事](http://qiita.com/laco0416/items/ae426eeb07db207dda44)をお勧めします。 | |
オススメ度: ★☆☆ | |
## ルータの最適解はComponent Routerですか? | |
**A.** 残念ながら[Component Router](https://angular.github.io/router/)を使う時期はまだ来ていません。 | |
Component Routerとは、2015年3月にNew Routerとして発表されたAngularチームが開発している新しいRouterです。New Routerの名称は陳腐化すると指摘があり、現在は(通称)Component Routerと呼ばれています。これはAPIがまだ安定しておらず、AngularJS 1.4.0にも間に合わないとアナウンスされている[^6]ため、今急いで導入する必要は無いでしょう。 | |
[Component Routerを実際に使う例](https://github.com/likr/gdgkobe20150429/tree/master/step3)が2015年4月のGDG神戸 Angular勉強会#3でありましたので、紹介しておきます。 | |
筆者はDirective化を強く進めた結果、[ui-router](https://github.com/angular-ui/ui-router)を必要とすることなく[ngRoute](https://docs.angularjs.org/api/ngRoute)で落ち着いてしまいました。ui-routerの有用性は認めているので、これは純粋に好みによるものです。ヒントを期待されていたならば、ごめんなさい。 | |
オススメ度: ★☆☆ | |
# §6 armorik83流モダン開発環境 | |
最後の章では、筆者がモダンだと考えているAngularJSの開発環境について記しておきます。Angular 2を意識してはいますが、入門者から熟練者まで幅広く受け容れられる構成とは思っておらず、参考にされる程度で構いません。 | |
## 汎用ライブラリ | |
ライブラリはできるだけ少なめにしたいという指針をもって選んでいます。 | |
- [lodash](https://lodash.com/docs) | |
- [string.js](http://stringjs.com/) | |
- [Moment.js](http://momentjs.com/) | |
- [decimal.js](http://mikemcl.github.io/decimal.js/) | |
- [D3.js](http://d3js.org/) | |
- [es6-promise](https://github.com/jakearchibald/es6-promise) | |
- [EventEmitter](https://nodejs.org/api/events.html) | |
オススメ度: ★★☆〜★★★ | |
## 言語 | |
**TypeScript** | |
まずは[TypeScript](http://www.typescriptlang.org/)です。TypeScriptとの付き合いもAngularJS歴と同じ2年弱で、強固な相棒でした。 | |
かつてJavaScriptでClass構文を使いたいならばTypeScriptしか選択肢がありませんでした。これもTypeScriptを選択した大きな理由です。最近は次に挙げるBabelも使います。 | |
オススメ度: ★★★ | |
**Babel** | |
[Babel](https://babeljs.io/)はもう手放せない存在です。AngularJSアプリケーションの場合、型を重視したいためTypeScriptを用いますが、シンプルに1000行程度で収まるライブラリはBabelで書くようになりました。2015年はBabelが急成長しており、TypeScriptでなくともClass構文を採り入れやすくなっています。Babelで書く場合でも、TypeScriptの`.d.ts`は用意しており、これは下手なドキュメントより優秀です。 | |
そのほか、テストではモックへの型検証がかえって邪魔になることがあるので、TypeScriptを使わず、すべてES6 + Babelで書いています。 | |
オススメ度: ★★☆ | |
**dtsm** | |
型定義`.d.ts`のインストールと管理は@vvakame氏の[dtsm](http://qiita.com/vvakame/items/38b953ab0f4de63cce8b)で行います。[tsd](https://github.com/DefinitelyTyped/tsd)と違い、コマンドがシンプルで複数のリポジトリに分散した型定義も管理しやすいだけでなく、やはり国産でバグ報告と修正が円滑という強みがあります。 | |
オススメ度: ★★★ | |
**ESLint** | |
[ESLint](http://eslint.org/)は[最近使い始めたら](http://qiita.com/armorik83/items/861e8b883ea5893a3320)とても気に入ったので、今後もBabelで書く際は併用したいと考えています。一方、[TSLint](https://github.com/palantir/tslint)は残念ながら処理待ちが長いため使っていません。これはコーディング・スタイル以外はtscが警告してくれるものと考え妥協しています。 | |
オススメ度: ★★☆ | |
**Less** | |
CSS自体あまり書きたくないのですが、そうも言えないため[LESS](http://less-ja.studiomohawk.com/)を使っています。以前流行りだしてすぐの頃は[Sass](http://sass-lang.com/)と[Compass](http://compass-style.org/)でした。Rubyってのがどうにも馴染まず。 | |
オススメ度: ★☆☆ | |
## タスクランナーとビルドツール | |
**gulp** | |
タスクランナーは、今は専ら[gulp](http://gulpjs.com/)を使っています。単純なものは`npm scripts`に記述し、残りの大半は`gulpfile.js`に記述しています。以前[記事を書きました](http://qiita.com/armorik83/items/0860dc097d11f18b9973)ので、gulpについてはそちらを。 | |
オススメ度: ★★☆ | |
**Browserify** | |
[Browserify](http://browserify.org/)は§3のCommonJS moduleの節でも触れました。AngularJSがCommonJSに対応したことでBrowserify導入の契機となりました。 | |
オススメ度: ★★☆ | |
## テスト | |
**Mocha** | |
AngularJSはテストの実行に[Jasmine](http://jasmine.github.io/)を用いて解説していますが、筆者はJasmineから[Mocha](http://mochajs.org/)に乗り換え現在も使い続けています。`describe, it`スタイルでJasmineとよく似ています。 | |
オススメ度: ★★★ | |
**Sinon.JS** | |
モックの強い味方[Sinon.JS](http://sinonjs.org/)です。AngularJSのDirectiveやControllerのテストは大半をSinon.JS併用で行い、最後の網掛けとして本物を使ったe2eで埋めています。 | |
オススメ度: ★★☆ | |
**Nightmare** | |
そのe2eは紆余曲折があり、Angularチームが開発している[Protractor](http://angular.github.io/protractor/#/)やGrouponの[Testium](https://github.com/groupon/testium)など、いろいろ試してきました。 | |
世の中がまだPhantomJS 2に向かいきってないのが歯痒いですが、現在は[Nightmare](http://www.nightmarejs.org/)になんとか落ち着いています。なお、残念ながらAngular 2 alphaではe2e周りがとても弱く、今後改善されることを期待しています。 | |
オススメ度: ★★☆ | |
**proxyquire** | |
§3のDIの節にて紹介した[proxyquire](https://github.com/thlorenz/proxyquire)、AngularJS DIをあえて用いない、かなり尖った構成です。[詳細はこちら](http://qiita.com/armorik83/items/46781adda80e9d53f7ee)。 | |
オススメ度: ★☆☆ | |
**power-assert** | |
@t_wada氏を中心に開発されている[power-assert](https://github.com/power-assert-js/power-assert)も最前線。コンソールデバッグの手間を大幅に減らせて心強い味方です。余談ですがこのプロダクトの[ロゴマーク](https://github.com/power-assert-js/power-assert-js-logo)をデザインさせていただきました。 | |
オススメ度: ★★★ | |
**cw-log** | |
手前味噌で[cw-log](http://qiita.com/armorik83/items/733ba1a41cf9aec0b382)も多用しました。AngularJSで複数のClassを組み合わせると処理順を確かめたい場合が多く、本番環境でもログの出力をコントロールでき重宝しています。 | |
## RIP | |
最後に、使っていたものの今は離れたライブラリを供養して締めくくります。 | |
- **Yeoman Generator**: 保守が面倒すぎて、どんどん陳腐化した | |
- **Grunt**: このJSの速さについてきていない、各種プラグインが負債化 | |
- **Bower**: npmとBrowserifyですべて管理するので不要に | |
- **Jasmine**: そこまで悪いわけではないが、Mocha + SinonJS + power-assertを考えるともう戻れない | |
- **Protractor**: Jasmineに依存しているため同上の理由 | |
- **Underscore.js**: lodashを使っている | |
- **jQuery**: §4参照 | |
# 謝辞 | |
本稿のためにたくさんの方々のご協力をいただきました。 | |
私がAngularJSに真摯に向き合う機会をくださり、長時間の相談と推敲にも付き合っていただいた@_likr氏、AngularJSを使い始め入門者の視点から沢山の指摘を寄せてくださった@shinsukeimai氏に感謝いたします。 | |
本稿のレビューを引き受けていただき、貴重な視点や指摘、理解の深まる質問を寄せてくださった@Quramy氏、@izumin5210氏、Angular 2の情報収集の際にとても支えとなっている@shuhei氏、@laco0416氏に感謝いたします。それぞれのAngularへの熱意がこの記事を押し上げました。@vvakame氏には私がAngularJSとTypeScriptを使い始めて間もない頃から、数々のアドバイスをいただけましたこと感謝いたします。 | |
最後に、本稿を最後までお読みいただいた皆様へ心より御礼申し上げます。本稿が皆様の役に立ち、Angularユーザがより豊かになることを願っています。 | |
2015年5月 Crescware 奥野賢太郎 @armorik83 | |
--- | |
[^1]: stableは1.4.1ですが、標準でインストールされる最新バージョンが1.5.0-betaとなっています。 | |
[^2]: `private`でも可。 | |
[^3]: オレオレDIはテスト困難なモジュールに対して暫定的に強引に導入する性質が強く、毎回やるべきではないと考えます。 | |
[^4]: webpack派はwebpackと読み替えて結構です。 | |
[^5]: CommonJSのmoduleを指します。 | |
[^6]: [Angular Weekly Meeting](https://docs.google.com/document/d/150lerb1LmNLuau_a_EznPV1I1UHMTbEl61t4hZ7ZpS0/mobilebasic?pli=1)議事録、開発者向けの情報です。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment