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
Changes from 13 commits
c92e538
8ad0fe7
0abf5ae
a28b96d
3b3d4ac
f7c87fb
d81f4ce
f825807
77e20e2
e758812
0a34424
b18b2bd
dfaaac8
94d9b71
264ae4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/) |
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 | ||
|
||
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 | ||
|
||
|
@@ -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. | ||
|
@@ -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)); | ||
|
@@ -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! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need more context around There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
@@ -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mega nit: AngularFire