Firebase + Angular Universal = impossible is possible
Firebase
is a great tool for for quick backend development. But the following issues may arise in development using Firebase
and Angular Universal
:
- What firebase package to use in client and on the server?
- What mechanism use for asynchronous operations?
- How pass data from server to client avoiding requests duplication?
When we started developing our Angular Commerce, on of the main requirenments was SEO optimisation of the application for search engines. For this requirenment search crawler need to recognize content on visited pages. Server rendered applications return external data on user request embedded directly into HTML template. But when we deal with single page applications there are different asynchronous requests. So when you open source code of SPA page you will see something like this:
Search engine doesn’t know what content to index, because of missing content in fetched page, so our page can’t rich first pages of search engines.
We started to find solution for server rendening of SPA and stumbled upon Angular Universal
. It's a universal (isomorphic) JavaScript support for Angular. In other words our Angular application renders on server and we get SEO friendly templates in response. You can inject meta tags to you page, get async data from database or apis and all this informations will be presented in source template.
But we got stuck on the problem. Angular Universal doesn’t deal well with WebSockets. As we are using Firebase as backend for our Angular Commerce it was critical to us. So we want to tell about some pitfalls that we met in development and how we solved them.
Using client and server firebase
As we understood client Firebase
package is opening WebSocket for authorization, so Universal doesn't know when terminate server rendering. So we had an infinite loop on requesting some page.
We solved this by using client Firebase
package in client module and node Firebase
in server module. So providers in app.module.ts
look like this:
And in app.server.module.ts
:
Initialization of firebaseServer
is presented in server.ts
. Also you must provide credentials key from Firebase Console
:
Now on server we are using node Firebase
package with Firebase Realtime Database
only and client Angular
is using client Firebase
package with all necessary functional.
Using Observables instead of Promises
As we noticed Angular Universal
doesn't deal well with promises
. But Firebase Realtime Database
library query methods return promises in most cases. So we wrapped those method calls into RxJs Observables
. Here is an example of simple query to products bucket in Firebase Realtime Database
:
Passing server data to client
How does Angular Universal
works? When user makes request, server run renderModuleFactory
method that gets use server build and immediately returns html template with rendered page. Then it starts to render browser Angular build. When rendering is over Universal
substitutes the view to the browser version. It's interesting that server and client build almost do the same work. It's noticeable in asynchronous requests to database when data is on view, than disappears (because Universal
substitutes the view to client version without data), and than appears again.
Here is demo with passing data from server to client:
universal-firebase.firebaseapp.com
And here without passing data:
universal-firebase-flashing.firebaseapp.com
In Angular 5
in @angular/platform-server
module there is aTransferStateModule
. This module helps you to transfer it’s state from the server to the browser, to remove the need of requesting data again in the browser.
As we are working with Angular 4
, we discovered a solution how to pass data from server to client without Transfer State
.
renderModuleFactory method
have an optional argument options
:
To extraProviders
you can pass providers that will be added to server module and will be present in server app. So we created a serverStore
object that will store all data that we want to pass to client app. Also providing object with setter of global serverStore
helped us setting it in server app.
app.engine
callback in server.ts
will look like this:
Finally we set our serverStore
to Window.store
before importing other scripts. Getting server data is simple in AppModule:
Getting server data on client
What about using store in components? It’s simple. In server app get the data and set it to store. In client app we need to check if there is necessary data on store and get it. Here is a simple example how to get products from Firebase Realtime Database
in ngOnInit
method:
Notice that serverStore
provider only exist on server build
so it's injected with @Optional
decorator for avoiding error on client:
So, using Angular Universal
with Firebase
is fairly unobvious, but there are way how to deal with it.