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.jsangular.json.gitignoreionic.config.jsonpackage.jsontsconfig.jsontslint.json
ディレクトリ構成の変更や新しく生成が必要なファイルは、サンプルリポジトリを探して参考にしましょう。
今回筆者が参考にしたリポジトリは次の2つです。
- ionic-5-angular-9-app
- starters
tabs
やsidemenu
の書き方の参考になる- 複数のテンプレートが入り混じっているためディレクトリ構成の参考にはしにくい
ionic.config.jsonの書き換え
ionic.config.jsonを書き換えておかないと、バージョンが誤認され@ionic/app-scripts
のインストールを求められてしまいます。
"type": "ionic-angular""type": "angular"
バージョンの互換性
自分が調べたバージョンの互換性に関するリンクを載せておきます。
インストールしてみて初めて分かるというケースもありますが……。
Angular・Node.js・TypeScript・RxJSのバージョンの互換性はAngular公式ドキュメントVersion compatibilityから確認できます。
CordovaのAndroid関係(Android API Level
・Gradle
・Android Gradle Plugin
・Java
など)のバージョン対応表は次のリンクにありました。
CordovaのiOS関係(Cocoapods
、Node
、XCode
など)のバージョン対応表は次のリンクです。
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のライフサイクルイベントionViewDidLoad
、ionViewCanLeave
、ionViewCanEnter
が廃止されました。
Ionicの公式ドキュメントやAngularの公式ドキュメントの図や一覧を見て、別のライフサイクルイベントに処理を移しておきましょう。
廃止されたライフサイクルイベントを移行し忘れるとかなり厄介です。
「単純に実行されず、エラーも出ない」状態になります。忘れずに移行しましょう。
ルーティングの大転換
Ionic@3まではNavControllerを使っていましたが、4からは各フレームワークのルーティングを使うように方針が変わりました。
一部のNavControllerは使えますが、このタイミングですべて移行してしまう方がよいです。
ページ一覧から遷移を考える
筆者は各ページの遷移やルーティングを考えるにあたり、次のような手順でページ一覧を出してから考えました。
ls -1 src/app/pages
home/login/foo-detail/foo-edit/foo-list/tabs/...
この出力結果からチェックリストを作っておくと、あとで紹介する「Styleの変更」のチェックなどでヌケモレが減ります。
あとあと、「使われてないけど削除されてなかったページ」の発見もあるかもしれません。
具体的なメソッドの移行
ざっくりとですが書き換えの例を書いておきます。
HTML関係
ルーティングをつかうのに必要なタグをhtmlに記載します。
<body> <app-root></app-root></body>
<ion-app> <ion-router-outlet></ion-router-outlet></ion-app>
Angular Routerの<router-outlet></router-outlet>
は上記のion-router-outlet
に組み込まれているようです。
ルーティングの設定
ルーティングの例です。
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 {}
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の例(クリックで開きます)
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 {}
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
ではありません。
クエリパラメータを使いたい場合はpath
にbar/:id
のように:
を使ってパラメータであること表現しておきます。
循環参照に注意!
循環参照しないように気を付けましょう。
たとえば、tabs-routing.module.ts
のcomponent: TabsPage
の代わりに次のように書くと循環参照になってしまいます。
loadChildren: import("../tabs/tabs.module").then((m) => m.TabsPageModule)
具体的には次のとおりです。
TabsPageModule
がTabsPageRoutingModule
を読み込むTabsPageRoutingModule
がTabsPageModule
を読み込む
ここ間違えると、アプリ起動時の画面が真っ白になります。
遷移前の画面
遷移前の画面の例を載せておきます。
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.getRootNav
やApp.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
のようにページを表すセレクタの囲いは外します。単純ですが差分の出る行が尋常ではないのでこまめにコミットしましょうね。
page-foo { .piyo { // ... }}.piyo { // ...}
併せて、対応する@Component
にstyleUrls
を追加します。
@Component({ selector: "page-foo", templateUrl: "foo.html", styleUrls: ["foo.scss"],})export class FooPage {
この2つの変更を忘れてしまうとスタイルが適用されないので注意です。
カプセル化によるスタイルの変化
Ionic@4から、一部のコンポーネントがShadow DOMでカプセル化されています。
よって、一部のスタイルの適用方法が通常のプロパティからCSS変数や::part()での指定
に変わりました。
「そもそもShadow DOMって何?」という人のために参考になりそうな公式ドキュメントをまとめておきました。
- ウェブコンポーネント - Web API | MDN
- シャドウ DOM の使用 - Web API | MDN
- CSS Variables | CSS Custom Properties for Variables & Components
- CSS Shadow Parts - Style CSS Properties Inside of A Shadow Tree
Ionicのコンポーネントごとに用意されているCSS変数や::part()
は、各コンポーネントのドキュメントを読みましょう。
たとえば、Ionic@4のion-contentの例は次のとおりです。
--background
: 背景の変更--color
: 色の変更
余計な空白や傍線がある!
余計な空白はだいたいion-content
やion-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-slide
がflex
になっていました。この場合はdisplay: block
を指定するだけで上書きできます。
以上、Ionic@3から@5に上げた時の記録でした。3から4の変更は大きかったですが、4から5はそこまで大変ではなかったです。
書き換え箇所が大量にありましたが、Neovimのマクロやプラグインを使ったらそこそこ楽にできました。感謝です。
そのプラグインというのは以前紹介したquicker.nvimです。紹介記事のリンクも貼っておくので興味があればどうぞ。
複数ファイルを一括で書き込めるquicker.nvimの使い方 | eiji.page