Angular2のリリースが近づいてきている今、Angular1で開発された資産を、どうやってAngular2に移行していけばよいのでしょうか?
この記事では、実際に移行を行う上でのプロセスを具体的に示していきます。
編集部注: この記事は、2016年3月21日に開催された「ng-japan 2016」のセッション「Angular1.4で作られた自社マイクロサービスを2へマイグレーション」についての、講演者自身によるレポートです。講演内容に加えて、講演者自身による解説や追記によって、よりわかりやすく詳細な記事に仕上げていただきました。
セッションの講演資料と動画はこちらになります。
講演資料
講演動画(4:19:52付近から始まります)
Angular1から2へ移行する前に
Angular2への移行を始める前に、Angular1の構成やソースコードを整理する必要があります。公式サイトに「UPGRADING FROM 1.X」という1からアップグレードする手順の解説があります。その手順に加え、実際のアプリケーションの構成を考慮すると以下のような流れになります。
- 事前準備
- ng-upgrade
- コンポーネント(ディレクティブ)の移植
- サービスの移植
- ページコンポーネントの移植
- フィルターの移植
- ルーターの移植
- ng1ライブラリの置き換え
- ng-upgradeの除去
※ セッションでは上記の「サービスの移植」までの前段のみを取り上げています。
事前準備
Angular2では1とは異なる点が多く、そのままではすぐにアップグレードすることはできません。そこでまずは1で近い構成にする事前準備が必要になります。
AngularJS Style Guideに従った構成になっていること
johnpapa/angular-styleguide Angular 1 Style Guide
上記スタイルガイドのRule of 1及びFolders-by-Feature Structure、Modularityの3点には最低限合わせておきましょう。
Angular2ではコンポーネントが中心になり、ファイルの構成もこのスタイルガイドと近い構成になっていきます。あらかじめコンポーネントごとにファイルを分けて置くことで、基本的な設計もコンポーネント指向に近づけておきます。
「その使い方はもう古いかも?AngularJS老化チェック」という記事の内容が現在のコードに当てはまらないかどうかをチェックしておきましょう。当てはまったものはスタイルガイドに沿ってリファクタリングを行いましょう。
モジュールローダー(モジュールバンドラー)(Browserify/Webpackなど)を使っていること
Angular2は必要なスクリプトを読み込んでいって作ることもできますが、モジュールローダー(モジュールバンドラー)前提で話が進んでいくことがあり、導入されていないと理解が難しい状況になります。何も導入していなければBrowserifyかWebpackをおすすめします。公式サイトではSystemJSを用いていますが、Browserify/Webpackを使う場合、必ずしもSystemJSを使う必要はありません。
TypeScriptへ移行
Angular2入門のページではBabelやTypeScriptなどの利用を、とありますが、マイグレーション時においてはTypeScriptへの移行を先にすることを強くお勧めします。
例えば先ほどの「UPGRADING FROM 1.X」のページですが、JavaScript用のドキュメントは準備中となっています(2016年4月現在)。
このような状況からみて、今後の新しい機能についてもTypeScript優先で情報提供されることがあることでしょう。また、既にAngular2に取り組んでいる先人たちのノウハウ(StackOverflowなど)もTypeScriptをベースにしているものが多いので、これを機にTypeScriptを使うと良いでしょう。型などを用いることで移行時の不具合を見つける手助けにもなります。
コンポーネントディレクティブの使用
Angular1.5からコンポーネント機能が提供されました。Angular2の基本はコンポーネントで構成されます。そのため、既存のアプリケーションをコンポーネント化していくことで移行がスムーズになります。対応ブラウザに問題がなければまずはAngular1.5にアップグレードし、ディレクティブや共通部分をコンポーネント化しましょう。
Angular Developer Guide / Component
ここまでを一通り行い、Angular2への移行の準備が整います。
この時点で難しいと思われる方もいるかもしれません。しかしながら、Angular2でも準備と同様の環境が求められます。Angular2はパフォーマンス改善やコンポーネント指向など魅力的な変更があるのでぜひとも移行していきたいところなのです。いずれにしても、今後も大きなフロントエンド開発においてはこのようなモダン構成が求められることでしょうから、そうした練習としてAngular2を採用するのも良いでしょう。
Angular2の導入
Angular2の方へ入っていきたいと思います。最低限必要なものとして以下のものが必要になります。(2016/4/24時点)
- angular2@2.0.0-beta.15
- systemjs@0.19.26
- refrect-metadata@0.1.2
- rxjs@5.0.0-beta.2
- zone.js@0.6.10
- es6-shim@0.35.0
npm installでまとめて導入できます。
npm install angular2@2.0.0-beta.15 systemjs@0.19.26 refrect-metadata@0.1.2 rxjs@5.0.0-beta.2 zone.js@0.6.10 es6-shim@0.35.0
まずAngularの起動方法を変更します。
<body ng-app="app">...</body>
これは以下のようにangular.bootstrap関数を用いて定義するようにします。この際HTMLにng-appは不要です。
angular.bootstrap(document.body,['app'],{strictDi:true});
さらにこの部分をng-upgradeと呼ばれているangular2/upgradeのUpgradeAdapterというものを用いてAngularを起動していきます。
import {UpgradeAdapter} from 'angular2/upgrade';
export const Adapter = new UpgradeAdapter();
import {Adapter} from './adapter.ts';
Adapter.bootstrap(document.body,['app'],{strictDi:true});
このようにupgradeの中にあるUpgradeAdapterを経由して実行をします。今後1と2を混在させた状態でアプリケーションを動かすためのハブのような役割をこのAdapterが行ってくれます。今後いろいろな場面でAdapterを呼び出して使うことになるので、あらかじめ別のファイルにしておいて外部から参照できるようにしておきましょう。
続いてangular2-polyfill.jsを読み込みます。後々Observableを使う時に必要になるので、忘れないように入れておきましょう。
import 'angular2/bundles/angular2-polyfills';// for old browsers import 'es6-shim/es6-shim.min.js'; import 'angular2/es6/dev/src/testing/shims_for_IE';
ここでユニットテストなどでPhantomJSなどで実行しているとエラーが出ます。es6-shimを入れることで回避することができます。
TypeScriptにもes6の型定義ファイルが必要になるので、es6-shimの型定義ファイルを入れておきましょう。
typings install es6-shim --save --ambient
以上で移行の前段はできました。ここからはAngular1で作ったものを徐々に2へと置き換えて行く作業になります。
コンポーネントを置き換え
まずは他にあまり依存しない、Directiveのrestrict:’E’で実装されていたものをAngular2へ移行していきます。大きなところからやりたくなりますが、移行は末端から順番にやるようにしましょう。
以下はAngular1のコンポーネントを使って書かれたローディングの例です。
const template = require('./loading.html');
class LoadingCtrl {
constructor(private LoadingService) {}
}
angular.module(‘app')
.component('globalLoading',{
controller: LoadingCtrl,
transclude: true,
template: template
});
export default LoadingCtrl;
class LoadingService {
count:number = 0;
loading:boolean = false;
constructor() {}
start() {
++this.count;
this.loading = true;
}
stop() {
--this.count;
if (this.count === 0) {
this.loading = false;
}
}
}
angular.module(‘app')
.service('LoadingService',LoadingService);
<div ng-show="LoadingService.loading" class="loading">
<div class="loading-text" ng-transclude></div>
</div>
このローディングコンポーネントをAngular2に置き換えていきます。 まずはコンポーネント部分を置き換えていきましょう。
import {Adapter} from '../../core/adapter';
import {Component} from 'angular2/core';
import {LoadingService} from './LoadingService';
const template = require('./loading.html');
@Component({
selector: 'global-loading',
template: template
})
export class LoadingCtrl {
constructor(private LoadingService:LoadingService) {}
}
angular.module('app').directive('globalLoading',<angular.IDirectiveFactory>Adapter.downgradeNg2Component(LoadingCtrl));
Angular2の基本はコンポーネントになっているので、出来る限りコンポーネントに置き換えておきましょう。
Angular2で書かれたコンポーネントは1環境では呼び出すことができません。そこでディレクティブにAdapter.downgradeNg2Component()を用いて登録することで、Angular1環境下においても使うことができるようになります。
続いて、サービス部分の移植です。
import {Adapter} from '../../core/adapter';
import {Injectable} from 'angular2/core';
@Injectable()
export class LoadingService {
count:number = 0;
loading:boolean = false;
constructor() {}
start() {
++this.count;
this.loading = true;
}
stop() {
--this.count;
if (this.count === 0) {
this.loading = false;
}
}
}
angular.module('app').factory('LoadingService',Adapter.downgradeNg2Provider(LoadingService));
Adapter.addProvider(LoadingService);
@Injectable()をつけてInject(DI)可能であることを示します。
コンポーネント同様に、Angular2記法のServiceをAngular1からもDIできるようにAdapter.downgradeNg2Provider()を使ってダウングレードして登録しておきます。
加えて、Adapter.addProvider()で作ったサービスをProviderに登録する必要があります。これはUpgradeAdapterを使っている場合で、Angular2のコンポーネントからDIさせるために必要になります。
これを行わないと以下のようなエラーとなります。
EXCEPTION: Error: [$injector:unpr] Unknown provider: LoadingServiceProvider <- LoadingService <- LoadingInterceptor <- $http <- $templateFactory <- $view <- $state
同様にHTMLもAngular2記法に置き換えを行います。
ディレクティブではng-transcludeを使っていましたが、Angular2ではng-contentになっているのは注意が必要です。
また、細かいところですが、angular.constantsもなくなっているので、constでオブジェクトを作っておき、それをimportして使うように置き換えましょう。
ServiceのAngular2移行
さて次に、ServiceをAngular2化していきます。 基本はLoadingのときと同じですが、通信周りには変更があります。
Ajax通信を行う際は$httpを使って通信を行っていました。Angular2ではHttpでAjax通信を行うことができます。$httpはPromiseを返していましたが、HttpはObservableを返すという違いがあります。
以下はAngular2記法に置き換えたいわゆるモデルです。作りは先程のローディングの場合と殆ど変わりませんが、HttpはObservableを返すので、処理の変更箇所を減らすためにPromiseで返すようにしています。
import {Adapter} from '../../core/adapter';
import {Injectable} from 'angular2/core';
import {CONSTANTS} from '../../core/constants';
import {Http, Response} from 'angular2/http';
@Injectable()
export class AccountModel {
constructor(protected http:Http) {}
get() {
return this.http.get(${CONSTANTS.API_PATH}account).toPromise();
}
}
angular.module('app').factory('AccountModel', Adapter.downgradeNg2Provider(AccountModel));
Adapter.addProvider(AccountModel);
Httpの返すObservableについてはRxJSのドキュメントを参照してください。
最後にあらかじめコンポーネントにしておいたページでフォームの部分を移植してみます。 まずテンプレートをAngular2記法に置き換えます。
テンプレートの記法は以下の参考リンクを上から順番に見ていくと良いでしょう。
- *ngIfのようなシンタックスについて – TemplateSyntax
- Angular1ではこう書いてたけど2だとどうなるの?というとき – ANGULAR 1 TO 2 QUICK REFERENCE
- 慣れてきて一覧でみたい時 – ANGULAR CHEAT SHEET
formの移植
続いてformの制御部分を移植します。メールアドレスの入力フォームで、バリデーションが通らないと更新ボタンが押せない(disabled)になるというよくあるものです。
<form ng-submit="$ctrl.save()">
<label class="layout-block">メールアドレス変更</label>
<div class="form-item">
<div class="form-input form-fill">
<input ng-model="$ctrl.newEmail" type=“email" name="email" placeholder="新しいメールアドレス" required>
<span class="form-error">
<span ng-if="!form.email.$error.email">メールアドレスを正しく入力してください</span>
<span ng-if="!form.email.$error.required">必ず入力してください</span>
</span>
</div>
</div>
<button ng-disabled="!emailform.$valid" class="btn btn-default btn-revert">メールアドレスを変更する</button>
</form>
formの制御のために[ngFormModel]="name"を使います。(これ以外にも複数方法があります)また、inputタグにはngControl="name"を使って名前を付けます。
下記のように書きかえます。
<form [ngFormModel]="form" (submit)="save();">
<div class="form-input">
<input [(ngModel)]="user['lastname-phonetic']" ngControl="lastphonetic" type="text" value="" placeholder="セイ">
<span class="form-error" *ngIf="form.controls.lastphonetic.errors">
<span class="form-error-message">
<div [hidden]="!form.controls.lastphonetic.errors.required">セイは必須項目です</div>
<div [hidden]="form.controls.lastphonetic.errors.required && form.controls.lastphonetic.errors.invalidPhonetic">全角カタカナで入力してください</div>
</span>
</span>
</div>
<button [disabled]="!form.valid" class="btn btn-default btn-revert">更新</button>
</form>
フォームのエラー状況によって振り分けしたい部分は[ngFormModel]でつけた名前.controls.inputにつけたngCotrol名.errors.エラー名でエラーが発生しているかどうかを確認することができるようになりますが、スクリプトも修正が必要です。
import {FORM_DIRECTIVES,Control,ControlGroup,Validators} from 'angular2/common';
@Component({
selector: 'profile-form',
templateUrl: 'webroot/components/profile/profile.html',
directives: [FORM_DIRECTIVES]
})
this.form = new ControlGroup({
lastphonetic: new Control('',Validators.compose([
Validators.required,
ValidateService.phonetic
]))
});
コンポーネント内でform関連のディレクティブを使うときには@Componentのdirectivesに配列で使うディレクティブを指定します。
先ほどngModelFormにつけた名前と同じ名称にnew ControlGroup()します。 そしてngControlにつけた名前と同じ名称でnew Control()します。 第一引数がデフォルトの値、第二引数にバリデーション関数を指定します。 複数指定する場合はValidators.componse([])を1つの場合はValidators.requiredなどを指定します。
Validatorsにはあらかじめ定義されているバリデーターがあります。
static required(control: modelModule.Control): {
[key: string]: boolean;
};
static minLength(minLength: number): Function;
static maxLength(maxLength: number): Function;
static pattern(pattern: string): Function;
static nullValidator(c: any): {
[key: string]: boolean;
};
Angular1ではtype=”email”でメールアドレスのチェックが行われていましたが、Angular2では上記のチェック以外は行われない点は注意してください。
今回はメールアドレスなので、カスタムバリデータを作成します。
export const ValidateService = {
email(control:Control) {
if (!/^[a-z0-9!#$%&'+\/=?^_`{|}~.-]+@a-z0-9?(.a-z0-9?)$/i.test(control.value)) {
return {email: true};
}
}
}
returnする名称はエラーが発生した時に form.controls.lastphonetic.errors.emailで取得することができる値です。
バリデーションの正規表現についてはAngular1で使用されていたものと同じです。
セッションでは時間の関係で移行手順の途中までしかお話できませんでした。 移行作業は作られたアプリケーションによって異なりますが、公式サイトの手順だけではハマりどころも発生することでしょう。
今後移行手順に関してはさらに整理され情報も出てくると思いますので、事前準備をしっかりと行い、ng-upgradeを用いて少しずつ移行を進めていきましょう。