-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Changes from 7 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,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/) |
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 | ||
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. 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 | ||
|
||
|
@@ -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,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` |
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.
"realtime" over "real-time" this goes way back to the early Firebase days.