Ionicアプリを作る際
Reactではなく
Angularを採用したわけ
株式会社TRIVE GROUP
くまぽん(なかの)
中野 伸吾
もくじ
- 自己紹介
- アーキ選定の話
- Angular Universalでハマったこと
- SSRモードでLocalStorageを使う
- RxJSとPromiseの両立
自己紹介
くまぽん(なかの) 中野伸吾
アーキ選定の話
アーキ判断基準
- 採用難易度
→ 今後のエンジニアの市場的にシェアあるかどうか
- 開発コスト
React/Ionic or Angular/Ionic(SSR) < React/Next
- SEO観点
React/Next or Angular/Ionic(SSR) > React/Ionic
- パフォーマンス
React/Next > Angular/Ionic or React/React (?)
- SHOPPLが実現したい世界観
※ デザインの雰囲気+遷移系アニメーションなど、UIコンポーネントの得意/不得意を含めて方針決め
→ 結論、Ionicは使いたい
余談
検証の一環で、React/Next.jsのアプリケーションに Ionic@coreの導入を試した
→ ルーティング設定周りで沼にハマりそうになったので、早めに検証を打ち止めた
最初の有力候補は
React/Ionicでした
SEOリスクを軽減したい
-
SPA × Dynamic Rendering
-
SSR
※Nuxt/React or Ionic/Angular (+ Angular Universal)
選択肢は、以下の2択
Dynamic Rendering
検証結果、うまくいかず断念
Ionic系のCustom Elementsの変換が上手くいかない
Rendertronを使って検証
結論
Ionicは利用したい+SPAは
(SEOリスクを回避するのが)厳しい
↓
Ionic Angular (+Angular Universal)
の採用を決めた
Angular Universalでハマったこと
windowオブジェクトは使えない
- 例:「window.location.hostname」などは使えない
const routes: Routes = [
{
path: '',
loadChildren: () => {
if (typeof window !== 'undefined' && isPlatform('capacitor')) {
// アプリ(下タブあり)
return import('./tabs/tabs.module').then((m) => m.TabsPageModule);
} else {
// Web(下タブなし)
return import('./about/about.module').then((m) => m.AboutPageModule);
}
},
},
Capacitorビルドした際にAuth0が上手く動かなかった
- iFrame周りの挙動が怪しくて動かなかった
- おそらくAngular Universal周りが悪さをしていた?
↓
結果的にFirebase Authを採用した
SSRモードの罠 1
- Ionicのライフサイクルメソッド(ionViewWillEnterなど)は、SSRモードでは上手く動かない
※ ngOnInitは問題なく動く
import { Component, Inject, OnInit } from '@angular/core';
import { Meta } from '@angular/platform-browser';
@Component({
selector: 'app-detail',
templateUrl: './detail.page.html',
styleUrls: ['./detail.page.scss'],
})
export class DetailPage implements OnInit {
constructor(
public meta: Meta,
@Inject(Localstorage) private localStorage
) {}
ngOnInit() {
// これはOK
this.meta.updateTag({
property: 'og:url',
content: `${environment.frontUrl}${this.router.url}`,
});
}
ionViewWillEnter() {
// こちらはNG
this.meta.updateTag({
property: 'og:url',
content: `${environment.frontUrl}${this.router.url}`,
});
}
}
SSRモードの罠 2
本番ビルドするとIonicがMaterial Designモード固定になる
(iOSのUserAgentであっても、無条件でmdモードになる)
公式Issueでも取り上げられている
既知の伸び代
SSRモードでLocalStorageを使う
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
static isBrowser = new BehaviorSubject<boolean>(null);
constructor(@Inject(PLATFORM_ID) private platformId: any) {
AppComponent.isBrowser.next(isPlatformBrowser(platformId));
}
}
src/app/app.component.ts
ブラウザ or サーバ描画の判定
LocalStorageをSSRで使うための抽象化クラスを用意
import { Injectable } from '@angular/core';
import { AppComponent } from '../app/app.component';
class LocalStorage implements Storage {
[name: string]: any;
readonly length: number;
clear(): void {}
getItem(key: string): string | null {
return undefined;
}
key(index: number): string | null {
return undefined;
}
removeItem(key: string): void {}
setItem(key: string, value: string): void {}
}
@Injectable({
providedIn: 'root',
})
export class Localstorage implements Storage {
private storage: Storage;
constructor() {
this.storage = new LocalStorage();
AppComponent.isBrowser.subscribe((isBrowser) => {
if (isBrowser) {
this.storage = localStorage;
}
});
}
[name: string]: any;
length: number;
clear(): void {
this.storage.clear();
}
getItem(key: string): string | null {
return this.storage.getItem(key);
}
key(index: number): string | null {
return this.storage.key(index);
}
removeItem(key: string): void {
return this.storage.removeItem(key);
}
setItem(key: string, value: string): void {
return this.storage.setItem(key, value);
}
}
src/local-storage.ts
抽象化した
LocalStorageクラスの利用
import { Injectable, Inject } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import { FirebaseUser } from '../../model/user';
import { Localstorage } from '../local-storage';
import * as firebase from 'firebase/app';
import 'firebase/auth';
@Injectable({
providedIn: 'root',
})
export class AuthenticationService {
constructor(
public ngFireAuth: AngularFireAuth,
@Inject(Localstorage) private localStorage
) {}
getCurrentUser() {
this.ngFireAuth.authState.subscribe((firebaseUser: FirebaseUser) => {
this.localStorage.setItem('user', JSON.stringify(firebaseUser));
});
}
}
src/app/service/authentication.service.ts ※ 一部抜粋
参考
RxJSとPromiseの両立
Promise処理と共存の
ベストプラクティスがわからない
async getItem(itemId: number): Promise<Observable<Item>> {
let params;
await this.authService.initFirebaseAuth();
if (firebase.auth().currentUser) {
const token = await firebase.auth().currentUser.getIdToken(true);
params = new HttpParams().set('token', token);
}
return this.http
.get<Item>(`${this.baseUrl}/items/${itemId}`, { params })
.pipe(
map((item) => {
return item;
})
);
}
例
最後になりますが
仲間を探しています!
【こんな方にぴったり】
◆ 創業メンバーとガッツリ働いてみたい方
◆ 事業視点と技術向上を両立したい方
◆ これから伸びていく市場で働いてみたい方
◆ 会社や事業の成長を肌で感じたい方
◆ この世にまだないプロダクトを生み出したい方
【具体的な仕事内容】
◆ 自社サービス/新規事業のグロースに向けた開発業務(フロントエンド・バックエンド)
【開発環境】
・Ruby (Ruby on Rails)
・Ionic/Angular
・MySQL
・Docker
・Firebase(Auth/Analytics等)
・GCP (App Engine/Cloud SQL/Cloud Function等)
・GitHub Actions (自動テスト/自動デプロイ等)
気になる方は、私のTwitter宛に
DMお待ちしてます!!
ご静聴
ありがとうございました
Ionicアプリを作る際、ReactではなくAngularを採用したわけ
By shingo-nakano
Ionicアプリを作る際、ReactではなくAngularを採用したわけ
- 405