From c92e5385de182fe8b4010cbfab52d11a19b732b6 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 27 Aug 2018 15:49:02 -0700 Subject: [PATCH 01/13] docs(ssr): New TOC for Angular Universal --- README.md | 36 +++++++++++++++---- docs/universal/cloud-functions.md | 0 .../getting-started.md} | 0 docs/universal/hosting-proxy.md | 1 + 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 docs/universal/cloud-functions.md rename docs/{server-side-rendering.md => universal/getting-started.md} (100%) create mode 100644 docs/universal/hosting-proxy.md diff --git a/README.md b/README.md index 8bb269210..77be2930c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Status: Release candidate ## What is AngularFire? - **Observable based** - Use the power of RxJS, Angular, and Firebase. -- **Realtime bindings** - Synchronize data in realtime. +- **Real-time bindings** - Synchronize data in real-time. - **Authentication** - Log users in with a variety of providers and monitor authentication state in realtime. - **Offline Data** - Store data offline automatically with AngularFirestore. - **ngrx friendly** - Integrate with ngrx using AngularFire's action based APIs. @@ -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 @@ -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. - [Objects](docs/rtdb/objects.md) - [Lists](docs/rtdb/lists.md) @@ -89,12 +89,34 @@ 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 +#### Static hosting on Firebase -- [Deploying AngularFire to Firebase Hosting](docs/deploying-angularfire-to-firebase.md) +> 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 with Firebase Hosting](docs/deploying-angularfire-to-firebase.md) + +#### Angular Universal + +> Angular Universal is a technology that allows you to run your Angular application on a server; this allows you to generate your HTML with a process called server-side rendering (SSR). Angularfire is compatible with server-side rendering; allowing you to take advantage of the performance gains and SEO granted by the technology. + +- [Getting started with Angularfire and Universal](docs/universal/getting-started.md) + +You have two options for deploying your Universal application, on-demand or pre-rendered (or a combination thereof): + +##### Render your application on-demand in response to HTTP requests + +> Server-side rendered pages can be generated on the server in response to a request from a web browser. + +- [Deploying your Universal application on Cloud Functions for Firebase](docs/universal/cloud-functions.md) +- [Proxy Firebase Hosting to Cloud Functions for Firebase](docs/universal/hosting-proxy.md) + +#### Pre-rendering your application to serve statically + +> Server-side rendered pages can also be pre-generated and rendered statically on hosting. This allows you to avoid the costs and complexity of spinning up servers in response to requests, while maintaining most of the benefits. It is often preferable if you don't have a lot of dynamic content or it changes infrequently. + +- [Prerendering your Universal application and deploying on Firebase Hosting](docs/universal/prerendering.md) ### Ionic diff --git a/docs/universal/cloud-functions.md b/docs/universal/cloud-functions.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/server-side-rendering.md b/docs/universal/getting-started.md similarity index 100% rename from docs/server-side-rendering.md rename to docs/universal/getting-started.md diff --git a/docs/universal/hosting-proxy.md b/docs/universal/hosting-proxy.md new file mode 100644 index 000000000..c8fd0311b --- /dev/null +++ b/docs/universal/hosting-proxy.md @@ -0,0 +1 @@ +# TDB \ No newline at end of file From 8ad0fe779ef9c1ca9f76f2720a17989c86447bb0 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 27 Aug 2018 16:11:08 -0700 Subject: [PATCH 02/13] docs(ssr): Handle lazy modules and webpack config for Cloud Functions --- README.md | 2 +- docs/universal/getting-started.md | 45 ++++++++++++++++--------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 77be2930c..1f3d7ba77 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Status: Release candidate - **Observable based** - Use the power of RxJS, Angular, and Firebase. - **Real-time bindings** - Synchronize data in real-time. -- **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 in real-time. - **Offline Data** - Store data offline automatically with AngularFirestore. - **ngrx friendly** - Integrate with ngrx using AngularFire's action based APIs. diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index ecbe12443..4e7b11f5e 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -1,11 +1,11 @@ -# Server-side Rendering with Universal +# Getting started with Angularfire and Universal 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 @@ -20,18 +20,19 @@ ng generate universal --client-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 +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. +Create a file called `server.ts` in the root of you project, replacing `YOUR_PROJECT_NAME` with the name of your project. ```ts // These are important and needed before anything else 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'; @@ -39,8 +40,7 @@ import { readFileSync } from 'fs'; // 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(); @@ -48,23 +48,20 @@ enableProdMode(); // Express server const app = express(); -const PORT = process.env.PORT || 4000; const DIST_FOLDER = join(process.cwd(), 'dist'); const APP_NAME = 'YOUR_PROJECT_NAME'; -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)); @@ -77,6 +74,8 @@ app.get('*', (req, res) => { res.render(join(DIST_FOLDER, APP_NAME, 'index.html'), { req }); }); +const PORT = process.env.PORT || 4000; + // Start up the Node server app.listen(PORT, () => { console.log(`Node server listening on http://localhost:${PORT}`); @@ -85,7 +84,7 @@ app.listen(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, replacing `YOUR_PROJECT_NAME` with the name of your project. ```js @@ -99,9 +98,13 @@ module.exports = { resolve: { extensions: ['.js', '.ts'] }, mode: 'development', target: 'node', - externals: [/(node_modules|main\..*\.js)/], + externals: [ + /^firebase/ + ], output: { - path: path.join(__dirname, `dist/${APP_NAME}`), + path: path.join(__dirname, `dist/${APP_NAME}-webpack`), + library: 'app', + libraryTarget: 'umd', filename: '[name].js' }, module: { @@ -126,7 +129,7 @@ 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": { From 0abf5ae31b0ee3856b128fd61deab858436d7b6a Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 28 Aug 2018 00:38:28 -0700 Subject: [PATCH 03/13] docs(ssr): Rearrange a bit and start of prerendering --- README.md | 21 ++-------- docs/universal/cloud-functions.md | 66 +++++++++++++++++++++++++++++++ docs/universal/getting-started.md | 52 +++++++++++++----------- docs/universal/hosting-proxy.md | 1 - docs/universal/prerendering.md | 60 ++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 42 deletions(-) delete mode 100644 docs/universal/hosting-proxy.md create mode 100644 docs/universal/prerendering.md diff --git a/README.md b/README.md index 1f3d7ba77..c34f46d14 100644 --- a/README.md +++ b/README.md @@ -91,32 +91,19 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo ### Deploying your application -#### Static hosting on Firebase +#### Client-side rendering > 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 with Firebase Hosting](docs/deploying-angularfire-to-firebase.md) -#### Angular Universal +#### 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 with a process called server-side rendering (SSR). Angularfire is compatible with server-side rendering; allowing you to take advantage of the performance gains and SEO granted by the technology. -- [Getting started with Angularfire and Universal](docs/universal/getting-started.md) - -You have two options for deploying your Universal application, on-demand or pre-rendered (or a combination thereof): - -##### Render your application on-demand in response to HTTP requests - -> Server-side rendered pages can be generated on the server in response to a request from a web browser. - +- [Getting started with Angular Universal](docs/universal/getting-started.md) - [Deploying your Universal application on Cloud Functions for Firebase](docs/universal/cloud-functions.md) -- [Proxy Firebase Hosting to Cloud Functions for Firebase](docs/universal/hosting-proxy.md) - -#### Pre-rendering your application to serve statically - -> Server-side rendered pages can also be pre-generated and rendered statically on hosting. This allows you to avoid the costs and complexity of spinning up servers in response to requests, while maintaining most of the benefits. It is often preferable if you don't have a lot of dynamic content or it changes infrequently. - -- [Prerendering your Universal application and deploying on Firebase Hosting](docs/universal/prerendering.md) +- [Prerendering your Universal application for Firebase Hosting](docs/universal/prerendering.md) ### Ionic diff --git a/docs/universal/cloud-functions.md b/docs/universal/cloud-functions.md index e69de29bb..72ae57b34 100644 --- a/docs/universal/cloud-functions.md +++ b/docs/universal/cloud-functions.md @@ -0,0 +1,66 @@ +# 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:ssr": "ng build --prod && npm run build:hosting && npm run build:functions", + "build:hosting": "npm run copy:hosting", + "copy:hosting": "cp -r ./dist/afdocsite/* ./public && rm ./public/index.html", + "copy:functions": "rm -r ./dist && cp -r ../dist .", + "webpack:ssr": "webpack --config webpack.server.config.js", + "build:functions": "ng run YOUR_PROJECT_NAME:server && npm run webpack:ssr && npm run copy: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); +}); +``` + +Add the following to your `firebase.json`: + +```js +{ + // ... + "hosting": { + // ... + "rewrites": [ + { "source": "**", "function": "universal" } + ] + } +} +``` + +We can now `firebase serve` and `firebase deploy`. + +Something, something, cache-control... + +### [Optional Next Step: Prerendering your Universal application for Firebase Hosting](prerendering.md) + +## Additional Resources + +- [Universal Starter Template](https://github.com/angular/universal-starter) +- [AngularFirebase SSR Videos](https://angularfirebase.com/tag/ssr/) \ No newline at end of file diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index 4e7b11f5e..f482b5076 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -19,11 +19,11 @@ ng generate universal --client-project [ExpressJS](https://expressjs.com/) is a lightweight web framework that can serve http requests in Node. First, install the dev dependencies: -``` +```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, replacing `YOUR_PROJECT_NAME` with the name of your project. +Create a file called `server.ts` in the root of you project. ```ts // These are important and needed before anything else @@ -38,18 +38,18 @@ 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('xhr2'); // Faster renders in prod mode enableProdMode(); -// Express server -const app = express(); +// Export our express server +export const app = express(); const DIST_FOLDER = join(process.cwd(), 'dist'); -const APP_NAME = 'YOUR_PROJECT_NAME'; +const APP_NAME = 'YOUR_PROJECT_NAME'; // TODO: replace me! const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require(`./dist/${APP_NAME}-server/main`); @@ -74,24 +74,25 @@ app.get('*', (req, res) => { res.render(join(DIST_FOLDER, APP_NAME, 'index.html'), { req }); }); -const PORT = process.env.PORT || 4000; - -// 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 (!CLOUD_FUNCTIONS) { + 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, replacing `YOUR_PROJECT_NAME` with the name of your project. +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! module.exports = { entry: { server: './server.ts' }, @@ -99,9 +100,15 @@ module.exports = { mode: 'development', target: 'node', 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: { + // 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', @@ -134,24 +141,21 @@ Update your `package.json` with the following build scripts, replacing `YOUR_PRO ```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:ssr": "ng build --prod && ng run YOUR_PROJECT_NAME:server", + "serve:ssr": "node dist/YOUR_PROJECT_NAME/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). - +At this point we could deploy our 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 +But we have another option, deploying the application to Firebase's serverless environment: [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/). + +### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) -- [Universal Starter Template](https://github.com/angular/universal-starter) -- [AngularFirebase SSR Videos](https://angularfirebase.com/tag/ssr/) \ No newline at end of file +*Or if you're only insterested in pre-rendering, skip to [**Prerendering your Universal application for Firebase Hosting**](prerendering.md)* \ No newline at end of file diff --git a/docs/universal/hosting-proxy.md b/docs/universal/hosting-proxy.md deleted file mode 100644 index c8fd0311b..000000000 --- a/docs/universal/hosting-proxy.md +++ /dev/null @@ -1 +0,0 @@ -# TDB \ No newline at end of file diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md new file mode 100644 index 000000000..f00e7f94b --- /dev/null +++ b/docs/universal/prerendering.md @@ -0,0 +1,60 @@ +# Prerendering your Universal application for Firebase Hosting + +`static.paths.js`: + +```js +export default [ + 'index.html', + 'ANOTHER_PATH', + 'ANOTHER_PATH', + // ... etc. +]; +``` + +Add the following to your `server.ts`: + +```ts +import ROUTES from './static.paths'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { renderModuleFactory } from '@angular/platform-server'; + +if (PRERENDER) { + + 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 (!CLOUD_FUNCTIONS) { + + 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:ssr": "ng build --prod && npm run build:hosting && npm run build:functions", + "build:hosting": "npm run copy:hosting && PRERENDER=1 node dist/YOUR_PROJECT_NAME/server.js", +}, +``` \ No newline at end of file From a28b96d747cffacf625e16f554489d43d9acb77a Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 28 Aug 2018 00:47:38 -0700 Subject: [PATCH 04/13] docs(): remove extra real-time add missing modules --- README.md | 2 +- docs/install-and-setup.md | 5 +++-- docs/ionic/cli.md | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c34f46d14..7085f8af9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Status: Release candidate - **Observable based** - Use the power of RxJS, Angular, and Firebase. - **Real-time bindings** - Synchronize data in real-time. -- **Authentication** - Log users in with a variety of providers and monitor authentication state 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. diff --git a/docs/install-and-setup.md b/docs/install-and-setup.md index 186d13448..7c9406e84 100644 --- a/docs/install-and-setup.md +++ b/docs/install-and-setup.md @@ -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 diff --git a/docs/ionic/cli.md b/docs/ionic/cli.md index fbf35391a..743d02bb8 100644 --- a/docs/ionic/cli.md +++ b/docs/ionic/cli.md @@ -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 From 3b3d4ac4844ec501c5f9d51701881862f84abdc1 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 28 Aug 2018 14:00:10 -0700 Subject: [PATCH 05/13] docs(ssr): Cleaned up the scripts --- docs/universal/cloud-functions.md | 20 +++++++---- docs/universal/getting-started.md | 18 ++++------ docs/universal/prerendering.md | 55 +++++++++++++++++-------------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/docs/universal/cloud-functions.md b/docs/universal/cloud-functions.md index 72ae57b34..1a2812cda 100644 --- a/docs/universal/cloud-functions.md +++ b/docs/universal/cloud-functions.md @@ -23,12 +23,9 @@ Let's make some modifications to our `package.json`, to build for Functions. ```js "scripts": { // ... omitted - "build:ssr": "ng build --prod && npm run build:hosting && npm run build:functions", - "build:hosting": "npm run copy:hosting", - "copy:hosting": "cp -r ./dist/afdocsite/* ./public && rm ./public/index.html", - "copy:functions": "rm -r ./dist && cp -r ../dist .", - "webpack:ssr": "webpack --config webpack.server.config.js", - "build:functions": "ng run YOUR_PROJECT_NAME:server && npm run webpack:ssr && npm run copy:functions && npm run --prefix functions build" + "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" }, ``` @@ -40,6 +37,15 @@ export const universal = functions.https.onRequest((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 @@ -54,7 +60,7 @@ Add the following to your `firebase.json`: } ``` -We can now `firebase serve` and `firebase deploy`. +We can now run `npm run build`, `firebase serve` and `firebase deploy`. Something, something, cache-control... diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index f482b5076..924f3b507 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -75,7 +75,7 @@ app.get('*', (req, res) => { }); // If we're not in the Cloud Functions environment, spin up a Node server -if (!CLOUD_FUNCTIONS) { +if (!process.env.FUNCTION_NAME) { const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`Node server listening on http://localhost:${PORT}`); @@ -141,20 +141,14 @@ Update your `package.json` with the following build scripts, replacing `YOUR_PRO ```js "scripts": { // ... omitted - "build:ssr": "ng build --prod && ng run YOUR_PROJECT_NAME:server", - "serve:ssr": "node dist/YOUR_PROJECT_NAME/server.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`. - -At this point we could deploy our 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. - -But we have another option, deploying the application to Firebase's serverless environment: [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/). +Test your app locally by running `npm run build && npm run serve:ssr`. ### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md index f00e7f94b..0b7f5502a 100644 --- a/docs/universal/prerendering.md +++ b/docs/universal/prerendering.md @@ -4,48 +4,53 @@ ```js export default [ - 'index.html', - 'ANOTHER_PATH', - 'ANOTHER_PATH', + '/', + '/another_path', + '/yet_another_path', // ... etc. ]; ``` +```bash +npm i --save-dev mkdir-recursive +``` + Add the following to your `server.ts`: ```ts -import ROUTES from './static.paths'; -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { readFileSync, writeFileSync, existsSync } from 'fs'; import { renderModuleFactory } from '@angular/platform-server'; +import { mkdirSync } from 'mkdir-recursive'; -if (PRERENDER) { +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]) + 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); + const fullPath = join('./public', route); + if (!existsSync(fullPath)) { mkdirSync(fullPath); } + writeFileSync(join(fullPath, 'index.html'), html); }); process.exit(); }); -} else if (!CLOUD_FUNCTIONS) { - - const PORT = process.env.PORT || 4000; - app.listen(PORT, () => { - console.log(`Node server listening on http://localhost:${PORT}`); - }); +} 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}`); + }); } ``` @@ -54,7 +59,7 @@ Let's make some modifications to our `package.json`, to prerender your content: ```js "scripts": { // ... omitted - "build:ssr": "ng build --prod && npm run build:hosting && npm run build:functions", - "build:hosting": "npm run copy:hosting && PRERENDER=1 node dist/YOUR_PROJECT_NAME/server.js", + "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", }, ``` \ No newline at end of file From f7c87fb77f14dad623ee0cf8d3a3e15f6ac0819c Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 28 Aug 2018 14:18:38 -0700 Subject: [PATCH 06/13] docs(ssr): Language changes and indenting --- README.md | 8 +++----- docs/universal/cloud-functions.md | 2 +- docs/universal/prerendering.md | 10 +++++----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7085f8af9..dd3e5cba3 100644 --- a/README.md +++ b/README.md @@ -91,19 +91,17 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo ### Deploying your application -#### Client-side rendering - > 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 with 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 with a process called server-side rendering (SSR). Angularfire is compatible with server-side rendering; allowing you to take advantage of the performance gains and SEO granted by the technology. +> 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 for Firebase Hosting](docs/universal/prerendering.md) +- [Prerendering your Universal application](docs/universal/prerendering.md) ### Ionic diff --git a/docs/universal/cloud-functions.md b/docs/universal/cloud-functions.md index 1a2812cda..de99dc4d6 100644 --- a/docs/universal/cloud-functions.md +++ b/docs/universal/cloud-functions.md @@ -64,7 +64,7 @@ We can now run `npm run build`, `firebase serve` and `firebase deploy`. Something, something, cache-control... -### [Optional Next Step: Prerendering your Universal application for Firebase Hosting](prerendering.md) +### [Next Step: Prerendering your Universal application](prerendering.md) ## Additional Resources diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md index 0b7f5502a..7a13f115a 100644 --- a/docs/universal/prerendering.md +++ b/docs/universal/prerendering.md @@ -1,4 +1,4 @@ -# Prerendering your Universal application for Firebase Hosting +# Prerendering your Universal application `static.paths.js`: @@ -31,15 +31,15 @@ if (process.env.PRERENDER) { document: template, url: route, extraProviders: [ - provideModuleMap(LAZY_MODULE_MAP) + 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); + const fullPath = join('./public', route); + if (!existsSync(fullPath)) { mkdirSync(fullPath); } + writeFileSync(join(fullPath, 'index.html'), html); }); process.exit(); }); From d81f4ce9a2acb52415290658924e3f661e555546 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 28 Aug 2018 14:28:44 -0700 Subject: [PATCH 07/13] docs(ssr): now fairly trivial, drop the optional --- docs/universal/getting-started.md | 4 +--- docs/universal/prerendering.md | 6 +++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index 924f3b507..049aae72f 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -150,6 +150,4 @@ Update your `package.json` with the following build scripts, replacing `YOUR_PRO Test your app locally by running `npm run build && npm run serve:ssr`. -### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) - -*Or if you're only insterested in pre-rendering, skip to [**Prerendering your Universal application for Firebase Hosting**](prerendering.md)* \ No newline at end of file +### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) \ No newline at end of file diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md index 7a13f115a..114a3362c 100644 --- a/docs/universal/prerendering.md +++ b/docs/universal/prerendering.md @@ -62,4 +62,8 @@ Let's make some modifications to our `package.json`, to prerender your content: "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", }, -``` \ No newline at end of file +``` + +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` \ No newline at end of file From 77e20e2debb8b96ebd22be71c44500d85e25bd22 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 6 Sep 2018 19:39:26 -0700 Subject: [PATCH 08/13] Fixed realtime references --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 182281df9..8da57864a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ## What is AngularFire? - **Observable based** - Use the power of RxJS, Angular, and Firebase. -- **Real-time bindings** - Synchronize data in real-time. +- **Realtime bindings** - Synchronize data 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. @@ -79,7 +79,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 real-time. +> `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. - [Objects](docs/rtdb/objects.md) - [Lists](docs/rtdb/lists.md) From e758812a1bdc3aa0bd2ea7992e0c8e8979c33248 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 6 Sep 2018 19:41:20 -0700 Subject: [PATCH 09/13] Reference to angularfire2 --- docs/universal/getting-started.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index 049aae72f..794b5427a 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -5,7 +5,7 @@ Server-side rendering (SSR) is the process of converting a JavaScript app to pla ## 0. Prerequisites - @angular/cli >= v6.0 -- angularfire2 >= v5.0.0-rc.12 +- @angular/fire >= v5.0.0 ## 1. Generate the Angular Universal Server Module @@ -150,4 +150,4 @@ Update your `package.json` with the following build scripts, replacing `YOUR_PRO Test your app locally by running `npm run build && npm run serve:ssr`. -### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) \ No newline at end of file +### [Next Step: Deploying your Universal application on Cloud Functions for Firebase](cloud-functions.md) From 0a34424ac419c17aa457ab04ec28973dd7f581f0 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 6 Sep 2018 20:02:04 -0700 Subject: [PATCH 10/13] Flushing out the Cloud Functions instructions --- docs/universal/cloud-functions.md | 61 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/universal/cloud-functions.md b/docs/universal/cloud-functions.md index de99dc4d6..62c1269ab 100644 --- a/docs/universal/cloud-functions.md +++ b/docs/universal/cloud-functions.md @@ -1,24 +1,41 @@ # Deploying your Universal application on Cloud Functions for Firebase -Pre-req, previous page, something, something, firebase cli +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. -```bash -npm i -g @firebase-tools -``` +> 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 ``` -Init Firebase if you haven't: +Then inside your project root, setup your Firebase CLI project: ```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. +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 make some modifications to our `package.json`, to build for Functions. +Let's go ahead and modify your `package.json` to build for Cloud Functions: ```js "scripts": { @@ -29,14 +46,6 @@ Let's make some modifications to our `package.json`, to build for Functions. }, ``` -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 @@ -46,27 +55,21 @@ Change the build script in your `functions/package.json` to the following: } ``` -Add the following to your `firebase.json`: +Finally, add the following to your `functions/src/index.ts`: -```js -{ - // ... - "hosting": { - // ... - "rewrites": [ - { "source": "**", "function": "universal" } - ] - } -} +```ts +export const universal = functions.https.onRequest((request, response) => { + require(`${process.cwd()}/dist/YOUR_PROJECT_NAME-webpack/server`).app(request, response); +}); ``` -We can now run `npm run build`, `firebase serve` and `firebase deploy`. +We you should now be able to run `npm run build` to build your project for Firebase Hosting and Cloud Functions. -Something, something, cache-control... +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/) \ No newline at end of file +- [AngularFirebase SSR Videos](https://angularfirebase.com/tag/ssr/) From b18b2bd68330355274a06511885f577d0aa8c62e Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 6 Sep 2018 20:11:33 -0700 Subject: [PATCH 11/13] Flush out the prerendering doc --- docs/universal/prerendering.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md index 114a3362c..cc8422a02 100644 --- a/docs/universal/prerendering.md +++ b/docs/universal/prerendering.md @@ -1,21 +1,24 @@ # Prerendering your Universal application -`static.paths.js`: +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', - // ... etc. + '/yet_another_path' ]; ``` +Let's install `mkdir-recursive` to make the next step a little easier: + ```bash npm i --save-dev mkdir-recursive ``` -Add the following to your `server.ts`: +Now replace the listener in your `server.ts` with the following: ```ts import { readFileSync, writeFileSync, existsSync } from 'fs'; @@ -27,13 +30,13 @@ 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]) + renderModuleFactory(AppServerModuleNgFactory, { + document: template, + url: route, + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => [route, html]) ) ).then(results => { results.forEach(([route, html]) => { @@ -54,7 +57,9 @@ if (process.env.PRERENDER) { } ``` -Let's make some modifications to our `package.json`, to prerender your content: +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 then, and then 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": { @@ -64,6 +69,4 @@ Let's make some modifications to our `package.json`, to prerender your content: }, ``` -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` \ No newline at end of file +Now when you run `npm run build` the prerendered content should be available in your `/public` directory, ready for deployment on Firebase Hosting. From dfaaac880d2829a893d471a2ee3811b79fcfaff3 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 6 Sep 2018 20:12:35 -0700 Subject: [PATCH 12/13] Render them... --- docs/universal/prerendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/universal/prerendering.md b/docs/universal/prerendering.md index cc8422a02..2bb0689e2 100644 --- a/docs/universal/prerendering.md +++ b/docs/universal/prerendering.md @@ -57,7 +57,7 @@ if (process.env.PRERENDER) { } ``` -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 then, and then write them to your `public` directory. *You could always make this a seperate script.* +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: From 94d9b712ce1dd8d01e82dad7ed275a5cbbf49b76 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 11 Sep 2018 15:01:43 -0700 Subject: [PATCH 13/13] AngularFire case --- docs/universal/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/universal/getting-started.md b/docs/universal/getting-started.md index 794b5427a..cde5ed5be 100644 --- a/docs/universal/getting-started.md +++ b/docs/universal/getting-started.md @@ -1,4 +1,4 @@ -# Getting started with Angularfire and Universal +# Getting started with AngularFire and Universal 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.