Aaron Te

@aaaronnte

Deploy Angular Universal w/ Firebase 🚀🔥

Disclaimer: This blog post will be focused on step by step tutorial of how to deploy a Universal Angular App using Firebase Hosting. For any explanations about Angular Universal and Server Side Rendering here are some helpful resources:

You can find the source code here if you want to follow along.

Requirements

  • node.js (I am using v8.3.0 for this tutorial)

Part I: Build an angular app (browser and server versions) 🔨

1. Install global dependencies

We are going to use @angular/cli and firebase-tools in the command line to build and deploy your application.

$ npm install --global @angular/cli firebase-tools

2. Create a new angular project

Using @angular/cli we are going to create a new angular app. In this case, I will name it angular-universal-firebase.

$ ng new angular-universal-firebase
$ cd angular-universal-firebase

3. Install @angular/platform-server

To build and render your universal app, we need to install @angular/platform-server.

$ npm install --save @angular/platform-server

4. Add Server Side Rendering configuration

We are basically copying the default app configuration and modifying it for the server side rendering (SSR) configuration. For the SSR configuration, we don’t need to add polyfill (since the code will run in a node server instead of a browser) and styles (since this will be added when we reference the main index.html when building the server HTML through @angular/platform-server. Also note, we added "platform": "server" which is a feature introduced in @angular/cli#v1.3.0. This will allow us to build a SSR version of your app using @angular/cli.

// .angular-cli.json
{
...
apps: [
{ /* default config */ },
{
"name": "ssr",
"root": "src",
"outDir": "functions/dist-ssr",
"assets": ["assets", "favicon.ico"],
"index": "index.html",
"main": "main-ssr.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app-ssr.json",
"prefix": "app",
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
},
"platform": "server"
}

],
...
}

5. Create necessary files for app server version

  • src/app/app.server.module.ts 
    Create a new module for the app’s server version.
// src/app/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
ServerModule,
AppModule
],
bootstrap: [AppComponent]
})
export class AppServerModule { }
  • src/main-ssr.ts
    Create entry point for the server module. This is main file we referenced in the server version of the app in .angular-cli.json.
// src/main-ssr.ts
export { AppServerModule } from './app/app.server.module';
  • src/tsconfig.app-ssr.json 
    Create the tsconfig for the server version. Similar to the browser version except for angularCompilerOptions.entryModule which will reference the entry module for the server version that we just created. This is also referenced in.angular-cli.json configuration as tsconfig.
// src/tsconfig.app-ssr.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

6. Include server transition in app’s browser module

Since we are sending the server version of your app to the browser before the browser version, we need to add call .withServerTransition() when adding BrowserModule in imports of the browser module of the app.

// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
const APP_ID = 'angular-universal-firebase';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: APP_ID })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Now we are ready to build the server and browser versions of the app!

7. Build browser and server versions of the app

Using the @angular/cli, we will build the two versions of the app.
ng build -prod : will build the browser version with prod configurations (i.e. minified html/js/css, aot, etc.)
ng build -aot -app ssr : will build the server version. It will generate an ngFactory file that we can use to render the app in node.

$ ng build -prod
$ ng build -aot -app ssr

When both builds are finished, you should now have a dist folder in your root directory and dist-ssr inside your functions directory. Hooray! 🎉

Part II: Deploying with Firebase 🚀

[1] Before continuing, you should have had created a firebase project here. I named mine angular-universal-firebase for this case.

1. Log in to `firebase` in the command line

Log in to firebase in the command line with the same google account you used to create your firebase project in [1].

$ firebase login

2. Initialize Firebase in the `angular` project

Initialize firebase configurations through the command line:

$ firebase init
  • Select Functions and Hosting for features to set up
Firebase setup configuration
  • Select the firebase project you created in [1]. (In my case, it’s angular-universal-firebase.
  • Accept all defaults in this stage; we will configure the rest in later steps.

3. Add package dependencies to `functions`

Since we are using a node server through firebase-functions, We need to include angular dependencies in functions/package.json to render the server version of the app.

Aside: Right now, I don’t know any way to mitigate this duplication of dependency declaration since as far as I know, you can’t access files outside the functions directory in any firebase-functions javascript files. But if you know a way, please let me know!

// functions/package.json
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"dependencies": {
"@angular/animations": "^4.3.5",
"@angular/common": "^4.3.5",
"@angular/compiler": "^4.3.5",
"@angular/core": "^4.3.5",
"@angular/forms": "^4.3.5",
"@angular/http": "^4.3.5",
"@angular/platform-browser": "^4.3.5",
"@angular/platform-server": "^4.3.5",
"express": "^4.15.4",

"firebase-admin": "~4.2.1",
"firebase-functions": "^0.5.7",
"rxjs": "^5.4.3",
"zone.js": "^0.8.16"

},
"private": true
}

4. Install packages in `functions` directory

Install da dependencies!

# in project root
$ npm --prefix functions install
or 
# in `functions` directory
$ npm install

5. Copy `dist` folder to `functions/dist`

Since you cannot access files outside of the functions directory in firebase-functions, we have to copy the dist directory inside the functions directory so we can access it in firebase-functions.

dist should now exist in root folder AND functions folder

6. Create Firebase function to serve the app

We’re going to use functions.https.onRequest Firebase function type to send the response from an express server. There are a lot of things going on in this file but the most notable are:

  • Importing AppServerModuleNgFactory which was generated from Part I: Step 7 — server version.
  • Creating an index variable which is getting the index.html file we generated from Part I: Step 7 — browser version.
  • Using renderModuleFactory to generate an html file that we send as a response with url and document parameters. 
    url parameter determines which route of the app is going to be rendered. Specifying this allows renderModuleFactory to build the html of that route.
    document is the full document HTML of the page to render. In this case, it will be the browser version index.html of the app.
// functions/index.js
require('zone.js/dist/zone-node');
const functions = require('firebase-functions');
const express = require('express');
const path = require('path');
const { enableProdMode } = require('@angular/core');
const { renderModuleFactory } = require('@angular/platform-server');
const { AppServerModuleNgFactory } = require('./dist-ssr/main.bundle');
enableProdMode();
const index = require('fs')
.readFileSync(path.resolve(__dirname, './dist/index.html'), 'utf8')
.toString();
let app = express();
app.get('**', function(req, res) {
renderModuleFactory(AppServerModuleNgFactory, {
url: req.path,
document: index
}).then(html => res.status(200).send(html));
});
exports.ssr = functions.https.onRequest(app);

7. Configure Firebase hosting

Now that we have built the function to render pages, we need to change the firebase hosting configuration to use this function. We also need to change the public directory to use the dist directory to access your assets.

// firebase.json
{
    "hosting": {
"public": "dist",
"rewrites": [{
"source": "**",
"function": "ssr"
}]

}
}

8. Delete `dist/index.html` from root directory

  • This is so Firebase won’t serve the html file but rather run the ssr function. NOTE: delete dist/index.html from the root directory. DO NOT DELETE functions/dist/index.html.
$ rm dist/index.html

9. Deploy to Firebase 🚀 🔥

If all things went well, you should be able to deploy your app to Firebase:

$ firebase deploy

That’s it! 👍

You can check out the source code here.

I hope this tutorial was helpful in some way! I would love to hear any feedback or questions if you have any!

Other helpful resources 
Creating an Angular Universal app with the Angular CLI
Angular Universal with Firebase Dynamic Hosting (Only one route handling)

More by Aaron Te

Topics of interest

More Related Stories