Made this as a checklist for myself to remember all the intricate bits of getting Angular Universal to work on a Firebase Hosting server through a Firebase Cloud Function, but I can only assume someone else in the world will be trying to implement their own similar functionality, so here we are.
I take no responsibility if anyone uses this checklist and subsequently screws up their own life. These settings and directories work for my use but there is no guarantee they will work for yours. This is by no means meant to be a comprehensive tutorial, but I hope it can at least bring some pieces together and help someone else not age more rapidly than absolutely necessary.
- Notes:
- Based on / works with Angular 7 and Windows
- Please replace all instances of
YOUR_PROJECT_NAME
with your actual project name.
This can be found in your rootpackage.json
in the"name"
field.
It should be the name you chose for your project when first setting up withng new
- In an existing Angular CLI project:
ng generate universal --client-project YOUR_PROJECT_NAME
- If not already installed:
npm i -g firebase-tools
- Close your terminal and reopen it.
- Check version with
firebase --version
and compare with the current version on npm
After installing, if the versions don't match, or your version isn't current / hasn't changed, double check your installation(s) for these potential issues
- In your project root:
firebase init
- Select both
Functions
andHosting
with[spacebar]
- Choose an existing project if you have one.
- Functions Setup
- Choose
TypeScript
. The functions we'll include later are.ts
files. n
to skip using TSLint as it's not imperative right now.n
to skip installing dependencies as we'll have to install more anyway.
- Choose
-
Hosting Setup
- Change your public directory to
dist
( you can change this later infirebase.json
) y
to configure single-page app
- Change your public directory to
- Firebase should now have created/populated
firebase.json
and created a/functions
folder in the root directory with some stuff in it. - It should also have included
firebase-admin
andfirebase-functions
as dependencies in thepackage.json
of this new/functions
folder.
@nguniversal/express-engine
@nguniversal/module-map-ngfactory-loader
- These dependencies can be installed in your project root. We will also include them in
/functions/package.json
later, but don't worry about that now. - In your project root:
npm i -S @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader
or
yarn add @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader
- The Firebase Hosting server needs all the dependencies we used for our client app to properly render our server-side bundle files.
- Copy all
dependencies
anddevDependencies
from your rootpackage.json
to/functions/package.json
- Now we can install the function / server dependencies:
- If in the project root folder, navigate to the
/functions
folder:cd functions npm install
- If in the project root folder, navigate to the
-
angular.json
-
projects.YOUR_PROJECT_NAME.architect.build.options.outputPath
change to"dist"
-
projects.YOUR_PROJECT_NAME.architect.server.options.outputPath
change to"functions/lib"
-
-
-
functions.predeploy
(sometimes the internet recommends more fields, but this seems to work on its own)
(for Windows) make sure it reads:"functions": { "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build" },
Alternative ways of writing
\"$RESOURCE_DIR\"
can be found here if yourpredeploy
isn't firing correctly. -
hosting.public
Value should be"dist"
(from Firebase Hosting Setup)."hosting": { "public": "dist" }
-
rewrites
Firebase defaults to pointing page requests to your hosting landing page,/index.html
However, we want to intercept the request with a"function"
instead of a"destination"
so Firebase knows about our Angular Universal server files.
Specifically, the function we'll name"ssrApp"
in the future."rewrites": [{ "source": "**", "destination": "/index.html" <--- }]
to
"rewrites": [{ "source": "**", "function": "ssrApp" <--- * }] * (where "ssrApp" is the name of your firebase cloud function)
-
-
- This is the
package.json
inside the/functions
folder Firebase made for you.
Make sure"main"
points to the compiled JavaScript file we'll be rendering to/functions/lib
:"main": "lib/index.js"
- This is the
-
app.module.ts
- Angular creates this file for you when creating a new app
ng new
If we want to appropriately handle the handoff of the app from the server to the client, we need theBrowserModule
andBrowserTransferStateModule
:
app.module.ts
should look like this:import { NgModule } from '@angular/core'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [ AppModule, BrowserModule.withServerTransition({ appId: 'YOUR_PROJECT_NAME' }), BrowserTransferStateModule, ], bootstrap: [AppComponent], }) export class AppModule {}
- Angular creates this file for you when creating a new app
-
app.server.module.ts
- Angular creates this file for you when you
ng generate
a new Universal project.
If we want to use lazy loading, we need to import theModuleMapLoaderModule
If we want to appropriately handle the handoff of the app from the server to the client, we need theBrowserModule
andServerTransferStateModule
:
app.server.module.ts
should look like this:import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ServerModule, ServerTransferStateModule } from '@angular/platform-server'; import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [ AppModule, BrowserModule.withServerTransition({ appId: 'YOUR_PROJECT_NAME' }), ServerModule, ServerTransferStateModule, ModuleMapLoaderModule ], bootstrap: [AppComponent], }) export class AppServerModule {}
- Angular creates this file for you when you
-
app-routing.module.ts
- If your app "flickers" when the server hands off the app to the client, adding this to your
AppRoutingModule
may help:@NgModule({ imports: [ RouterModule.forRoot([ /* your routes */ ], { initialNavigation: 'enabled' }) ] }) export class AppRoutingModule {}
- If your app "flickers" when the server hands off the app to the client, adding this to your
-
Some have implemented this step with an npm package, however the code is tolerable enough to include in a few files, so we'll just do that.
-
Copy the
/universal
folder from/functions/src
in this repository to your local/functions/src
.Also note: The code in these
.ts
files works in Windows and usespath.join(__dirname, ..)
to point the object passed intoangularUniversal.trigger()
to the correctindex
,main
andstaticDirectory
files. -
Replace the contents of the
/functions/src/index.ts
file Firebase made for you, with:import universal from './universal/server-function'; export const ssrApp = universal;
-
Note the
export const ssrApp
which is the same function name we defined in thefirebase.json
rewrites
-
We've set our app up to expect and accept the files we will create with these build scripts.
We'll go through each command separately, which will give us a better idea of what is going on with the finalbuild-and-deploy
script. -
First, we'll build our normal Angular app in production mode:
ng build --prod
-
Then we'll create the server bundle from the completed production bundle:
ng run YOUR_PROJECT_NAME:server:production
-
An
index.html
file will be created in your client productiondist
folder.
Firebase will first look for anindex.html
, and only run ourssrApp
Cloud Function ifindex.html
is not found.
However, we do need some sort ofindex
file for our server bundle, so we can safely take the newly createdindex.html
file in the/dist
folder and move it to our server rendered folder,/functions/lib
.
Additionally, we'll rename this moved file to reflect theindex
parameter of the Firebase config object we're using in/functions/src/universal/server-function.ts
which we've nameindex-server.html
On Windows, this command looks like this:
move dist\\index.html functions\\lib\\index-server.html
-
Once our builds are finished and
index
files are in their correct places, we can finally deploy our app to Firebase Hosting and ourexpress
function to Firebase Cloud Functions:firebase deploy
-
If we wish to run a build again, we'll have to delete the production build files we rendered in
/dist
and/functions/lib
before creating new ones.
The first step in creating a new build is to clean up the old build.
We'll remove both folders at once. On Windows we can usermdir
with/S
to remove entire directory trees, and/Q
to not ask questions.
In your rootpackage.json
in thescripts
object:rmdir /S /Q dist && rmdir /S /Q functions\\lib
-
We can group the creation of our client and server bundles, removal of old bundles and index file moving into a few commands:
"build:client-and-server-bundles": "ng build --prod && ng run YOUR_PROJECT_NAME:server:production", "move-index": "move dist\\index.html functions\\lib\\index-server.html",
-
And then we can group these commands into a final command which cleans out old bundles, compiles new ones, moves appropriate files and ultimately deploys the finshed files and
express
Cloud Function:"build-and-deploy": "rmdir /S /Q dist && rmdir /S /Q functions\\lib && npm run build:client-and-server-bundles && npm run move-index && firebase deploy"
-
Your CLI will do some things and in the end Firebase should tell you
Deploy complete
at which point you can stop holding your breath and go get a drink. Hurrah.
-
firebase-tools
does not update / install new versions.- Close your terminal and reopen it. Sometimes this is all it takes.
firebase-tools
must be installed globally. Make sure it is not installed in your project directory.- Try updating with
npm update -g firebase-tools
. - Try updating to a specific version with something like
npm install -g firebase-tools@6.11.0
- Did you previously install
firebase-tools
withyarn global add
, and are now trying to update with npm (or vice versa)? Make sure to remove any straggling installations in your local project or globally withnpm uninstall -g firebase-tools
and/oryarn global remove firebase-tools
.
-
$RESOURCE_DIR
isn't found / Firebasefunctions.predeploy
isn't firing correctly.- Try writing
$RESOURCE_DIR
like this:
\"$RESOURCE_DIR\"
%$RESOURCE_DIR%
$RESOURCE_DIR
functions
- ^ aka the hard-coded
functions
folder itself
- ^ aka the hard-coded
- Try writing
There are a lot of hard-working, smart people on the internet with buckets of knowledge to tap into. This "checklist" is not derived from any single resource, but there are a few key resources I used to widen my understanding on the subject.
Some resources are free, others require payment to deliver a complete package. I've personally paid for some of these resources, do not regret it, and have much respect for the individuals teaching these concepts in depth.
angular-university.io
Angular University Course- Vasco is a knowlegeable and detailed teacher. There are a few outdated pieces to this course, but such is web development, and I regularly reference this course to refresh my understanding.
fireship.io
Angular Universal SSR with Firebase- Jeff wonderfully packs large concepts into easily digestible bits. His lessons and clarifications have been invaluable to making sense of this "stack".
Github heros
- There are a mess of random Github issue comments which have helped immensely, however they are strewn about hundreds of browser tabs. I will list them when I find them again.