2年以上放置していたIonic Angularプロジェクトを最新にキャッチアップした話
2021年に最後の更新をして以降、ずっと放置してたアプリ「食品表示印刷」を、ようやく最新の環境にキャッチアップした話です。
がんばったとかそういう話ではなく、どういうステップで変更していったということをまとめています。なお、リプレイス以前のpackage.jsonの一部です。見る人がみたら、「おおう」ってなるやつ。
{
"dependencies": {
"@angular/core": "~11.1.0",
"@angular/fire": "^6.1.3",
"@capacitor/core": "^2.4.6",
"@ionic-native/core": "^5.29.0",
"@ionic/angular": "^5.5.4",
"@ionic/storage": "^2.3.1",
...
},
"devDependencies": {
"@angular/cli": "~11.1.0",
"@capacitor/cli": "^2.4.6",
"@ionic/angular-toolkit": "^3.1.0",
"typescript": "~4.0.5",
...
}
}
アップデート作業
1. Angularをアップデート
まず、脳死でAngularをアップデートしていきました。Angularには、めっちゃありがたいことに ng update
コマンドがあり、これを使えばコアの破壊的変更は自動的にコードをアップデートしてくれます。これがなかったら、この作業自体断念していたかも。 ng update
コマンドは、メジャーバージョンをひとつずつあげていく必要があるため、淡々と以下を実行してきました。
% ng update @angular/core@12 @angular/cli@12 --force
% ng update @angular/core@13 @angular/cli@13 --force
% ng update @angular/core@14 @angular/cli@14 --force
% ng update @angular/core@15 @angular/cli@15 --force
% ng update @angular/core @angular/cli @ngrx/store @ionic/angular-toolkit firebase-tools @ionic/angular @angular/fire --force
--force
オプションは、依存関係の解決を無視してくれるオプションです。これがないと、依存関係の解決でエラーが出てしまいます。 最後の行でパッケージ増えてるのは、ついてにAngular16に依存するパッケージをすべて最新版にするためです。これで、Angularのバージョンは最新になりました。やったー!
2. 古くなったライブラリのアップデート
時代を感じますよね。まず、 @ionic-native
が、 @awesome-cordova-plugins
にリネームされたので、すべて置き換えます。
% npm remove @ionic-native/core
% npm install @awesome-cordova-plugins/core
こんな感じで全部を入れ替え。あと、 @ionic/storage
も @ionic/storage-angular
になったのでこれも入れ替えます。これは使い方も大きく変わりましたね。
Angularは昔はtslintを採用してたのですが、eslintになったので、eslintを導入します。
% ng add @angular-eslint/schematics
% npm remove tslint
このコマンドだけですべて自動設定してくれました。
3. Capacitorのアップデート
Capacitor3以降はマイグレーションの自動化コマンド( npx cap migrate
)が用意されてるのですが、Capacitor2はないので、一度すべてを入れ直しました。まず、Capacitor用のiOS/Androidフォルダを削除して、そのあと関連パッケージをすべてアップデートします。
% rm -rf ios android
% npm install @capacitor/cli@4 @capacitor/core@4 @capacitor/android@4 @capacitor/ios@4
// あと、プラグインまわりもアップデート
4. とりあえず動くようにする
特にCapacitor2から3になる時に、プラグインを Plugins
オブジェクトから取得する形から、直接インポートする形に変わったり、コアプラグインがすべて個別パッケージになったりと大きな変更があったので、そこをよしなにします。 ionic serve
してる状態だと、CLIがよしなにエラー箇所を教えてくれるのでそこを片っ端から直していきます。あと、もしかすると angular.json
が古い可能性もあるので、 https://github.com/ionic-team/starters/blob/main/angular-standalone/base/angular.json と見比べておくといいかと思います。
といっても、Gitのコミットログをみてると、2時間あれば終わってますね。私のアプリの範囲では、 rxjs
と @angular/fire
がバージョンアップに伴って破壊的変更があったりで( compat
を消すために)時間がかかってたようです。FireStore使ってなかったら多分もっとはやく終わったんじゃないかな。
5. Ionic 7の記法に変更
で書きましたが、フォームシンタックスが簡素化されたので、最新版の書き方に変更します。あと、Ionic 6で ion-datetime
が大きく変わったので、新しい ion-datetime
に対応しました。
マイグレーション作業
1. Standalone Componentsに移行
以下のコマンドで自動的にStandalone Componentsに移行できます。
% ng generate @angular/core:standalone
ただ、マイグレーションが3段階になってるので、 Convert all components, directives and pipes to standalone
、 Remove unnecessary NgModule classes
、 Bootstrap the application using standalone APIs
を順番に実行します。なので上記コマンドは3回叩きます。
ただ、これですべてのModuleが削除されるわけではなく、Router Moduleだけ残るので、そこは手動で移行する必要があります。といっても、記法がシンプルになるだけです。今までのRouter Moduleから、Route部分を以下のようにひっぺがして
それを main.ts
で読み込むだけです。
これは本当先送りせずにやっておいた方がいいです。30分もかからない上に、今後の開発コストが落ちます。落ちました。
ここまですませると、プロジェクトから ngModule
を一掃できたと思います(私の場合、テスト用の ngModule
だけ残りましたが)。
2. strict: true にする
昔の Ionic Angular って strict: true
じゃなかったんですよね。なので、 このタイミング tsconfig.json
を strict: true
にしてしまいましょう。なんだったら、スターターテンプレートの tsconfig.json
をコピペしてしまって、最新版にする方がはやいです。
3. constructor injectから、property injectに変更
https://zenn.dev/rdlabo/articles/7aa0b566f97c80 を導入して、Lintを走らせただけです。簡単。
Overlay ControllerをComponentから分離
constructor injectからproperty injectにする一番のメリットって、Componentの外でもAngularのServiceを使うことができることだと思っています。そして、私は日頃から感じていたんですが、Overlay Controllerって、Componentにあると冗長じゃないですか? Modal Controller
とか Alert Controller
たちのことです。これをComponentにおいておくと行数がとられるんですが(Alert Controllerだと1つ使うのに20行ぐらい)、そのコンポーネントに依存してるのをいちいちServiceに持っていくのは億劫です。 なので、Overlay Controllerを持つComponentsはすべて同じファイルに OverlayFunc
という関数を持って、それをComponentsのプロパティに置くようにしました。こんな感じ。
Before:
@Component({
selector: 'app-main',
templateUrl: 'main.page.html',
styleUrls: ['main.page.scss'],
standalone: true,
imports: [IonicModule, FormsModule, RouterLink, CommonModule],
})
export class MainPage {
private readonly overlayFunc = overlayFunc();
public readonly platform = inject(Platform);
constructor() {}
public async navigatePrint (event: Event) {
event.stopPropagation();
const modal = await this.modalCtrl.create({
component: SimplePrintPage,
});
await modal.present();
};
async alertDeleteItem(productName: string): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const alert = await this.alertCtrl.create({
header: `${productName}を削除しますか?`,
message: '削除した場合、復元を行うことはできませんので再登録が必要です。',
buttons: [
{
text: 'キャンセル',
handler: () => resolve(false),
},
{
text: '削除',
handler: () => resolve(true),
},
],
});
await alert.present();
});
}
public async shareApp () {
await Share.share({
title: '食品表示印刷',
text: '食品表示ラベルをシンプルに印刷するモバイルアプリ',
url: 'https://foodlabel.rdlabo.jp/',
dialogTitle: '食品表示ラベルをシンプルに印刷するモバイルアプリ「食品表示印刷」',
});
};
}
After:
@Component({
selector: 'app-main',
templateUrl: 'main.page.html',
styleUrls: ['main.page.scss'],
standalone: true,
imports: [IonicModule, FormsModule, RouterLink, CommonModule],
})
export class MainPage {
public readonly platform = inject(Platform);
private readonly overlayFunc = overlayFunc();
constructor() {}
public navigatePrint = (event: Event) => {
event.stopPropagation();
return this.overlayFunc.navigatePrint();
};
public shareApp = () => this.overlayFunc.shareApp();
}
function overlayFunc() {
const [alertCtrl, modalCtrl] = [inject(AlertController), inject(ModalController)];
return {
async shareApp(): Promise<void> {
await Share.share({
title: '食品表示印刷',
text: '食品表示ラベルをシンプルに印刷するモバイルアプリ',
url: 'https://foodlabel.rdlabo.jp/',
dialogTitle: '食品表示ラベルをシンプルに印刷するモバイルアプリ「食品表示印刷」',
});
},
async navigatePrint(): Promise<void> {
const modal = await modalCtrl.create({
component: SimplePrintPage,
});
await modal.present();
},
async alertDeleteItem(productName: string): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const alert = await alertCtrl.create({
header: `${productName}を削除しますか?`,
message: '削除した場合、復元を行うことはできませんので再登録が必要です。',
buttons: [
{
text: 'キャンセル',
handler: () => resolve(false),
},
{
text: '削除',
handler: () => resolve(true),
},
],
});
await alert.present();
});
},
};
}
行数自体は変わらないのですが、Overlay ControllerってPromiseで結果を返すだけのUI操作ライブラリを使ってるだけなので、Componentに置くのは冗長だと思っています。 なので、Overlay ControllerをComponentから分離して、Overlay Controllerを使うComponentはOverlay Controllerを持つ関数をプロパティに持つようにしました。もしかすると将来的に別ファイルにしてより目のつかないところに追い出すかもしれません(笑)
まとめ
ざっと急ぎ足ですが、Angular11/Ionic5のアプリをAngular16/Ionic7にアップデートした方法を紹介しました。これ系のアップデートは後回しにするほど大変になるので、こまめにやっていきたいですね。
それではまた。
Discussion