速習!Angular1からAngular2への移行

og

こんにちは!Pairsの開発を担当している太田です。

前回はAngular2に対するインプレッションを書きました。Angular2への気持ちが高まってきたところで、今回はAngular1からAngular2への移行を試してみましたのでフィードバックします。


※今回の記事の内容はangular2のbeta2を利用したものになります。
※公式の移行ガイドはこちらです。
UPGRADING FROM 1.X: https://angular.io/docs/ts/latest/guide/upgrade.html

移行のイメージをつかむ

公式では、アプリのすべてを一気に移行するビックバン移行ではなく

段階的移行

が推奨されています!


Angular2はAngular1の考え方を引き継ぎながらも実装は全く異なる形で開発が進められてきたため、システマティックな移行は不可能と思われていました。
そんな折、とても大きなニュースが飛び込んできたのです!


Angular 2とAugular 1はアプリ内で混在可能になると発表。Anguler 1から2への段階的な移行が可能に: http://www.publickey1.jp/blog/15/angular_2augular_1anguler_12.html


この記事を読んだときは震えました。

「こんなこと本気でやる気なのか・・・?!」(ガクガク)

しかしAngular2開発チームはやってのけてくれたのです。Awesome!


1つのアプリ内でAngular1とAngular2が共存可能になる仕組みが提供され、
Angular1アプリ => 1と2のハイブリッドアプリ => Angular2アプリ
といった段階を踏んだ移行が可能になりました。


ただし、やはりというかなんというか、どんなアプリでもカンタンに共存 & 移行ができるわけではなく、
Angular2 に近い構成の Angular1アプリになっていること
が前提条件になります!(具体的な内容は後述します)


とはいえ、これは以前から想定されていたことはありますので、しっかり準備されていた方も多いのではないでしょうか!(勝ち組)
例えばこの記事では未来のAngular2を踏まえた上で、Angular1アプリをよりよく構築するためのポイントがふんだんに紹介されていました。

AngularJSモダンプラクティス: http://qiita.com/armorik83/items/5542daed0c408cb9f605

段階的移行の前提条件

段階的にAngular2へ移行できるAngular1アプリは下記の4つの条件を満たしている必要があります!
満たしてなかったら、
がんばって改修しよう!話はそれからだ!
ということになります。:-P

1. Angular Style Guideに沿っている

Angular Style Guide: https://github.com/johnpapa/angular-styleguide
Angular Style Guideとは、そもそもはAngular1アプリを最適に組むためのガイドなのですが、Angular2へ通じる内容も含まれています。

2. Module Loaderを使っている

1つのファイルには1つのコンポーネントだけを書いてModule化し、Module Loaderにより外部参照するES2015のModule仕様に沿うことになります。
ちなみにAngular1のmoduleの概念とはまったく関係がありません。
公式ではSystemJSをデフォルトのModule Loader実装としているようなので、SystemJSを使うのがよいと思います。

3. TypeScriptで書いている

Angular2自体がTypeScriptで実装されているので、アプリもTypeScriptで書けば型情報の恩恵をシームレスに受けることができます!
また、アプリの規模が大きくなるにつれて生のJSで安全なコードを書くのが困難なのは自明です。
TypeScriptコンパイラに頼りましょう。TypeScript採用待ったなしです。

4. Component Directiveを使っている

Component Directiveってなんやねん?というところですが、Directiveで再利用可能なUIコンポーネントを定義して、これの組み合わせてアプリを構築することが推奨されています!

Angular2はコンポーネントベースのフレームワークですが、Angular1でもカスタムDirectiveによりコンポーネントベースなアプリを組むことは可能です!
具体的な取り組み方は下記が参考になります。


AngularJSでつらくならないために抑えておきたい5つのポイント: https://developers.eure.jp/web/advent-calendar-15
その使い方はもう古いかも?AngularJS老化チェック(ディレクティブ篇): http://qiita.com/laco0416/items/edfa917583af4593ad6c


ちなみに弊社のAngular1アプリは
1. Angular Style Guideに沿っている
3. TypeScriptで書いている
4. Directiveコンポーネントを使っている
を満たしていましたが、

2. Module Loaderを使っている
ができておらず、前提条件をクリアできていませんでした。。

TypeScriptの依存関係を内部モジュールで解決していて、namespaceを多用してしまっているので全体的に外部モジュール化する改修が必要となっております。。
まあ、仕方ありません。:-(

移行の流れ

ハイブリッドアプリにする

段階的移行にあたり、まずはAngular1とAngular2が共存するハイブリッドアプリにします。
ハイブリッドアプリを実現するキーマンはAngular2が提供するUpgradeAdapterです。

ハイブリッドアプリにするには

  • 既存のAngular1アプリにAngular2のライブラリ群をロードする
  • Angular2のUpgradeAdapterからAngular1アプリを起動する

をやる必要があります。

ちなみにAngular1ではアプリの起動手段に

  • ng-appディレクティブを起点に起動する
  • angular.bootstrapで起動する

の2種類が用意されていましたが、Angular2ではbootstrap起動しかできなくなっており、ハイブリッドアプリもbootstrap起動する必要があります。

// Angular1
//angular.bootstrap(document.body, ['Pairs.main']);

// Angular2
upgradeAdapter.bootstrap(document.body, ['Pairs.main']);

UpgradeAdapterはグルーとしてアプリ起動だけでなくいろいろな場面で使われます。

Angular2コンポーネントへ変換する

前述の「段階的移行の前提条件」を満たしていれば、Angular1のコンポーネントをAngular2へ変換していくのは比較的容易です。(きっとそうだと信じていますよ。。)


変換の過程で、混在するAngular1コンポーネントとAngular2コンポーネントをインタラクションさせる方法が公式ガイド に下記の6パターンとして紹介されています。

  1. Angular1からAngular2のComponentを使う
  2. Angular2からAngular1のDirectiveを使う
  3. Angular1からAngular2のComponentとtranscludeされたAngular1のDirectiveを使う
  4. Angular2からAngular1のDirectiveとtranscludeされたAngular2のComponentを使う
  5. Angular1のServiceをAngular2のComponentにInjectする
  6. Angular2のServiceをAngular1のDirectiveにInjectする

実践!移行レポート

ここからは実際に手を動かして移行を試してみた内容になります。
移行対象にはproductionで稼働しているガチンコAngular1アプリをご用意しました!


このアプリを使ってAngular2移行の6パターンのうち、
1. Angular1からAngular2のComponentを使う
をやってみます!


ハイブリッドアプリを起動させる

まずはAngular2のライブラリ群をpackage.jsonに追加してnpm installします。

    "angular2": "2.0.0-beta.2",
     "systemjs": "0.19.6",
     "es6-promise": "^3.0.2",
     "es6-shim": "^0.33.3",
     "reflect-metadata": "0.1.2",
     "rxjs": "5.0.0-beta.0",
     "zone.js": "0.5.10"

UpgradeAdapterでAngular1アプリを起動するコードを書きます。

import {upgradeAdapter} from './upgrade_adapter';

upgradeAdapter.bootstrap(document.body, ['Pairs.main']);

UpgradeAdapterの同じインスタンスをハイブリッドアプリの至るところで使うのでモジュール化しておきます。

var upgrade_1 = require('angular2/upgrade');

exports.upgradeAdapter = new upgrade_1.UpgradeAdapter();

TypeScriptのコンパイル設定ファイルtsconfig.jsonを用意して、TypeScriptをコンパイルします。

{
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules"
  ]
}

Angular2のライブラリ群をブラウザにロードしてハイブリッドアプリを起動させます。

<script src="/public/js/pc/es6-shim.min.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/system-polyfills.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/angular2-polyfills.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/system.src.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/Rx.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/angular2.dev.js" type="text/javascript" charset="utf-8"></script>
<script src="/public/js/pc/upgrade.dev.js" type="text/javascript" charset="utf-8"></script>
<script>
  System.config({
    packages: {
      'public/js/pc': {
        format: 'register',
        defaultExtension: 'js'
      }
    }
  });
  System.import('public/js/pc/ng2_bootstrap')
    .then(null, console.error.bind(console));
</script>

ハイブリッドアプリが起動できました!

Angular1 ディレクティブからAngular2 Componentへ変換する

今回は移行を試すことを目的とした超簡易なinquiryHeaderディレクティブを用意してみました。テンプレートのみのシンプルなディレクティブです。

export function inquiryHeader() {
        return {
                scope: {
                },
                bindToController: {
                },
                template: `
                <div class="common_page_header">
                    <span class="page_tile icon_square">お問い合わせ</span>
                </div>
                `,
                controller: function () {
                },
                controllerAs: 'ctrl'
        }
}

inquiryHeaderディレクティブはAngular1のinquiryモジュールに含まれています。

/// <reference path="../../../../typings/angularjs/angular.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-animate.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-cookies.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-resource.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-route.d.ts" />
/// <reference path="../../../../typings/angular-ui/angular-ui-router.d.ts" />

import * as directive from '../directive/inquiryDirective';

export default angular.module('Pairs.inquiry', [
        'ui.router',
        'ngAnimate',
        'ngResource'
    ])
    .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', states])
    .directive('inquiryHeader', [directive.inquiryHeader])
<inquiry-header></inquiry-header>  // 外からこのように使います

それではこのAngular1ディレクティブを移行していきます。
まず、このディレクティブをAngular2のComponentへ変換します。
(Componentの詳細については前回の記事をご参照ください)

import {Component} from 'angular2/core';

@Component({
    selector: 'inquiry-header',
    template: `
    <div class="common_page_header">
        <span class="page_tile icon_square">お問い合わせ</span>
    </div>
    `
})

export class InquiryHeaderComponent {
}

次に、このAngular2のComponentを既存のAngular1から使えるようにするために、UpgradeAdapterのdowngradeNg2Componentを介してComponentをAngular1のディレクティブとして登録します。
これでAngular2のComponentをAngular1のディレクティブとして使えるようになりました!

/// <reference path="../../../../typings/angularjs/angular.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-animate.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-cookies.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-resource.d.ts" />
/// <reference path="../../../../typings/angularjs/angular-route.d.ts" />
/// <reference path="../../../../typings/angular-ui/angular-ui-router.d.ts" />

import {InquiryHeaderComponent} from '../component/inquiry_header_component';
import {upgradeAdapter} from '../../upgrade_adapter';

export default angular.module('Pairs.inquiry', [
        'ui.router',
        'ngAnimate',
        'ngResource'
    ])
    .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', states])
        .directive('inquiryHeader', <any>upgradeAdapter.downgradeNg2Component(InquiryHeaderComponent));  // ダウングレード
<inquiry-header></inquiry-header>  // 外のAngular1から変わりなく使える。実際の中身はAngular2のComponent

まとめ

今回はAngular1からAngular2への移行手順を理解し、簡単なサンプルを試してみました。

移行のハードルは低くはない

「段階的移行の前提条件」は一朝一夕で満たせるものではありません。
もし「段階的移行の前提条件」から乖離が大きい場合は移行を諦めてスクラッチでAngular2アプリを開発する判断も必要でしょう。
当然のことですが、普段からフレームワークの更新情報をキャッチアップして対策を取り続けることが大切だと思いました。

モダンなJS力が求められる

Angular1が登場した当時と現在ではJS周辺の状況は大きく異なっています。
以前はJSの標準仕様(ECMAScript仕様)が足りていなかったため、それをカバーするためのさまざまな仕様やライブラリが乱立していました。
Angular1はこのような背景から、ある意味独特な仕様・実装でした。これはJS自体に明るくないエンジニアであってもなんとなく書ける、という面があり、人気の要因の1つになったと思います。反面、外部ライブラリを使うにはAngular-Wayに合わせてやらなければなりませんでした。


現在は

  • ECMAScript仕様自体が進化して機能的になった
  • BabelやTypeScriptなどによりECMAScript仕様の利用が広がった

などの要因から、ECMAScript仕様に基づいたライブラリ、フレームワークがスタンダードになっています。


もちろんAngular2もモダンな標準仕様に基づいたフレームワークになっています。これは正しいことですが、逆に言えばこれまでのAngular1知識だけでなく、標準仕様に対するしっかりとした理解がなければAngular2をコントロールできなくなっていると言えます。

モダンJS待ったなし!現場からは以上です!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

SwiftでiPhone標準写真アプリのアニメーションを再現してみる

JSON Schema から API 仕様と Go コードを自動で生成する – BOT エントリーの裏側 Part.1