ngUpgrade を使って AngularJS から Angular に移行
2018/03/23
  • このエントリーをはてなブックマークに追加

ソフトウェアエンジニアの花岡です。今回は弊社で ngUpgrade を使って AngularJS から Angular に移行した際の実例についてご紹介したいと思います。

本記事は前回の記事の背景の

既存の AngularJS のコードベースをなるべくそのままインポートして動作させる

に相当します。

バージョン

TypeScript、AngularJS、Angular、Angular CLI のバージョンは以下の通りです。

  • TypeScript 2.3
  • AngularJS 1.5
  • Angular version 4.4
  • Angular CLI 1.5

移行した後で TypeScript、Angular、Angular CLI はそれぞれ 2.6、5.2、1.7 に更新しています。

移行前・移行後の構成

移行前

以降前の構成は以下の通りです。

  • フロントエンド
    • TypeScript、CoffeeScript
    • AngularJS
    • npm、Bower
    • Grunt
  • バックエンド
    • Python
    • Flask
      • HTML ファイルや AngularJS テンプレートを生成
      • Web API
      • i18n(Jinja2)

フロントエンドはもともと CoffeeScript で書かれていて大部分が TypeScript に書き換えられている状態でした。JavaScript ライブラリなどの管理は Bower が使われていて Grunt でビルドしていました。一部 npm でインストールされるパッケージもありました。

バックエンドは Flask を使っていて、HTML ファイルだけでなく AngularJS テンプレートも Jinja2 の i18n 機能で多言語化対応して生成していました。Flask は Python の Web フレームワークで Jinja2 は Flask のテンプレートエンジンです。

移行後

移行後の構成は以下の通りです。AngularJS アプリのコードベースは ng1 ディレクトリに閉じ込めています。

Bower はなくなりパッケージは npm だけでインストールされます。ビルドは Angular CLI に委譲します。ディレクトリ構成も Angular CLI のものをそのまま使っています。

i18n は Jinja2 から暫定的に AngularJS の angular-translate に移行しました(理由は後述)。Grunt はそのメッセージファイルの生成のためだけに使っています。

Flask アプリはもはや AngularJS テンプレートを生成しません。AngularJS テンプレートは ng1 ディレクトリから静的な HTML ファイルとして利用されます。

移行の進め方

  • なるべく素早く移行したい
  • 移行中も既存のコードベースをメンテナンスしたい
  • もしかしたら移行できないかも

という理由から既存のコードベースを大きく変更することなく進めていくことにしました。具体的には Upgrading from AngularJS の Preparation はスキップしました。

そして移行前のコードベースの frontend の隣に Angular CLI で frontend2 というプロジェクトを生成して、frontend2 から frontend のファイルを .angular-cli.json で参照するようにしました。

.
|-- frontend/
|   |-- README.md
|   |-- bower_components/
|   |-- node_modules/
|   |-- ...
|   `-- tslint.json
|-- frontend2/
|   |-- README.md
|   |-- node_modules
|   |-- ...
|   `-- tslint.json
...

たとえば Bower のパッケージも .angular-cli.json で参照していました。そうすることで frontend の方では Angular への移行を意識することなくパッケージの更新を含むメンテナンスが可能でした。Bower から npm への移行は Angular への移行の最終段階で行いました。

ただしこのやり方が可能なのは Angular CLI 1.5.3 までで 1.5.4 からはセキュリティのためプロジェクトの外のファイルを .angular-cli.json で参照できなくなったため、移行が終わるまでは Angular CLI 1.5.3 を使っていました。

移行ポイント

Jinja2 テンプレート

移行前のコードベースでは templateUrl にバックエンドの URL を指定していたのですがビルドできなくなりました。たとえば

angular.module('myModule').component('myComponent', {
  controller: MyComponentController,
  templateUrl: '/url/for/my-component.template.html',
})

をビルドすると

Module not found: Error: Can't resolve './/url/for/my-component.template.html

というエラーになります。

angular.module('myModule').component('myComponent', {
  controller: MyComponentController,
  templateUrl: './my-component.template.html',
})

のようにバックエンドの URL ではなく、そのソースコードからのファイルパスを指定するとビルドできるようになります。

つまり Anguar への移行では AngularJS テンプレートは静的な HTML ファイルである必要があり、Jinja2 テンプレートから Jinja2 構文を取り除いて AngularJS テンプレートに変換しなければなりませんでした。

i18n

Jinja2 構文には i18n が含まれるため i18n 機能の移行も必要になります。念のため Angular の i18n を AngularJS のテンプレートに適用しようとしてみましたができませんでした。

そのため今回は暫定的に angular-translate に移行し、コンポーネントなどを Angular に移行するタイミングで Angular の i18n に移行しようと考えています。

script タグでの text/ng-template によるテンプレート

script タグでの text/ng-template によるテンプレートの定義も templateUrl で Module not found エラーとなるため別ファイルに抽出する必要がありました。

UI Router でバックエンドの URL

UI Router でもバックエンドの URL を指定すると Module not found エラーとなります。

バックエンドのロジックが複雑で AngularJS テンプレートへの変換が難しいところは

const createTemplateProvider = (url: string) => {
  return ($templateCache: angular.ITemplateCacheService, $http: angular.IHttpService) => {
    'ngInject';
    return $http.get(url, { cache: $templateCache }).then(response => response.data);
  };
};

$stateProvider.state('root.dashboard', {
  url: '/',
  templateProvider: createTemplateProvider('/dashboard'),
  // ...
})

のように UI Router の templateProvider で指定することにしました。

CoffeeScript

一部残っていた CoffeeScript の TypeScript への移行は Decaffeinate を使い、ES 2015 に変換した結果を微調整しました。

AngularJS の dependency annotation

前回の記事でご紹介した ts-ng-annotate を使って dependency annotation を追加しました。

テーマ機能

Angular への移行と直接は関係ないのですが、今回はテーマに応じて CSS ファイルを切り替える簡単なテーマ機能の移行も必要でした。CSS ファイルの生成自体は .angular-cli.json

{
  ...
  "apps": [
    {
      ...
      "styles": [
        { "input": "themes/black.scss", "output": "theme-black", "lazy": true },
        { "input": "themes/dark-gray.scss", "output": "theme-dark-gray", "lazy": true },
        ...
      ]
      ...
    }
  ]
}

と書いて npm run build -- --extract-css すると、dist ディレクトリに CSS ファイルが生成されます。

開発ビルド時には theme-black.bundle.css というファイルが生成されますが、プロダクションビルド時には --output-hashing フラグが all となり theme-black.184f93a894912f969bde.bundle.css のようなハッシュ値つきのファイルが生成されます。

Flask アプリでは、これらを透過的に扱えるように以下のようなエンドポイントを定義しました。

def build_theme_to_url_map():
    theme_to_url = {
        theme: '/theme-' + theme + '.bundle.css'
        for theme in THEMES
    }
    for filename in glob.glob(THEME_DIR + '/theme-*.bundle.css'):
        basename = os.path.basename(filename)
        theme = basename.split('.', 1)[0][len('theme-'):]
        theme_to_url[theme] = '/' + basename
    return theme_to_url


_theme_to_url_map = build_theme_to_url_map()


@app.route('/theme.css')
def theme():
    theme = get_theme()
    return redirect(_theme_to_url_map[theme])

npm start 時には実際の CSS ファイルが生成されず glob でマッチしないため、npm start 用のハッシュ値なしバージョンを theme_to_url の初期化式で定義しています。

最後に

これで

既存の AngularJS のコードベースを段階的に Angular に移行していく

ことができるようになりました。

弊社では Angular に興味のある/Angular をお任せできるエンジニアを募集しています。興味のある方は是非こちらからご応募ください。