ngUpgrade を使って AngularJS から Angular に移行
ソフトウェアエンジニアの花岡です。今回は弊社で 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
ディレクトリに閉じ込めています。
- フロントエンド
- TypeScript
- Angular
- Angular CLI
ng1
- AngularJS
- angular-translate
- i18n
- npm
- Grunt
- バックエンド
- Python
- Flask
- HTML ファイルを生成
- Web API
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 をお任せできるエンジニアを募集しています。興味のある方は是非こちらからご応募ください。
その他の記事
Other Articles
関連職種
Recruit