Ionic3から5へ移行した時の備忘録

Ionicのバージョンを3から5に上げた時の記録です。諸事情で最新バージョンの8ではなく5までのアップデートになりますが、調査方法などは役に立つはずです。

筆者はIonicを使い始めて3か月も経っていないのでかなり労力を費やしました。

アップデートするなら1から作り直すべき?

まず公式のアップデートガイドを読むと3から4のアップデートの場合は新しくプロジェクトを作るところから始めようねと書いてあります。

ionic startだとIonicが最新版になるため、Ionicのバージョンを指定したい場合は自分でバージョンアップしていくしかないようです。

※ 特殊な事情がない場合は新しくプロジェクトを作って最新のIonicを使ったほうがよいです。

ちなみに自分で頑張ってバージョンアップするつらみは次のとおりです。

  • ライブラリの同士の対応するバージョンを探すのが大変
  • 本来なら自動で作成されるファイルを手動で作る必要がある
  • あまりにもdeprecatedが多い。そりゃそうだ

アップデート作業の基本方針

先に、具体的な手順ではなく方針や流れについて書いておきます。

Ionicのアップデートをするには、最初に公式ドキュメントの「Updating to 〇〇」の手順に従います。
次に、具体的なコードの書き換えとしてIonicのGitHubリポジトリにあるBREAKING_ARCHIVEを確認します。

v4へのアップデートであれば、併せてlinterv4-migration-tslintも活用できます。

公式ドキュメントに書いていない変更も多数あります。思わず涙が……。この記事ではそういった変更もなるべく拾います。

えげつない量の差分が出るため、こまめにコミットしましょう

ざっくりと、どんな変更が必要なのか

Ionicの3から4への変更概要をまとめておきます(4から5は大したことない)。

  • ライブラリのバージョンアップ
  • ディレクトリ構成の変更
  • 画面遷移のやり方も変更
  • コンポーネントの書き方も一部変更
  • Styleが乱れに乱れるので修正

変更箇所が尋常ではないため、特に苦しんだところのみのピックアップします。

ディレクトリ構成の変更

Ionic@3から4にかけて、ディレクトリ構成がガラッと変わってしまいました。

src/
├── pages/ (src/appに移動)
│ ├── about/
│ ├── home/
├── app/
│ ├── about/
│ ├── home/
│ │
│ ├── app-routing.module.ts
│ │
│ ├── app.html
│ ├── app.component.html
│ │
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── app.scss (src/global.scssになった)
│ │
│ └── main.ts
├── main.ts
├── environments/
├── global.scss
├── polyfills.ts
├── tsconfig.app.json
├── assets/
├── theme/
├── index.html
├── manifest.json
└── service-worker.js
angular.json
.gitignore
ionic.config.json
package.json
tsconfig.json
tslint.json

ディレクトリ構成の変更や新しく生成が必要なファイルは、サンプルリポジトリを探して参考にしましょう

今回筆者が参考にしたリポジトリは次の2つです。

  • ionic-5-angular-9-app
  • starters
    • tabssidemenuの書き方の参考になる
    • 複数のテンプレートが入り混じっているためディレクトリ構成の参考にはしにくい

ionic.config.jsonの書き換え

ionic.config.jsonを書き換えておかないと、バージョンが誤認され@ionic/app-scriptsのインストールを求められてしまいます。

ionic.config.json
"type": "ionic-angular"
"type": "angular"

バージョンの互換性

自分が調べたバージョンの互換性に関するリンクを載せておきます。
インストールしてみて初めて分かるというケースもありますが……。

Angular・Node.js・TypeScript・RxJSのバージョンの互換性はAngular公式ドキュメントVersion compatibilityから確認できます。

CordovaのAndroid関係(Android API LevelGradleAndroid Gradle PluginJavaなど)のバージョン対応表は次のリンクにありました。

CordovaのiOS関係(CocoapodsNodeXCodeなど)のバージョン対応表は次のリンクです。

Angularのアップデート方法

Angularのバージョンアップは、ご丁寧に公式がUpdate Guideを用意してくれています
移行したいバージョンを選択するとコマンドの実行手順や書き換え箇所が書いてあります。淡々と実行していきましょう。

情報源の探し方

その他、バージョンや互換性・破壊的変更などを確認するための情報源を書いておきます。

  • 公式ドキュメント
  • README.md
  • リリースノート(https://github.com/〇〇/〇〇/releasesのやつ)
  • CHANGELOG.md
  • BREAKING CHANGESのファイルやIssue
  • GitHubの検索で、そのリポジトリを検索対象にして漁る
  • npm installできなかったときのエラー文
  • package.jsonの変更をたどる(バージョンの互換性が確実に分かる)

ライブラリ名の変更や移行

どさくさに紛れてめちゃくちゃ変わっていました。

ionic-angularから@ionic/angularへ

ionic-angular@ionic/angularに変わりました。パッケージの入れ替えはもちろん、import.*ionic-angularのような正規表現で検索し、一括置換しておきましょう

のちほど紹介しますがルーティング関係も変わったため、importを書き換えてもまだ@ionic/angular関係の変更は続きます。

また、一部のionic-angularのクラスは@ionic/angularには移行せず、@ionic-native/keyboardのような別のパッケージに移っています。

次のメッセージがそれを知らずに実行した時のエラー文です。

[ng] src/app/app.component.ts:2:20 - error TS2305: Module '"../../node_modules/@ionic/angular/dist/core"' has no exported member 'Keyboard'.
[ng] 2 import { Platform, Keyboard } from '@ionic/angular';
[ng] ~~~~~~~~

@ionic/storageは必要か?

現在、@ionic/storageのAngular部分は@ionic/storage-angularへ移行しています。
しかしプロジェクトが使っているTypeScriptが3.8未満だと次のようなエラーが出ます。

[ng] ERROR in node_modules/@ionic/storage-angular/index.d.ts:1:13 - error TS1005: '=' expected.
[ng] 1 import type { ModuleWithProviders } from '@angular/core';
[ng] ~
[ng] node_modules/@ionic/storage-angular/index.d.ts:1:42 - error TS1005: ';' expected.
[ng] 1 import type { ModuleWithProviders } from '@angular/core';

そんなわけで筆者の環境では@ionic/storage-angularには移行できませんでした。
リリースノートを漁り、2.2.0を使いました。

npm install @ionic/[email protected]

Cordovaプラグインもチェックする

メンテナンスされていないプラグインがあれば移行しましょう。

筆者の環境ではフォーク版のフォーク版を使っていました。このプラグインはもう動かず、issueを見るとフォーク版のフォーク版のフォーク版のフォーク版を勧められていましたが、さすがに良くないので別のプラグインに移行しました。
メンテナンスって大変ですよね。

コードの修正

一括置換では対応しきれない箇所が結構ありました。頑張りましょう。

ここでは、苦しんだ箇所をピックアップしておきます。

IonicErrorHandlerの削除

Ionic@4ではIonicErrorHandlerは不要になったようなので削除します。独自に実装したい人は、[email protected]の実装を見てみるといいかも。

参考:v4 Breaking Change for IonicErrorHandler · Issue #14651 · ionic-team/ionic-framework

一部のライフサイクルイベントの廃止

IonicのライフサイクルイベントionViewDidLoadionViewCanLeaveionViewCanEnterが廃止されました。

Ionicの公式ドキュメントAngularの公式ドキュメントの図や一覧を見て、別のライフサイクルイベントに処理を移しておきましょう。

廃止されたライフサイクルイベントを移行し忘れるとかなり厄介です。
「単純に実行されず、エラーも出ない」状態になります。忘れずに移行しましょう。

ルーティングの大転換

Ionic@3まではNavControllerを使っていましたが、4からは各フレームワークのルーティングを使うように方針が変わりました。
一部のNavControllerは使えますが、このタイミングですべて移行してしまう方がよいです。

ページ一覧から遷移を考える

筆者は各ページの遷移やルーティングを考えるにあたり、次のような手順でページ一覧を出してから考えました。

Terminal window
ls -1 src/app/pages
home/
login/
foo-detail/
foo-edit/
foo-list/
tabs/
...

この出力結果からチェックリストを作っておくと、あとで紹介する「Styleの変更」のチェックなどでヌケモレが減ります。

あとあと、「使われてないけど削除されてなかったページ」の発見もあるかもしれません。

具体的なメソッドの移行

ざっくりとですが書き換えの例を書いておきます。

HTML関係

ルーティングをつかうのに必要なタグをhtmlに記載します。

src/index.htmlのbody部分
<body>
<app-root></app-root>
</body>
src/app/app.component.html
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>

Angular Routerの<router-outlet></router-outlet>は上記のion-router-outletに組み込まれているようです。

ルーティングの設定

ルーティングの例です。

app-routing.module.tsの例(クリックで開きます)
src/app/app-routing.module.ts
import { NgModule } from "@angular/core";
import { RouterModule, Routes, PreloadAllModules } from "@angular/router";
const routes: Routes = [
{
path: "top",
loadChildren: () =>
import("./pages/top/top.module").then((m) => m.TopPageModule),
},
{
path: "tabs",
loadChildren: () =>
import("./pages/tabs/tabs.module").then((m) => m.TabsPageModule),
},
{
path: "",
redirectTo: "top",
pathMatch: "full",
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
src/app/app.module.ts
import { NgModule } from "@angular/core";
import { RouteReuseStrategy } from "@angular/router";
import { IonicModule, IonicRouteStrategy } from "@ionic/angular";
import { AppComponent } from "./app.component";
import { AppRoutingModule } from "./app-routing.module";
@NgModule({
declarations: [AppComponent],
imports: [
// ...
IonicModule.forRoot(),
AppRoutingModule,
],
bootstrap: [AppComponent],
providers: [
// ...
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
],
})
export class AppModule {}

src/app/app.module.tsにて、ルーティングとして切り分けたモジュールをimportsの中に入れておきます。

ついでにタブUIを採用しているケースでのルーティングの例も記載しておきます。

tabs-routing.module.tsの例(クリックで開きます)
src/app/pages/tabs/tabs-routing.module.ts
import { RouterModule, Routes } from "@angular/router";
import { TabsPage } from "./tabs";
import { NgModule } from "@angular/core";
const routes: Routes = [
{
path: "",
component: TabsPage,
children: [
{
path: "home",
children: [
{
path: "",
loadChildren: () =>
import("../home/home.module").then((m) => m.HomePageModule),
},
],
},
{
path: "calendar",
children: [
{
path: "",
loadChildren: () =>
import("../calendar/calendar.module").then(
(m) => m.CalendarPageModule,
),
},
],
},
{
path: "foo-list",
children: [
{
path: "",
loadChildren: () =>
import("../foo-list/foo-list.module").then(
(m) => m.FooListPageModule,
),
},
],
},
{
path: "",
redirectTo: "home",
pathMatch: "full",
},
],
},
{
path: "",
redirectTo: "home",
pathMatch: "full",
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class TabsPageRoutingModule {}
src/app/pages/tabs/tabs.module.ts
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { IonicModule } from "@ionic/angular";
import { TabsPageRoutingModule } from "./tabs-routing.module";
import { TabsPage } from "./tabs";
@NgModule({
imports: [IonicModule, CommonModule, TabsPageRoutingModule],
declarations: [TabsPage],
})
export class TabsPageModule {}

loadChildrenで指定するのは〇〇Moduleです。〇〇RoutingModuleではありません。

クエリパラメータを使いたい場合はpathbar/:idのように:を使ってパラメータであること表現しておきます。

循環参照に注意!

循環参照しないように気を付けましょう
たとえば、tabs-routing.module.tscomponent: TabsPageの代わりに次のように書くと循環参照になってしまいます。

tabs-routing.module.ts
loadChildren: import("../tabs/tabs.module").then((m) => m.TabsPageModule)

具体的には次のとおりです。

  1. TabsPageModuleTabsPageRoutingModuleを読み込む
  2. TabsPageRoutingModuleTabsPageModuleを読み込む

ここ間違えると、アプリ起動時の画面が真っ白になります。

遷移前の画面

遷移前の画面の例を載せておきます。

import { NavController } from '@ionic/angular';
import { Location } from '@angular/common';
import { Router } from '@angular/router';
// ...
constructor(
private navCtrl: NavController,
private router: Router,
private location: Location,
) {}
back() {
this.navCtrl.pop();
this.location.back();
}
moveFooDetails(id: number) {
this.navCtrl.push("FooDetailsPage", {
id: id
this.router.navigateByUrl("/tabs/foo-list/foo-detail", {
queryParams: { id },
});
}
onClick() {
this.navCtrl.push("homePage");
this.router.navigateByUrl("/tabs/home");
}
// ...

基本的にはrouter.navigateByUrlで移動し、戻るボタンのような「履歴を戻る」処理ではlocation.backを呼びます。

App.getRootNavApp.navPopなどを使っていた場合はrouter.navigateByUrlなどで明示的に遷移先を決めましょう。

遷移後の画面でパラメータの取得

次のようにActivatedRouteを使ってクエリパラメータを取得できます。

import { ActivatedRoute, Router } from "@angular/router";
// ...
constructor(
private router: Router,
private activatedRoute: ActivatedRoute,
) {}
ngOnInit() {
this.activatedRoute.paramMap.subscribe((params) => {
const id = params.get("id");
});
}
// ...

スタイル崩れに立ち向かう

Ionic@3から4のアップデートで、スタイルの指定方法もガラッと変わってしまいました。

グローバルなスタイルを書く場所が変更

今までsrc/app/app.scssに書いていた内容はsrc/global.scssに書きます。

scssでのタグのラップが不要になった

page-fooのようにページを表すセレクタの囲いは外します。単純ですが差分の出る行が尋常ではないのでこまめにコミットしましょうね。

./foo.scss
page-foo {
.piyo {
// ...
}
}
.piyo {
// ...
}

併せて、対応する@ComponentstyleUrlsを追加します。

./foo.ts
@Component({
selector: "page-foo",
templateUrl: "foo.html",
styleUrls: ["foo.scss"],
})
export class FooPage {

この2つの変更を忘れてしまうとスタイルが適用されないので注意です。

カプセル化によるスタイルの変化

Ionic@4から、一部のコンポーネントがShadow DOMでカプセル化されています。
よって、一部のスタイルの適用方法が通常のプロパティからCSS変数や::part()での指定に変わりました

そもそもShadow DOMって何?」という人のために参考になりそうな公式ドキュメントをまとめておきました。

Ionicのコンポーネントごとに用意されているCSS変数や::part()は、各コンポーネントのドキュメントを読みましょう。

たとえば、Ionic@4のion-contentの例は次のとおりです。

  • --background: 背景の変更
  • --color: 色の変更

余計な空白や傍線がある!

余計な空白はだいたいion-contention-itemのpaddingのCSS変数によるものです。今までのスタイルと照らし合わせて適宜調整しましょう。

ion-content {
--padding-top: 0;
--padding-bottom: 0;
--padding-start: 0;
--padding-end: 0;
}

ion-itemにいたっては下部に傍線も加わっています。不要であれば--inner-border-widthを0にしましょう。

ion-item {
--inner-border-width: 0;
--padding-top: 0;
--padding-bottom: 0;
--padding-start: 0;
--padding-end: 0;
--inner-padding-start: 0;
--inner-padding-end: 0;
}

他にも--inner-padding-startなどの愉快な仲間たちが続々と登場しています。

padding関係だとclass="ion-no-padding"のようなCSS Utilitiesもあります。

レイアウトが完全に崩れている

レイアウトが完全に崩れている場合、Ionic側でdisplayプロパティをflexに変更している可能性が高いです。
たとえばion-slideflexになっていました。この場合はdisplay: blockを指定するだけで上書きできます。


以上、Ionic@3から@5に上げた時の記録でした。3から4の変更は大きかったですが、4から5はそこまで大変ではなかったです。

書き換え箇所が大量にありましたが、Neovimのマクロやプラグインを使ったらそこそこ楽にできました。感謝です。
そのプラグインというのは以前紹介したquicker.nvimです。紹介記事のリンクも貼っておくので興味があればどうぞ。
複数ファイルを一括で書き込めるquicker.nvimの使い方 | eiji.page