Firebase
迅速なアプリケーション開発に最適なツールです。 ただし、 Firebase
とAngular Universal
を使用すると、次の質問が発生する場合があります。
- ユーザーのブラウザで使用するFirebaseパッケージとサーバーで使用するFirebaseパッケージはどれですか?
- 非同期操作にはどのメカニズムを使用する必要がありますか?
- リクエストの重複を避けて、サーバーからブラウザにデータを転送する方法は?
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
を使用する方法はあります。