Firebase
great tool for rapid application development. However, when using Firebase
and Angular Universal
, the following questions may arise:
When we started developing our Angular Commerce , the main requirement was SEO optimization of the application for search engines. For this, the search robot must be able to recognize the content on the pages visited. The application server returns external data (XMLHttpRequest, fetch) at the user's request, embedding their results directly into HTML. But when we deal with complex SPAs we make various asynchronous requests. Therefore, when you open the source code of the SPA page, you will see something close to:
<!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>
The search engine does not know what content to index, due to the lack of content on the selected page, so our application cannot get to the top search engines.
We started looking for a server-side SPA rendering solution and stumbled upon Angular Universal
. This is the universal (isomorphic) JavaScript support for Angular. In other words, our Angular application is rendered on the server and the browser receives SEO friendly pages in response to a request. You can add meta tags to your page, get asynchronous data from a database or api and all this information will be presented in a rendered page.
However, we had an unexpected problem that we were stuck on. Angular Universal is not friendly with WebSockets. Since we use Firebase as a backend for our Angular Commerce project, for us it was a very unpleasant fact. Therefore, we want to talk about some of the pitfalls that we met in the development process and how we solved them.
The "client Firebase" package opens a WebSocket for authorization ('firebase.auth ()') which supports a persistent connection, so Universal does not know when to finish rendering on the server. Thus, the rendering process was not completed when some pages were requested.
We solved this using the client Firebase
package in the client module and the node Firebase
in the server module. Thus, the providers in app.module.ts
look like this:
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; }
And in 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; }
Initialization of firebaseServer
is presented in server.ts
. You must also provide the credential key from the Firebase Console
(the server and client versions of the library use different initialization mechanisms):
import * as firebaseServer from 'firebase-admin'; firebaseServer.initializeApp({ credential: firebase.credential.cert('./src/app/key.json'), databaseURL: 'https://pond-store.firebaseio.com' });
Now on the server we use the node Firebase
package only with the Firebase Realtime Database
, and the client Angular
uses the client Firebase
package with all the necessary functionality.
Angular Universal
does not work well with promises
. But the query methods of the Firebase Realtime Database
Library return promises
. That's why we wrapped these method calls in RxJs Observables
. Below is an example of a simple query to the 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 });
The transition to observables
allowed Angular Universal
correctly determine when the request was completed.
How does Angular Universal
? When a user makes a request, the server starts the renderModuleFactory
method, which renderModuleFactory
application bundle built for the server and immediately returns html with the rendered data on the page. Then the browser begins to render the browser assembly of Angular. When rendering is complete, Universal
will replace the rendered code on the server with a new, rendered in the browser. Interestingly, the server and browser assemblies perform almost the same work. This is noticeable on asynchronous database queries, when the data is visible, then disappears (because Universal
deletes the code rendered on the server), and then reappears when the new request is completed in the browser.
In Angular 5
, the @angular/platform-server
module has a TransferStateModule
. This module helps you transfer status from the server to the browser, eliminating the need to re-request data in the browser.
Since we are working with Angular 4
, we have found a solution how to transfer data from the server to the browser without Transfer State
.
The renderModuleFactory method
has an optional options
argument:
export declare function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: { document?: string; url?: string; extraProviders?: Provider[]; }): Promise<string>;
Through extraProviders
you can transfer providers that will be added to the server module and will be present in the server application. Thus, we created a serverStore
object that will store the data we want to pass to the client application.
Callback app.engine
in server.ts
will look like this:
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); }); });
Finally, we install our serverStore
in Window.store
before importing other scripts. Data acquisition in the browser occurs in the AppModule:
@NgModule({ // declarations, imports and others... providers: [ { provide: 'store', useValue: window['store'] } ] }) export class AppModule {}
How about using pre-rendered data in components? When rendering on the server, the data is received and installed in the storage. In the browser presentation, we need to check if there is the necessary information in the repository and get it. Below is a simple example of how to get products from the Firebase Realtime Database
in the ngOnInit
method:
@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; } } }
Please note that the serverStore
provider exists only in server build
, so it is declared using the @Optional
decorator to prevent browser errors:
No provider for serverStore!
Thus, using Angular Universal
with Firebase
is not quite obvious, but there are ways to live with it.
Source: https://habr.com/ru/post/341044/