Firebase + Angular Universal =不可能は可能です

画像

Firebase



迅速なアプリケーション開発に最適なツールです。 ただし、 Firebase



Angular Universal



を使用すると、次の質問が発生する場合があります。









Angular Commerceの開発を開始したとき、主な要件は検索エンジン用のアプリケーションのSEO最適化でした。 これを行うには、検索ロボットが訪問したページのコンテンツを認識できる必要があります。 アプリケーションサーバーは、ユーザーの要求に応じて外部データ(XMLHttpRequest、フェッチ)を返し、結果をHTMLに直接埋め込みます。 しかし、複雑なSPAを扱うときは、非同期の小売リクエストを行います。 したがって、SPAページのソースコードを開くと、次のようなものが表示されます。







 <!DOCTYPE html> <html lang="en"> <head> <title>Home Page</title> <base href="/"> </head> <body> <app-root></app-root> <script type="text/javascript" src="inline.8f218e778038a291ea60.bundle.js"></script> <script type="text/javascript" src="polyfills.bfe9b544d7ff1fc5d078.bundle.js"></script> <script type="text/javascript" src="main.5092b018fbcb0e59195e.bundle.js"></script> </body> </html>
      
      





選択したページにコンテンツがないため、検索エンジンはインデックスを作成するコンテンツを認識しません。そのため、アプリケーションは上位の検索エンジンにアクセスできません。







私たちは、SPAのサーバー側レンダリングのソリューションを探し始め、 Angular Universal



Angular Universal



。 これは、Angularの汎用(同形)JavaScriptサポートです。 つまり、Angularアプリケーションはサーバー上でレンダリングされ、ブラウザーはリクエストに応じてSEOフレンドリーページを受信します。 メタタグをページに追加し、データベースまたはAPIから非同期データを受信すると、これらの情報はすべてレンダリングされたページに表示されます。







しかし、私たちが立ち往生している予期しない問題がありました。 Angular UniversalはWebSocketsに対応していません。 Angular CommerceプロジェクトのバックエンドとしてFirebaseを使用しているため、これは非常に不快な事実でした。 したがって、開発プロセス中に遭遇した落とし穴のいくつかと、それらをどのように解決したかについてお話したいと思います。







クライアントとサーバーのFirebaseを使用する



「クライアントFirebase」パッケージは、一定の接続をサポートする認証用のWebSocket( 'firebase.auth()')を開くため、Universalはサーバーでのレンダリングをいつ終了するかを知りません。 そのため、一部のページが要求されたときにレンダリングプロセスが完了しませんでした。







クライアントモジュールのclient Firebase



パッケージとサーバーモジュールのnode Firebase



を使用してこれを解決しました。 したがって、 app.module.ts



のプロバイダーはapp.module.ts



ようにapp.module.ts



ます。







 import * as firebaseClient from 'firebase'; @NgModule( // declarations, imports and others... providers: [ { provide: 'Firebase', useFactory: firebaseFactory } ], bootstrap: [AppComponent] }) export class AppModule { } export function firebaseFactory() { const config = { apiKey: 'API_KEY', authDomain: 'authDomain', databaseURL: 'databaseURL', projectId: 'projectId', storageBucket: 'storageBucket', messagingSenderId: 'messagingSenderId' }; firebaseClient.initializeApp(config); return firebaseClient; }
      
      





そしてapp.server.module.ts









 import * as firebaseServer from 'firebase-admin'; @NgModule( // declarations, imports and others... providers: [ { provide: 'Firebase', useFactory: firebaseFactory } ], bootstrap: [AppComponent] }) export class AppServerModule { } export function firebaseFactory() { return firebaseServer; }
      
      





firebaseServer



初期化firebaseServer



server.ts



firebaseServer



提供されserver.ts



Firebase Console



から資格情報キーも提供する必要があります(ライブラリのサーバーバージョンとクライアントバージョンは異なる初期化メカニズムを使用します)。







 import * as firebaseServer from 'firebase-admin'; firebaseServer.initializeApp({ credential: firebase.credential.cert('./src/app/key.json'), databaseURL: 'https://pond-store.firebaseio.com' });
      
      





サーバーでは、 Firebase Realtime Database



と一緒にのみnode Firebase



パッケージを使用し、 client Angular



は必要な機能をすべて備えたclient Firebase



パッケージを使用しclient Firebase









Promiseの代わりにObservablesを使用する



Angular Universal



は、 Angular Universal



ではうまく機能しません。 ただし、 Firebase Realtime Database



ライブラリのクエリメソッドはFirebase Realtime Database



返しpromises



。 したがって、これらのメソッド呼び出しをRxJs Observables



RxJs Observables



。 以下は、 Firebase Realtime Database



での単純なクエリの例です。







 const ref = this.firebase.database().ref('products'); Observable .fromPromise(ref.once('value')) .map(data => data.val()) .subscribe( products => { this.products = products; ref.off(); // closing listener of reference });
      
      





observables



ことで、 Angular Universal



はリクエストがいつ完了したかAngular Universal



正しく判断できました。







サーバーデータをブラウザに転送する



Angular Universal



はどのように機能しますか? ユーザーがリクエストを行うと、サーバーはrenderModuleFactory



メソッドを実行します。 renderModuleFactory



メソッドは、サーバー用に収集されたアプリケーションバンドルをrenderModuleFactory



し、レンダリングされたデータを含むHTMLをすぐにページに返します。 その後、ブラウザはAngularのブラウザアセンブリのレンダリングを開始します。 レンダリングが完了すると、 Universal



はサーバーでレンダリングされたコードをブラウザーでレンダリングされた新しいコードに置き換えます。 興味深いことに、サーバーとブラウザーのビルドはほぼ同じ作業を実行します。 これは、データが表示されてから消える( Universal



がサーバーにレンダリングされたコードを削除するため)非同期データベースクエリで顕著であり、ブラウザで新しいクエリが完了すると再び表示されます。







Angular 5



では、 @angular/platform-server



モジュールにTransferStateModule



ます。 このモジュールは、サーバーからブラウザにステータスを転送するのに役立ち、ブラウザでデータを再要求する必要がなくなります。







Angular 4



で作業しているため、 Transfer State



使用せずにサーバーからブラウザにデータを転送する方法に関するソリューションが見つかりました。







renderModuleFactory method



は、オプションのoptions



引数がoptions



ます。







 export declare function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: { document?: string; url?: string; extraProviders?: Provider[]; }): Promise<string>;
      
      





extraProviders



を使用して、サーバーモジュールに追加され、サーバーアプリケーションに存在するプロバイダーを転送できます。 したがって、クライアントアプリケーションに転送するデータを格納するserverStore



オブジェクトを作成しました。







app.engine



app.engine



server.ts



ようになります。







 app.engine('html', (_, options, callback) => { let serverStore = {}; const opts = { document: template, url: options.req.url, extraProviders: [ { provide: 'serverStore', useValue: { set: (data) => { serverStore = data; } } } ] }; renderModuleFactory(AppServerModuleNgFactory, opts) .then( (html: string) => { const position = html.indexOf('</app-root>'); html = [ html.slice(0, position), `<script type="text/javascript">window.store = ${JSON.stringify(serverStore['store'])}</script>`, html.slice(position)] .join(''); callback(null, html); }); });
      
      





最後に、他のスクリプトをインポートする前に、 serverStore



Window.store



に設定します。 ブラウザでのデータ取得はAppModuleで発生します。







 @NgModule({ // declarations, imports and others... providers: [ { provide: 'store', useValue: window['store'] } ] }) export class AppModule {}
      
      





ブラウザでサーバーデータを取得する



コンポーネントで事前にレンダリングされたデータを使用してはどうですか? サーバーでレンダリングすると、データが受信され、リポジトリにインストールされます。 ブラウザアプリケーションでは、リポジトリに必要な情報があるかどうかを確認して取得する必要があります。 以下は、 ngOnInit



メソッドでFirebase Realtime Database



から製品を取得する方法の簡単な例です。







 @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'] }) export class HomeComponent implements OnInit { public products: any; constructor( @Inject(PLATFORM_ID) private platformId, @Inject('Firebase') private firebase, @Optional() @Inject('serverStore' ) private serverStore, @Inject('store') private store ) { } ngOnInit() { if (isPlatformServer(this.platformId)) { const ref = this.firebase.database().ref('products'); const obs = Observable.fromPromise(ref.once('value')) .subscribe( data => { data = data['val'](); if (data) { const products = Object.keys(data).map(key => data[key]); this.products = products; this.serverStore.set(products); } ref.off(); obs.unsubscribe(); }); } else { this.products = this.store; } } }
      
      





serverStore



プロバイダーはserver build



にのみ存在するため、ブラウザーエラーを防ぐために@Optional



デコレーターを使用して宣言されていることに注意してください。







 No provider for serverStore!
      
      





したがって、 Angular Universal



Firebase



で使用するだけでは十分ではありませんが、 Angular Universal



を使用する方法はあります。








All Articles