Skip to content

docs(universal): getting started, serving with Cloud Functions, and prerendering #1841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Sep 11, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Status: Release candidate
## What is AngularFire?

- **Observable based** - Use the power of RxJS, Angular, and Firebase.
- **Realtime bindings** - Synchronize data in realtime.
- **Authentication** - Log users in with a variety of providers and monitor authentication state in realtime.
- **Real-time bindings** - Synchronize data in real-time.
- **Authentication** - Log users in with a variety of providers and monitor authentication state.
- **Offline Data** - Store data offline automatically with AngularFirestore.
- **ngrx friendly** - Integrate with ngrx using AngularFire's action based APIs.

Expand All @@ -22,7 +22,7 @@ Status: Release candidate

[Upgrading to v5.0? Check out our guide.](docs/version-5-upgrade.md)

**Having troubles?** Get help on the [Firebase Mailing List](https://groups.google.com/forum/#!forum/firebase-talk) (offically supported), the [Firebase Community Slack](https://firebase.community/) (look for the `#angularfire2` room), [Gitter](https://gitter.im/angular/angularfire2), or [Stack Overflow](https://stackoverflow.com/questions/tagged/angularfire2).
**Having troubles?** Get help on the [Firebase Mailing List](https://groups.google.com/forum/#!forum/firebase-talk) (officially supported), the [Firebase Community Slack](https://firebase.community/) (look for the `#angularfire2` room), [Gitter](https://gitter.im/angular/angularfire2), or [Stack Overflow](https://stackoverflow.com/questions/tagged/angularfire2).

## Install

Expand Down Expand Up @@ -76,7 +76,7 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo

#### Realtime Database

> `AngularFireDatabase` allows you to work with the Realtime Database, Firebase's original database. It's an efficient, low-latency solution for mobile apps that require synced states across clients in realtime.
> `AngularFireDatabase` allows you to work with the Realtime Database, Firebase's original database. It's an efficient, low-latency solution for mobile apps that require synced states across clients in real-time.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"realtime" over "real-time" this goes way back to the early Firebase days.


- [Objects](docs/rtdb/objects.md)
- [Lists](docs/rtdb/lists.md)
Expand All @@ -89,12 +89,19 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo
### Upload files
- [Getting started with Cloud Storage](docs/storage/storage.md)

### Universal
- [Server-side Rendering with Universal](docs/server-side-rendering.md)
### Deploying your application

### Deploy to Firebase Hosting
> Firebase Hosting is production-grade web content hosting for developers. With Hosting, you can quickly and easily deploy web apps and static content to a global content delivery network (CDN) with a single command.

- [Deploying AngularFire to Firebase Hosting](docs/deploying-angularfire-to-firebase.md)
- [Deploy your Angular application on Firebase Hosting](docs/deploying-angularfire-to-firebase.md)

#### Server-side rendering

> Angular Universal is a technology that allows you to run your Angular application on a server. This allows you to generate your HTML in a process called server-side rendering (SSR). Angularfire is compatible with server-side rendering; allowing you to take advantage of the Search Engine Optimization, link previews, the performance gains granted by the technology, and more. [Learn more about Angular Universal](https://angular.io/guide/universal).

- [Getting started with Angular Universal](docs/universal/getting-started.md)
- [Deploying your Universal application on Cloud Functions for Firebase](docs/universal/cloud-functions.md)
- [Prerendering your Universal application](docs/universal/prerendering.md)

### Ionic

Expand Down
5 changes: 3 additions & 2 deletions docs/install-and-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,12 @@ export class AppModule {}

After adding the AngularFireModule you also need to add modules for the individual @NgModules that your application needs.

- `AngularFirestoreModule`
- `AngularFireAuthModule`
- `AngularFireDatabaseModule`
- `AngularFireFunctionsModule`
- `AngularFirestoreModule`
- `AngularFireStorageModule`
- `AngularFireMessagingModule` (Future release)
- `AngularFireMessagingModule`

#### Adding the Firebase Database and Auth Modules

Expand Down
6 changes: 4 additions & 2 deletions docs/ionic/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ export class AppModule {}
After adding the AngularFireModule you also need to add modules for the individual @NgModules that your application needs.
- AngularFireAuthModule
- AngularFireDatabaseModule
- AngularFireStorageModule (Future release)
- AngularFireMessagingModule (Future release)
- AngularFireFunctionsModule
- AngularFirestoreModule
- AngularFireStorageModule
- AngularFireMessagingModule

#### Adding the Firebase Database and Auth Modules

Expand Down
72 changes: 72 additions & 0 deletions docs/universal/cloud-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Deploying your Universal application on Cloud Functions for Firebase

Pre-req, previous page, something, something, firebase cli

```bash
npm i -g @firebase-tools
```

```bash
firebase login
```

Init Firebase if you haven't:

```bash
firebase init
```

Whatever features you want but at least `functions` and `hosting`. For Functions, choose `typescript` and Hosting the default `public` directory is fine.

Let's make some modifications to our `package.json`, to build for Functions.

```js
"scripts": {
// ... omitted
"build": "ng build && npm run copy:hosting && npm run build:ssr && npm run build:functions",
"copy:hosting": "cp -r ./dist/YOUR_PROJECT_NAME/* ./public && rm ./public/index.html",
"build:functions": "npm run --prefix functions build"
},
```

Add the following to your `functions/src/index.ts`:

```ts
export const universal = functions.https.onRequest((request, response) => {
require(`${process.cwd()}/dist/YOUR_PROJECT_NAME-webpack/server`).app(request, response);
});
```

Change the build script in your `functions/package.json` to the following:

```js
"scripts": {
// ... omitted
"build": "rm -r ./dist && cp -r ../dist . && tsc",
}
```

Add the following to your `firebase.json`:

```js
{
// ...
"hosting": {
// ...
"rewrites": [
{ "source": "**", "function": "universal" }
]
}
}
```

We can now run `npm run build`, `firebase serve` and `firebase deploy`.

Something, something, cache-control...

### [Next Step: Prerendering your Universal application](prerendering.md)

## Additional Resources

- [Universal Starter Template](https://github.com/angular/universal-starter)
- [AngularFirebase SSR Videos](https://angularfirebase.com/tag/ssr/)
93 changes: 46 additions & 47 deletions docs/server-side-rendering.md → docs/universal/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Server-side Rendering with Universal
# Getting started with Angularfire and Universal
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mega nit: AngularFire


Server-side rendering (SSR) is the process of converting a JavaScript app to plain HTML at request-time, allowing search engine crawlers and linkbots to understand page content reliably.

## 0. Prerequisites

- @angular/cli >= v6.0
- angularfire2 >= v5.0.0-rc.7
- angularfire2 >= v5.0.0-rc.12

## 1. Generate the Angular Universal Server Module

Expand All @@ -19,8 +19,8 @@ ng generate universal --client-project <your-project>

[ExpressJS](https://expressjs.com/) is a lightweight web framework that can serve http requests in Node. First, install the dev dependencies:

```
npm install --save-dev express webpack-cli ts-loader ws xmlhttprequest
```bash
npm install --save-dev @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader express webpack-cli ts-loader ws xhr2
```

Create a file called `server.ts` in the root of you project.
Expand All @@ -30,41 +30,38 @@ Create a file called `server.ts` in the root of you project.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

// Required for Firebase
// Polyfills required for Firebase
(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;

(global as any).XMLHttpRequest = require('xhr2');

// Faster renders in prod mode
enableProdMode();

// Express server
const app = express();
// Export our express server
export const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');
const APP_NAME = 'YOUR_PROJECT_NAME';
const APP_NAME = 'YOUR_PROJECT_NAME'; // TODO: replace me!

const { AppServerModuleNgFactory } = require(`./dist/${APP_NAME}-server/main`);
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(`./dist/${APP_NAME}-server/main`);

// index.html template
const template = readFileSync(join(DIST_FOLDER, APP_NAME, 'index.html')).toString();

app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: options.req.url,
}).then(html => {
callback(null, html);
});
});
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, APP_NAME));
Expand All @@ -77,31 +74,44 @@ app.get('*', (req, res) => {
res.render(join(DIST_FOLDER, APP_NAME, 'index.html'), { req });
});

// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
// If we're not in the Cloud Functions environment, spin up a Node server
if (!process.env.FUNCTION_NAME) {
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
}
```

## 3. Add a Webpack Config for the Express Server

Create a new file named `webpack.server.config.js` to bundle the express app from previous step.
Create a new file named `webpack.server.config.js` to bundle the express app from previous step.


```js
const path = require('path');
const webpack = require('webpack');

const APP_NAME = 'YOUR_PROJECT_NAME';
const APP_NAME = 'YOUR_PROJECT_NAME'; // TODO: replace me!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need more context around 'YOUR_APP_NAME. Something comment like "This is the same as the folder name...".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better yet, my mid-term plan here is to read the paths from the angular.json file; need to fiddle there. SGTY?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM!


module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
mode: 'development',
target: 'node',
externals: [/(node_modules|main\..*\.js)/],
externals: [
/* Firebase has some troubles being webpacked when in
in the Node environment, let's skip it.
Note: you may need to exclude other dependencies depending
on your project. */
/^firebase/
],
output: {
path: path.join(__dirname, `dist/${APP_NAME}`),
// Export a UMD of the webpacked server.ts & deps, for
// rendering in Cloud Functions
path: path.join(__dirname, `dist/${APP_NAME}-webpack`),
library: 'app',
libraryTarget: 'umd',
filename: '[name].js'
},
module: {
Expand All @@ -126,29 +136,18 @@ module.exports = {

## 4.0 Build Scripts

Update your `package.json` with the following build scripts.
Update your `package.json` with the following build scripts, replacing `YOUR_PROJECT_NAME` with the name of your project.

```js
"scripts": {
// ... omitted
"build:ssr": "ng build --prod && ng run YOUR_PROJECT_NAME:server && npm run webpack:ssr",
"serve:ssr": "node dist/YOUR_PROJECT_NAME/server.js",
"webpack:ssr": "webpack --config webpack.server.config.js"
"build": "ng build && npm run build:ssr",
"build:ssr": "ng run YOUR_PROJECT_NAME:server && npm run webpack:ssr",
"webpack:ssr": "webpack --config webpack.server.config.js",
"serve:ssr": "node dist/YOUR_PROJECT_NAME-webpack/server.js"
},
```

Test your app locally by running `npm run build:ssr && npm run serve:ssr`.

## 5.0 Deployment

With an existing Firebase project, you can easily deploy your ExpressJS server to [App Engine Flex](https://cloud.google.com/appengine/docs/flexible/) (Note: This is a paid service based on resource allocation).


1. Install [gcloud CLI tools](https://cloud.google.com/sdk/gcloud/) and authenticate.
2. Change the start script in package.json to `"start": "npm run serve:ssr"`
2. Run `gcloud app deploy` and you're on the cloud.

## Additional Resources
Test your app locally by running `npm run build && npm run serve:ssr`.

- [Universal Starter Template](https://github.com/angular/universal-starter)
- [AngularFirebase SSR Videos](https://angularfirebase.com/tag/ssr/)
### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md)
69 changes: 69 additions & 0 deletions docs/universal/prerendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Prerendering your Universal application

`static.paths.js`:

```js
export default [
'/',
'/another_path',
'/yet_another_path',
// ... etc.
];
```

```bash
npm i --save-dev mkdir-recursive
```

Add the following to your `server.ts`:

```ts
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { renderModuleFactory } from '@angular/platform-server';
import { mkdirSync } from 'mkdir-recursive';

if (process.env.PRERENDER) {

const routes = require('./static.paths').default;
Promise.all(
routes.map(route =>
renderModuleFactory(AppServerModuleNgFactory, {
document: template,
url: route,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => [route, html])
)
).then(results => {
results.forEach(([route, html]) => {
const fullPath = join('./public', route);
if (!existsSync(fullPath)) { mkdirSync(fullPath); }
writeFileSync(join(fullPath, 'index.html'), html);
});
process.exit();
});

} else if (!process.env.FUNCTION_NAME) {

// If we're not in the Cloud Functions environment, spin up a Node server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
}
```

Let's make some modifications to our `package.json`, to prerender your content:

```js
"scripts": {
// ... omitted
"build": "ng build && npm run copy:hosting && npm run build:functions && npm run prerender:ssr",
"prerender:ssr": "PRERENDER=1 node dist/YOUR_PROJECT_NAME-webpack/server.js",
},
```

Now when you run `npm run build` you should see prerendered content in your `/public` directory, ready for deployment on Firebase Hosting.

`firebase serve`, `firebase deploy`