Angular を v7 から v8 に (あと周辺ツールも合わせて) 更新


はじめに

この記事は Angular Advent Calender 2枚目 の 13日 (金) 分の記事です。
昨日の 12/12 (木) は @noxi515 さんによる デバッグで追いかけるAngularのViewレンダリング でした。

最近とある Angular のプロジェクトで v7 → v8 のバージョンアップをすることがあったので、その時のことを書きます。
もうすぐ v9 が出そうという話もあるので、サポート対象外になる前にそろそろ上げとくか?という人も多いのかもしれません。

とは言いつつ、ただ「Angular のバージョン上げたよ!」だと既にそこら中に記事が散らばっていて面白くないので、周辺ツールと合わせてバージョンメンテナンスするところまでを書こうと思います。

プロジェクトの構成

本題に入る前に、今回私が Angular のバージョンアップを行ったプロジェクトについて説明しておきます。
このプロジェクトではデフォルトの構成からいくつか手を入れていました。主な構成は下記のようになっています。

デフォルトのプロジェクトからの主な変更点でいうとユニットテストのフレームワークを karma + jasmine から jest へ変更していたり、Angular material や NgRx, Storybook などを導入していたりします。
見返してみるとずいぶん古いバージョン達だな…という感じですが、放っておく訳にはいかないのでまるっとまとめてバージョンアップします。

実施したこと

Angular のバージョンアップをするにあたって非常に便利なのが Angular Update Guide (https://update.angular.io/) というサイトです。
下図のように、現バージョンと更新先のバージョンに加えていくつか入力すると、バージョンアップに必要な作業をチェックリストにしてくれます。
App Complexity (アプリケーションの複雑度) はどれにすればいいか迷うところですが、私はとりあえず Advanced にしておいて関係なさそうな手順をスキップするという方法をとりました。
(必要な手順を抜かしてしまうよりは余分な手順を踏むほうがマシ理論)

基本的にはこのリストに従って進めればよいのですが、 ng udpate @angular/cli @angular/core を叩いたところで下記のようなエラーが発生しました。

Package "@storybook/angular" has an incompatible peer dependency to "zone.js" (requires "^0.8.26", would install "0.9.1").

どうやら angular のバージョンを上げると storybook の peerDependencies を満たさなくなってしまうようです。
このように、プロジェクトに追加した周辺ツールやライブラリによっては先に解決しておかなければならない依存関係が発生してしまいます。
そのため一旦寄り道して、今回は先に storybook のバージョンを上げてしまいます。

storybook のバージョンを上げる

基本的には このブログ記事 や、github リポジトリにあるマイグレーションガイド に従って進めていきます。
Angular とは直接関係ないのでさらっと飛ばします。

angular のバージョンアップに戻る

storybook のバージョンが無事に 5 系へ上がり依存関係の課題が解決されたので、 angular のバージョンアップに戻ります。
Upgrade Guide には次のような項目があるのですが、ここで引っかかりました。

We have switched from the native Sass compiler to the JavaScript compiler. To switch back to the native version, install it as a devDependency: npm install node-sass --save-dev.

App Complexity を Advanced まで上げてないと出てこない項目だし、 Angular チームが良かれと思ってスイッチしているわけだからわざわざ元に戻す必要もないよな…と一見、特に何もする必要がなさそうに見えます。
が、実は storybook のほうが node-sass に依存しているのでこのままでは storybook のほうが起動しなくなります。
なので devDependencies に node-sass を追加して、 sass コンパイラを元に戻しておきましょう。

(もちろん、node-sass に依存しているパッケージを使っていない場合はこの手順は不要です。今回はたまたま必要な構成だった。)

あと引っかかりそうな項目としては、 @ViewChild / @ContentChild の仕様変更 などがあります。

https://speakerdeck.com/sayanaka/ng-kyoto-angular-meetup-10
私はこちらのスライドを参考にしながら対処しました。この発表があった meetup に参加して直接話しを聞いていたので、特段引っかかることもなく解決することができました。
(というかここまでの内容だいたいこれでカバーされてるのでは…。)

ngrx のバージョンを上げる

次に ngrx のバージョンを 8 系に上げます。
実際にやるときまで知らなかったのですが、実は ngrx も angular-cli でサポートされているので下のコマンドでいけてしまいました。

$ ng update @ngrx/store

とは言いつつ、Breaking Changes もそれなりにあるので下のリンク先を見ながら対処していきます。
https://ngrx.io/guide/migration/v8

jest のためにプロジェクトの構成を調整する

ユニットテストのフレームワークを Jest に変えているので、こちらも動作するようにメンテしないといけません。
Jest を導入するために jest-preset-angular を使っているのですが、作業した日の時点でこのパッケージの v8.0.0 がリリースされていました。
(メジャーバージョン上がったばっかりは少し怖いけど) この際なので追従しよう!ということでバージョンを上げると、angular-cli v8 で生成されるディレクトリ構成が前提になっていました。

おそらく今後は他のツールも新しいディレクトリ構成を前提にしていくようになると思われるので、合わせることにしました。
変更があったのは tsconfig.{app|spec}.json と tslint.json, そして browserslist の位置です。

v7
root
|- ...
|- src/
|  |- ...
|  |- browserslist
|  |- tsconfig.app.json
|  |- tsconfig.spec.json
|  |- tslint.json
|
|- tsconfig.json
|- tslint.json
|- ...
v8
root
|- ...
|- src/
|
|- browserslist
|- tsconfig.app.json
|- tsconfig.spec.json
|- tsconfig.json
|- tslint.json
|- ...

tslint.json は二つに分かれていたものが一つにまとまったので、 root/src/tslint.json に含まれていた設定を root/tslint.json のほうに含めてしまいます。
tsconfig.app.jsontsconfig.spec.json に関しては、このファイル自身に記述してあるパスや、 angular.json に含まれているこれらのファイルを示すパスがずれてしまうので新しいものに合わせます。

(ちなみにこの記事を書いている途中で、メンテしていたプロジェクトからいつの間にか browserslist が消えていたことに気づきました。 アウトプットすることでこんなことに気付けるとは…。)

ivy を有効にする

ここまでやって満足してしまいそうになるのですが、このままでは巷で話題の (?) Ivy Renderer が有効になりません。
ng new <app> --enable-ivy で作ったプロジェクトならそのままで Ivy が有効になるのですが、v7 からアップデートした場合には自分で設定を書かないといけないのでここでやります。

手順としてはここ (https://angular.io/guide/ivy) に書いてある通りで、tsconfig.app.json に少し追加するだけです。

tsconfig.app.json
{
  // ...
  "angularCompilerOptions": {
    "enableIvy": true
  }
}

この設定だけ入れて ng build してみると、「AoTビルドが高速になったから develop ビルドでもオンにするのが推奨だよ」的なメッセージが表示されました。(上記リンク先の手順にも書いてありますが)
そこで、angular.json を編集して常に AoT ビルドするように変更します。

angular.json
{
  "projects": {
    "my-app-name": {
      "architect": {
        "build": {
          "options": {
            "aot": true, // <-- false になっていれば true に変更する。なければ追加する。
            // ...
          },
          "configurations": {
            "production": {
              "aot": true, // <-- こっちは本番環境用。デフォルトなら true になっていると思うのでそのままにする。
              // ...

AoT コンパイルを使う場合の注意

AoT コンパイルを有効にした状態で ng serve していると、ファイルを編集するたびにビルドが壊れることがありました。
おそらく AoT コンパイルと ng serve の相性が悪いようです。 (issue も見かけた気がするけどどれか分からなくなりました…。)
そこで、私たちは ng serve するときだけ AoT コンパイルをしないようにしています。

package.json
{  
  "scripts": {
    "serve": "ng serve --aot=false"
  }
}

もしかしたら Bazel を導入すれば解決するのだろうか…。と淡い期待を抱いていたりします。

lint の構成を見直す

Angular のバージョンアップのついで (?) に codelyzer のバージョンも 4.5 -> 5.2 に上げてみました。
すると下記のような warning が出てきました。

Could not find implementations for the following rules specified in the configuration:
    use-input-property-decorator
    use-output-property-decorator
    use-host-property-decorator
Try upgrading TSLint and/or ensuring that you have all necessary custom rules installed.
If TSLint was recently upgraded, you may have old rules configured which need to be cleaned up.

codelyzer に上がってた issue によると、メジャーバージョンアップとともにルール名が変わったものがいくつかあるようです。
(あとで見てみるとリリースのところにちゃんと書いてた)

contextual-life-cycle => contextual-lifecycle
no-conflicting-life-cycle-hooks => no-conflicting-lifecycle
no-life-cycle-call => no-lifecycle-call
use-life-cycle-interface => use-lifecycle-interface
decorator-not-allowed => contextual-decorator
enforce-component-selector => use-component-selector
no-output-named-after-standard-event => no-output-native
use-host-property-decorator => no-host-metadata-property
use-input-property-decorator => no-inputs-metadata-property
use-output-property-decorator => no-outputs-metadata-property
no-queries-parameter => no-queries-metadata-property
pipe-impure => no-pipe-impure
use-view-encapsulation => use-component-view-encapsulation
i18n => template-i18n
banana-in-box => template-banana-in-box
no-template-call-expression => template-no-call-expression
templates-no-negated-async => template-no-negated-async
trackBy-function => template-use-track-by-function
no-attribute-parameter-decorator => no-attribute-decorator
max-inline-declarations => component-max-inline-declarations

なので、これに従って tslint.json のルール名を新しいものに修正します。

結果

最終的に、下記のようにバージョンアップすることができました。

よく見る記事だと angular 本体をバージョンアップしてそれでおしまい!かんたん!みたいなイメージなのですが、実プロジェクトだと周辺ツールも面倒見なくてはならず、こちらのほうが面倒くさかったりします。
ここに書いた手順そのものが役に立つことはそんなにないとは思いますが、プロジェクト全体のバージョンメンテナンスの雰囲気が伝わればうれしいです。

おわりに

私事ですが、本日 12/13 (金) は現職の最終出社日です。
この転職が私自身のバージョンアップになればいいなあ…という想いを乗せてこの記事を書きました。
この記事を読んでくれた皆様にも素敵なバージョンアップが訪れますように…。

明日 12/14 (土) は @TomoyukiAota さんです!よろしくおねがいします!