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

画像
Firebase迅速なアプリケーション開発に最適なツールです。 ただし、 FirebaseAngular 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 UniversalAngular 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.tsFirebase 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 ObservablesRxJs 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.engineapp.engineserver.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); }); }); 

最後に、他のスクリプトをインポートする前に、 serverStoreWindow.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 UniversalFirebaseで使用するだけでは十分ではありませんが、 Angular Universalを使用する方法はあります。



Source: https://habr.com/ru/post/J341044/


All Articles