Angular2のリリースが刻一刻と近づいてきました。しかし世の中のプロダクトは、まだまだ大半がAngular1.xで開発されています。Angular2はコンポーネント指向が徹底されていたり、TypeScriptが推奨の開発言語であるなど、Angular1から大きく変わっており、一見すると移行は容易ではありません。
しかしAngular1.xの最新バージョンである1.5では、Angular1から2への移行をスムーズに行うために、Angular2を見据えたコーディングが行えるようになっています。この記事ではAngular2への移行をスムーズにするための、Angular1の書き方を紹介していきます。
【編集部注】
※この記事は、2016年3月21日に開催された「ng-japan 2016」のセッション「Angular2を書くためのAngularJSの書き方」についての、講演者自身によるレポートです。講演内容に加えて、講演者自身による解説や追記によって、よりわかりやすく詳細な記事に仕上げていただきました。
※本記事では、AngularJSをAngular1、 AngularJSの2系をAngularと表記しています。
セッションの講演資料と動画はこちらになります。
講演資料
講演動画(2:03:46付近から始まります)
Angular1の歴史と背景
Angular1は、今から4年前の2012年6月に、バージョン1.0.0がリリースされました。この4年間に、Windowsなら8->10、Macで10.8->10.11、Androidで4->6、iOSで6->9という変化がありました。バージョンアップが比較的遅いとされるOSでも、4年の間にこれほど大きなバージョンアップが行われているのです。
現在のIT業界では、4年とはこれほど長い期間です。また4年前のWebを振り返ってみますと、当時はBackbone.jsなどのMVCフレームワークが全盛期でした。Angularはリリースされてから大きな人気を博し、世界中の数多くのプロジェクトで採用されました。
しかし月日は流れ、2014年にReactが発表されてからは、Angular1の人気にも陰りが訪れます。コンポーネント指向を採用したReactの優れた設計が広く支持されたこと、Angular1はパフォーマンスがあまりよくないということもあり、Reactを採用するプロジェクトも増えてきました。
そこでパフォーマンスを大きく改善し、アーキテクチャが刷新されたAngular2が今年中にリリースが予定されています。しかしAngular2は変更点が多く、普及には時間がかかることが予想されます。そのため、Angular1.xは当分サポートされることが決定しています。
Angular1とAngular2の違い
Angular1とAngular2の違いは多岐にわたります。その中でも代表的な違いを以下に挙げます。
- 主な開発言語がJavaScriptからTypeScriptへ
- Two-wayデータバインディングを廃し、One-wayデータバインディングへ
- 従来のスコープに代わり、コンポーネントがUIの状態を保持する(コンポーネント指向)
- テンプレートの文法が変更
- 従来のng-routerからコンポーネントルーターに
- 従来の文字列ベースのDI (Dependency Injection)から、型によるDIに
対象ブラウザに関して
Angular1.5は、残念ながらIE8以前では動作しません。IE8以前を対象とする場合は、Angular1.2を利用する必要があります。もしIE8を対象にしなくてよいのであれば、Angular1.5へのアップデートが強く推奨されます。
JavaScriptからTypeScriptへ
Angular2では、メインの開発言語がTypeScriptに変更されました。
簡単に経緯を説明しますと、もともとAngular2を開発する際、JavaScriptのスーパーセットであるAtScriptという言語を同時に開発していました。
AtScriptの実体は、アノテーション付きのTypeScriptと言ってよいものでしたが、開発は難航。2015年3月にTypeScriptが(アノテーションに近い機能である)デコレーターの実装を表明すると、Angular2はTypeScriptへと移行したという流れがあります。
実際には、Angular2にとってTypeScriptは必須ではなく、JavaScriptやDartでも開発は可能です。しかしTypeScriptを利用すると多くのメリットがあるので、TypeScriptの使用が推奨されています。
ちなみに、TypeScriptというのは以下のような特徴を持った言語です。
TypeScriptの特徴
- 型がある
- インターフェースがある
- モジュールインポートがある
- 仕様がしっかりしている
- JavaScriptのスーパーセットである
- TypeScriptはそのままではブラウザでは利用できないのでコンパイル(JavaScriptへの変換)が必要
BrowserifyやWebpackを使おう
皆さん、GruntやGulpと言ったタスクランナーを既に使われている方は多いと思います。Angular1.5 / 2の開発では、TypeScriptのimport文を使用してモジュール間の依存性を記述していくため、こうした依存関係を解決して実行可能なプログラムを生成できるツールが必要です。
そうしたツールにはBrowserifyやWebpack、System.jsなどがあり、import文やCommonJSのrequire()関数などを解釈し、依存関係を解決した上で、ファイルを一つにまとめる機能を持ちます。
TypeScriptからのコンパイルなどもプラグインとして提供されており、今後のAngular開発には必須のツールとなっています。
Angular1.5のコードを眺める
Angular2とAngular1の違いや開発に必要な情報が一通り揃ったところで、実際にAngular1.5のコードを眺めてみましょう。 以下は、画面に「Hello」と表示するだけのプログラムです。
import * as angular from 'angular';angular.module('app') .component('app', { template:
<div>Hello {{ $ctrl.text }}</div>
, controller: class App { public text: string; constructor() {} }, bindings: { text: '@' } });
<!doctype html> <html> <body> <app text="ng-japan"> Loading... </app> <script src="./bundle.js"></script> <script> angular.bootstrap(document, ['app']); </script> <body> </html>
Angular1のコードには違いないように見えますが、これまでとは何かが違いますね。
実際上のコードはTypeScriptで書かれており、import
やclass
、public
などのアクセス指定子など、素のJavaScriptでは使えない文法が多く使われています。
DirectiveからComponentへ
Angular2から、コンポーネントという概念が登場します。それに合わせて、Angular1.5でもコンポーネントが利用できるようになりました。Angular1のディレクティブに比べて、簡単に作ることができます。
DirectiveとComponentでの設定値の違い
Directiveには先述したComponentと呼ばれるDOMの生成と他にAttributeに設定する処理ng-repeatやng-showなども同じ方法で作成しています。
そのため、Componentを作るには設定が過多気味であり、Angular2へ移行する場合に必要なものと不必要なものをより分けた作成メソッドを新たに追加されました。
以下の表が元々のDirectiveメソッドと追加されたComponentメソッドとの設定の違いとなります。
Directive | Component | |
---|---|---|
bindings | No | Yes (binds to controller) |
bindToController | Yes (default: false) | No (use bindings instead) |
compile function | Yes | No |
controller | Yes | Yes (default function() {}) |
controllerAs | Yes (default: false) | Yes (default: $ctrl) |
link functions | Yes | No |
multiElement | Yes | No |
priority | Yes | No |
require | Yes | Yes |
restrict | Yes | No (restricted to elements only) |
scope | Yes (default: false) | No (scope is always isolate) |
template | Yes | Yes, injectable |
templateNamespace | Yes | No |
templateUrl | Yes | Yes, injectable |
terminal | Yes | No |
transclude | Yes (default: false) | Yes (default: false) |
引用:https://docs.angularjs.org/guide/component
Angular1のパフォーマンス問題
Angular1のパフォーマンスが良くない理由ですが、その原因を挙げてみましょう。
- Two-wayデータバインディングが重い
- 状態の監視に伴うオーバーヘッド
- dirty check
- $digest loop
では、どうしたらよいのでしょうか?以下の様なアーキテクチャが推奨されていますが、Fluxアーキテクチャによく似ています。
では、このアーキテクチャに従ったコードのサンプルを紹介します。
まずはアプリケーションのエントリーポイントとなるHTML(index.html
)です。これは、angular.bootstrap()
を呼び出しているだけで、特に変わったことはしていません。ただし、list-cmp
というタグを使用していることは覚えておいてください。
<!doctype html> <html> <body> <list-cmp> Loading... </list-cmp> <script src="./bundle.js"></script> <script> angular.bootstrap(document, ['app']); </script> </body>
以下がサービスのコードです。このサービスは、アプリケーションのデータを保持しており、データの操作を行うことが可能です。
import * as angular from 'angular';angular.module('app') .service('StoreService', class Store{ private datas: Array<string> = []; constructor(){ this.datas = ['angular', 'javascript', 'typescript', 'angular2']; } getList() { return this.datas; } addData(data:string) { this.datas.push(data); } changeData(index:number, data:string){ this.datas[index] = data; } deleteData(index:number){ this.datas.splice(index, 1); } })
以下は、index.html
で使用されていたlist-cmp
タグ(コンポーネント)の実装です。内部で、さらにlang-cmp
というタグ(コンポーネント)を使用しています。
また、Angular1で多用されていた$scope
はもう使われておらず、代わりにコンポーネントのコントローラーの参照である$ctrl
が使用されています。基本的には、$scopeはもう使用してはならないものとして考えましょう。
import * as angular from 'angular';angular.module('app') .component('listCmp', { template:
<ul><li ng-repeat="data in $ctrl.datas"> <lang-cmp index="$index" lang-data="data" lang-change="$ctrl.change(index, data)" lang-delete="$ctrl.delete($index)"></lang-cmp> </li></ul>
, controller: class List { private store; private datas: Array<string> = []; constructor(StoreService) { this.store = StoreService; this.datas = StoreService.getList(); } change(index: number, data: string) { this.store.changeData(index, data); } delete(index: number){ this.store.deleteData(index); } } });
最後に、lang-cmp
タグの実装です。上のコードで指定されていたlang-change
やlang-delete
と言った属性が、langChange
やlangDelete
といったプロパティに対応しています。
import * as angular from 'angular';angular.module('app') .component('langCmp', { template:
<input ng-model='$ctrl.langData'> <button ng-click="$ctrl.change()">変更</button> <button ng-click="$ctrl.delete()">削除</button>
, controller: class Data { private langData:string; private index:number; private langChange; private langDelete; constructor() {} change() { console.log(this.index, this.langData); console.log(this.langChange); this.langChange({index: this.index, data: this.langData}); } delete() { this.langDelete({index: this.index}); } }, bindings: { 'langData': '<', 'index': '<', 'langChange': '&', 'langDelete': '&' } })
コンポーネント指向のディレクトリ構造
コンポーネント指向になったことから、コンポーネント単位にまとめると見通しがよくなります。
├─about │ └─components │ about.component.e2e.ts │ about.component.html │ about.component.scss │ about.component.spec.ts │ about.component.ts │ ├─app │ └─components │ app.component.e2e.ts │ app.component.html │ app.component.spec.ts │ app.component.ts │ navbar.component.html │ navbar.component.scss │ navbar.component.ts │ toolbar.component.html │ toolbar.component.scss │ toolbar.component.ts │ ├─assets │ │ main.scss │ │ _colors.scss │ │ _variables.scss │ │ │ └─svg │ more.svg │ ├─home │ └─components │ home.component.e2e.ts │ home.component.html │ home.component.scss │ home.component.spec.ts │ home.component.ts │ └─shared └─services name-list.service.spec.ts name-list.service.ts
テストについて
Angular2ではテストの基本セットがJasmineとなっていますので、Jasmineだと学習コストは少なくなります。
ルーティング
Angular1ではngRouteよりもui-routerがよく使われていると思いますが、Angular2では性能と管理が簡単なComponentRouterが登場します。 Angular1.5用のComponent Routerが用意されています。
まとめ
このセッションで学んだことを最後にまとめます。
- Angular1のプロジェクトでもTypeScriptを使おう
- タスクランナーを利用しましょう
- Webpackやbrowserifyを利用しましょう
- DirectiveからComponentに変更しよう
- controllerの廃止
- Scopeは原則利用しないように
- Two-way data-bindingよりOne-way data-binding
- コンポーネント単位でファイルを整理しよう