Skip to content
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

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

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Expand Up @@ -9,7 +9,7 @@

- **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.
- **Authentication** - Log users in with a variety of providers and monitor authentication state.
- **Offline Data** - Store data offline automatically with AngularFirestore.
- **Server-side Render** - Generate static HTML to boost perceived performance or create static sites.
- **ngrx friendly** - Integrate with ngrx using AngularFire's action based APIs.
Expand All @@ -25,7 +25,7 @@

[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 @@ -92,17 +92,25 @@ 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)

### Send push notifications
- [Getting started with Firebase Messaging](docs/messaging/messaging.md)

### Directly call Cloud Functions
- [Getting started with Callable Functions](docs/functions/functions.md)

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

> 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.

- [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
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
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
75 changes: 75 additions & 0 deletions docs/universal/cloud-functions.md
@@ -0,0 +1,75 @@
# Deploying your Universal application on Cloud Functions for Firebase

After [setting up your application with Angular Universal as outlined in Getting Started](getting-started.md), you're now ready to build your application for Firebase Hosting & Cloud Functions.

> Cloud Functions for Firebase lets you automatically run backend code in response to events triggered by Firebase features and HTTPS requests. Your code is stored in Google's cloud and runs in a managed environment. There's no need to manage and scale your own servers. [Learn more about Cloud Functions for Firebase](https://firebase.google.com/docs/functions/).

If you don't already have the Firebase CLI installed, do so:

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

Then inside your project root, setup your Firebase CLI project:

```bash
firebase init
```

Configure whichever features you'd want to manage but make sure to select at least `functions` and `hosting`. Choose Typescript for Cloud Functions and use the default `public` directory for Hosting.

After you're configured, you should now see a `firebase.json` file in your project root. Let's add the following `rewrites` directive to it:

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

This will inform Firebase Hosting that it should proxy all requests to Cloud Functions, if a file isn't already present in the hosting directory.

Let's go ahead and modify your `package.json` to build for Cloud 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"
},
```

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

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

Finally, 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);
});
```

We you should now be able to run `npm run build` to build your project for Firebase Hosting and Cloud Functions.

To test, spin up the emulator with `firebase serve`. Once you've confirmed it's working go ahead and `firebase deploy`.

### [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
@@ -1,11 +1,11 @@
# Server-side Rendering with Universal
# Getting started with Angularfire and Universal
Copy link
Member

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
- @angular/fire >= v5.0.0

## 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
Member

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
Member

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)
72 changes: 72 additions & 0 deletions docs/universal/prerendering.md
@@ -0,0 +1,72 @@
# Prerendering your Universal application

Prerendering a Universal application allows us to generate the HTML before the user requests it; increasing performance and decreasing cost. Let's configure your application to prerender and staticly serve it's most commonly accessed routes on Firebase Hosting.

First create a `static.paths.js` in your project root, which lists the URLs you'd want to prerender:

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

Let's install `mkdir-recursive` to make the next step a little easier:

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

Now replace the listener in your `server.ts` with the following:

```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}`);
});
}
```

Now if the `PRERENDER` environment variable is passed any value, instead of serving your application it will iterate over the paths in `static.paths.js`, render them, and write them to your `public` directory. *You could always make this a seperate script.*

Finally make some modifications to your `package.json`, to prerender your content when you build:

```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` the prerendered content should be available in your `/public` directory, ready for deployment on Firebase Hosting.