HTML5Experts.jp

Angular2を書くためのAngularJSの書き方

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の違いは多岐にわたります。その中でも代表的な違いを以下に挙げます。

対象ブラウザに関して

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の特徴

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で書かれており、importclasspublicなどのアクセス指定子など、素の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のパフォーマンスが良くない理由ですが、その原因を挙げてみましょう。

では、どうしたらよいのでしょうか?以下の様なアーキテクチャが推奨されていますが、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: &lt;ul&gt;&lt;li ng-repeat="data in $ctrl.datas"&gt; &lt;lang-cmp index="$index" lang-data="data" lang-change="$ctrl.change(index, data)" lang-delete="$ctrl.delete($index)"&gt;&lt;/lang-cmp&gt; &lt;/li&gt;&lt;/ul&gt;, 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-changelang-deleteと言った属性が、langChangelangDeleteといったプロパティに対応しています。

import * as angular from 'angular';

angular.module('app') .component('langCmp', { template: &lt;input ng-model='$ctrl.langData'&gt; &lt;button ng-click="$ctrl.change()"&gt;変更&lt;/button&gt; &lt;button ng-click="$ctrl.delete()"&gt;削除&lt;/button&gt;, 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が用意されています。

まとめ

このセッションで学んだことを最後にまとめます。