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.
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
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
@angular/platform-server
To build and render your universal
app, we need to install @angular/platform-server
.
$ npm install --save @angular/platform-server
erver Side Rendering
configurationWe 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"} **],...}
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"}}
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!
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! š
[1] Before continuing, you should have had created a firebase project here. I named mine _angular-universal-firebase_
for this case.
firebase
` in the commandĀ lineLog in to firebase in the command line with the same google account you used to create your firebase project in [1].
$ firebase login
Initialize firebase configurations through the command line:
$ firebase init
Functions
and Hosting
for features to set up
Firebase setup configuration
angular-universal-firebase
.
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}
functions
` directoryInstall da dependencies!
# in project root$ npm --prefix functions install
or
# in `functions` directory$ npm install
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
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:
AppServerModuleNgFactory
which was generated from Part I: Step 7āāāserver version.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);
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"
}\]**
}
}
dist/index.html
` from root directoryhtml
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
If all things went well, you should be able to deploy your app to Firebase:
$ firebase deploy
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)