docs: update SSR doc to reflect version 17 (#52398)

Changes to SSR doc

PR Close #52398
This commit is contained in:
Alan Agius 2023-10-26 16:17:34 +00:00 committed by Alex Rickabaugh
parent f1a020b511
commit cb8a741f14
107 changed files with 415 additions and 2686 deletions

View file

@ -520,10 +520,8 @@ groups:
contains_any_globs(files, [
'packages/platform-server/**/{*,.*}',
'aio/content/guide/prerendering.md',
'aio/content/guide/universal.md',
'aio/content/examples/universal/**/{*,.*}',
'aio/content/guide/universal-ngmodule.md',
'aio/content/examples/universal-ngmodule/**/{*,.*}'
'aio/content/guide/ssr.md',
'aio/content/examples/ssr/**/{*,.*}',
])
reviewers:
users:

View file

@ -152,7 +152,7 @@ Join the conversation and help the community.
[forms]: https://angular.io/guide/forms-overview
[api]: https://angular.io/api
[angularelements]: https://angular.io/guide/elements
[ssr]: https://angular.io/guide/universal
[ssr]: https://angular.io/guide/ssr
[schematics]: https://angular.io/guide/schematics
[lazyloading]: https://angular.io/guide/lazy-loading-ngmodules
[node.js]: https://nodejs.org/

View file

@ -7,7 +7,7 @@ When creating components, Angular generates a unique component ID for each compo
Component IDs are used in Angular internally:
- for extra annotations of DOM nodes for style encapsulation
- during [hydration](guide/hydration) to restore an application state after [server-side rendering](guide/universal).
- during [hydration](guide/hydration) to restore an application state after [server-side rendering](guide/ssr).
To avoid issues that might be caused by the component ID collision, it's recommended to resolve them as described below.
@ -32,9 +32,9 @@ Having these two components defined will trigger an ID generation collision and
The warning message includes the class name of the two components whose IDs are colliding.
The problem can be resolved using one of the solutions below:
The problem can be resolved using one of the solutions below:
1. Change the selector of one of the two components. For example by using a pseudo-selector with no effect like `:not()` and a different tag name.
1. Change the selector of one of the two components. For example by using a pseudo-selector with no effect like `:not()` and a different tag name.
```typescript
@Component({

View file

@ -103,8 +103,7 @@ EXAMPLES = {
"toh-pt5": {"stackblitz": True, "zip": True},
"toh-pt6": {"stackblitz": True, "zip": True},
"two-way-binding": {"stackblitz": True, "zip": True},
"universal": {"stackblitz": False, "zip": True},
"universal-ngmodule": {"stackblitz": False, "zip": True},
"ssr": {"stackblitz": False, "zip": True},
"upgrade-lazy-load-ajs": {"stackblitz": False, "zip": True},
"upgrade-module": {"stackblitz": False, "zip": False},
"upgrade-phonecat-1-typescript": {"stackblitz": False, "zip": False},

View file

@ -3,5 +3,5 @@ load("//aio/content/examples:examples.bzl", "docs_example")
package(default_visibility = ["//visibility:public"])
docs_example(
name = "universal",
name = "ssr",
)

View file

@ -1,7 +1,7 @@
{
"projectType": "universal",
"projectType": "ssr",
"e2e": [
{"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]},
{"cmd": "yarn", "args": ["run", "build:ssr"]}
{"cmd": "yarn", "args": ["build"]}
]
}

View file

@ -0,0 +1,9 @@
# Instructions for Angular SSR Example Download
This is the downloaded sample code for the [Angular SSR (Standalone) guide](https://angular.io/guide/ssr).
## Install and Run
1. `npm install` to install the `node_module` packages
2. `ng serve` to launch the dev-server
3. Launch the browser to `http://localhost:4200`

View file

@ -0,0 +1,68 @@
// #docplaster
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
// #docregion CommonEngine
const commonEngine = new CommonEngine();
// #enddocregion CommonEngine
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// #docregion data-request
// TODO: implement data requests securely
// Serve data from URLS that begin "/api/"
server.get('/api/**', (req, res) => {
res.status(404).send('data requests are not yet supported');
});
// #enddocregion data-request
// #docregion static
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
// #enddocregion static
// #docregion navigation-request
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
// #enddocregion navigation-request
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
run();

View file

@ -1,30 +1,22 @@
// #docplaster
import { importProvidersFrom } from '@angular/core';
import { provideProtractorTestingSupport } from '@angular/platform-browser';
// #docregion client-hydration
import { provideClientHydration} from '@angular/platform-browser';
// #enddocregion client-hydration
// #docregion core
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';
// #enddocregion core
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
// #docregion client-hydration, core
export const appConfig: ApplicationConfig = {
providers: [
// #enddocregion client-hydration
provideRouter(routes),
provideHttpClient(),
// #enddocregion core
// #docregion client-hydration
// TODO: Enable using Fetch API when disabling `HttpClientInMemoryWebApiModule`.
provideHttpClient(/* withFetch()*/ ),
provideClientHydration(),
// #enddocregion client-hydration
provideProtractorTestingSupport(), // essential for e2e testing
// #docregion in-mem
@ -38,9 +30,6 @@ export const appConfig: ApplicationConfig = {
)
),
// #enddocregion in-mem
// #docregion client-hydration
// ...
// #docregion core
],
};
// #enddocregion client-hydration, core

View file

@ -2,8 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
// #docregion bootstrap
// Parallels the client-side bootstrapping call in `main.ts`
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
// #enddocregion bootstrap

View file

@ -2,7 +2,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
// #docregion bootstrap
bootstrapApplication(AppComponent, appConfig)
// #enddocregion bootstrap
.catch(err => console.error(err));

View file

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
},
"files": [
"src/main.ts",
"src/main.server.ts",
"server.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View file

@ -1,71 +0,0 @@
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// #docregion ngExpressEngine
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
// #enddocregion ngExpressEngine
server.set('view engine', 'html');
server.set('views', distFolder);
// #docregion data-request
// TODO: implement data requests securely
server.get('/api/**', (req, res) => {
res.status(404).send('data requests are not yet supported');
});
// #enddocregion data-request
// #docregion static
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// #enddocregion static
// #docregion navigation-request
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
// #enddocregion navigation-request
return server;
}
function run(): void {
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

View file

@ -1,19 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

View file

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Tour of Heroes';
}

View file

@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
],
providers: [
// Add server-only providers here.
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

View file

@ -1,55 +0,0 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroSearchComponent } from './hero-search/hero-search.component';
import { MessagesComponent } from './messages/messages.component';
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
declarations: [
AppComponent,
DashboardComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
HeroSearchComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule {
constructor(
// eslint-disable-next-line @typescript-eslint/ban-types
@Inject(PLATFORM_ID) private platformId: object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ?
'in the browser' : 'on the server';
console.log(`Running ${platform} with appId=${appId}`);
}
}

View file

@ -1,23 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}

View file

@ -1,40 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
@Input() hero!: Hero;
constructor(
private route: ActivatedRoute,
private heroService: HeroService,
private location: Location
) {}
ngOnInit(): void {
this.getHero();
}
getHero(): void {
const id = parseInt(this.route.snapshot.paramMap.get('id')!, 10);
this.heroService.getHero(id)
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}

View file

@ -1,40 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes$!: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}

View file

@ -1,39 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
}

View file

@ -1,13 +0,0 @@
import { Component } from '@angular/core';
import { MessageService } from '../message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent {
constructor(public messageService: MessageService) {}
}

View file

@ -1 +0,0 @@
export { AppServerModule } from './app/app.module.server';

View file

@ -1,7 +0,0 @@
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

View file

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/app-server",
"types": ["node"]
},
"files": [
"server.ts"
]
}

View file

@ -1,7 +0,0 @@
load("//aio/content/examples:examples.bzl", "docs_example")
package(default_visibility = ["//visibility:public"])
docs_example(
name = "universal",
)

View file

@ -1,300 +0,0 @@
import { browser, by, element, ElementArrayFinder, ElementFinder, logging } from 'protractor';
class Hero {
constructor(public id: number, public name: string) {}
// Factory methods
// Hero from string formatted as '<id> <name>'.
static fromString(s: string): Hero {
return new Hero(
+s.substring(0, s.indexOf(' ')),
s.slice(s.indexOf(' ') + 1),
);
}
// Hero from hero list <li> element.
static async fromLi(li: ElementFinder): Promise<Hero> {
const stringsFromA = await li.all(by.css('a')).getText();
const strings = stringsFromA[0].split(' ');
return { id: +strings[0], name: strings[1] };
}
// Hero id and name from the given detail element.
static async fromDetail(detail: ElementFinder): Promise<Hero> {
// Get hero id from the first <div>
const id = await detail.all(by.css('div')).first().getText();
// Get name from the h2
const name = await detail.element(by.css('h2')).getText();
return {
id: +id.slice(id.indexOf(' ') + 1),
name: name.substring(0, name.lastIndexOf(' '))
};
}
}
describe('Universal', () => {
const expectedH1 = 'Tour of Heroes';
const expectedTitle = `${expectedH1}`;
const targetHero = { id: 15, name: 'Magneta' };
const targetHeroDashboardIndex = 2;
const nameSuffix = 'X';
const newHeroName = targetHero.name + nameSuffix;
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
const severeLogs = logs.filter(entry => entry.level === logging.Level.SEVERE);
expect(severeLogs).toEqual([]);
});
describe('Initial page', () => {
beforeAll(() => browser.get(''));
it(`has title '${expectedTitle}'`, async () => {
expect(await browser.getTitle()).toEqual(expectedTitle);
});
it(`has h1 '${expectedH1}'`, async () => {
await expectHeading(1, expectedH1);
});
const expectedViewNames = ['Dashboard', 'Heroes'];
it(`has views ${expectedViewNames}`, async () => {
const viewNames = await getPageElts().navElts.map(el => el!.getText());
expect(viewNames).toEqual(expectedViewNames);
});
it('has dashboard as the active view', async () => {
const page = getPageElts();
expect(await page.appDashboard.isPresent()).toBeTruthy();
});
});
describe('Dashboard tests', () => {
beforeAll(() => browser.get(''));
it('has top heroes', async () => {
const page = getPageElts();
expect(await page.topHeroes.count()).toEqual(4);
});
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`cancels and shows ${targetHero.name} in Dashboard`, async () => {
await element(by.buttonText('go back')).click();
await browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(await targetHeroElt.getText()).toEqual(targetHero.name);
});
it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`saves and shows ${newHeroName} in Dashboard`, async () => {
await element(by.buttonText('save')).click();
await browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(await targetHeroElt.getText()).toEqual(newHeroName);
});
});
describe('Heroes tests', () => {
beforeAll(() => browser.get(''));
it('can switch to Heroes view', async () => {
await getPageElts().appHeroesHref.click();
const page = getPageElts();
expect(await page.appHeroes.isPresent()).toBeTruthy();
expect(await page.allHeroes.count()).toEqual(9, 'number of heroes');
});
it('can route to hero details', async () => {
await getHeroLiEltById(targetHero.id).click();
const page = getPageElts();
expect(await page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
const hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(targetHero.name.toUpperCase());
});
it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);
it(`shows ${newHeroName} in Heroes list`, async () => {
await element(by.buttonText('save')).click();
await browser.waitForAngular();
const expectedText = `${targetHero.id} ${newHeroName}`;
expect(await getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);
});
it(`deletes ${newHeroName} from Heroes list`, async () => {
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
const li = getHeroLiEltById(targetHero.id);
await li.element(by.buttonText('x')).click();
const page = getPageElts();
expect(await page.appHeroes.isPresent()).toBeTruthy();
expect(await page.allHeroes.count()).toEqual(8, 'number of heroes');
const heroesAfter = await toHeroArray(page.allHeroes);
// console.log(await Hero.fromLi(page.allHeroes[0]));
const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName);
expect(heroesAfter).toEqual(expectedHeroes);
// expect(await page.selectedHeroSubview.isPresent()).toBeFalsy();
});
it(`adds back ${targetHero.name}`, async () => {
const updatedHeroName = 'Alice';
const heroesBefore = await toHeroArray(getPageElts().allHeroes);
const numHeroes = heroesBefore.length;
await element(by.css('input')).sendKeys(updatedHeroName);
await element(by.buttonText('add')).click();
const page = getPageElts();
const heroesAfter = await toHeroArray(page.allHeroes);
expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');
expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');
const maxId = heroesBefore[heroesBefore.length - 1].id;
expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: updatedHeroName});
});
it('displays correctly styled buttons', async () => {
const buttons = await element.all(by.buttonText('x'));
for (const button of buttons) {
// Inherited styles from styles.css
expect(await button.getCssValue('font-family')).toBe('Arial, sans-serif');
expect(await button.getCssValue('border')).toContain('none');
expect(await button.getCssValue('padding')).toBe('5px 10px');
expect(await button.getCssValue('border-radius')).toBe('4px');
// Styles defined in heroes.component.css
expect(await button.getCssValue('right')).toBe('0px');
expect(await button.getCssValue('top')).toBe('0px');
expect(await button.getCssValue('bottom')).toBe('0px');
}
const addButton = element(by.buttonText('add'));
// Inherited styles from styles.css
expect(await addButton.getCssValue('font-family')).toBe('Arial, sans-serif');
expect(await addButton.getCssValue('border')).toContain('none');
expect(await addButton.getCssValue('padding')).toBe('5px 10px');
expect(await addButton.getCssValue('border-radius')).toBe('4px');
});
});
describe('Progressive hero search', () => {
beforeAll(() => browser.get(''));
it(`searches for 'Ma'`, async () => {
await getPageElts().searchBox.sendKeys('Ma');
await browser.sleep(1000);
expect(await getPageElts().searchResults.count()).toBe(4);
});
it(`continues search with 'g'`, async () => {
await getPageElts().searchBox.sendKeys('g');
await browser.sleep(1000);
expect(await getPageElts().searchResults.count()).toBe(2);
});
it(`continues search with 'n' and gets ${targetHero.name}`, async () => {
await getPageElts().searchBox.sendKeys('n');
await browser.sleep(1000);
const page = getPageElts();
expect(await page.searchResults.count()).toBe(1);
const hero = page.searchResults.get(0);
expect(await hero.getText()).toEqual(targetHero.name);
});
it(`navigates to ${targetHero.name} details view`, async () => {
const hero = getPageElts().searchResults.get(0);
expect(await hero.getText()).toEqual(targetHero.name);
await hero.click();
const page = getPageElts();
expect(await page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
const hero2 = await Hero.fromDetail(page.heroDetail);
expect(hero2.id).toEqual(targetHero.id);
expect(hero2.name).toEqual(targetHero.name.toUpperCase());
});
});
// Helpers
async function addToHeroName(text: string): Promise<void> {
await element(by.css('input')).sendKeys(text);
}
async function dashboardSelectTargetHero(): Promise<void> {
const targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);
expect(await targetHeroElt.getText()).toEqual(targetHero.name);
await targetHeroElt.click();
await browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6
const page = getPageElts();
expect(await page.heroDetail.isPresent()).toBeTruthy('shows hero detail');
const hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(targetHero.name.toUpperCase());
}
async function expectHeading(hLevel: number, expectedText: string): Promise<void> {
const hTag = `h${hLevel}`;
const hText = await element(by.css(hTag)).getText();
expect(hText).toEqual(expectedText, hTag);
}
function getHeroAEltById(id: number): ElementFinder {
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
return spanForId.element(by.xpath('..'));
}
function getHeroLiEltById(id: number): ElementFinder {
const spanForId = element(by.cssContainingText('li span.badge', id.toString()));
return spanForId.element(by.xpath('../..'));
}
function getPageElts() {
const navElts = element.all(by.css('app-root nav a'));
return {
navElts,
appDashboardHref: navElts.get(0),
appDashboard: element(by.css('app-root app-dashboard')),
topHeroes: element.all(by.css('app-root app-dashboard > div h4')),
appHeroesHref: navElts.get(1),
appHeroes: element(by.css('app-root app-heroes')),
allHeroes: element.all(by.css('app-root app-heroes li')),
selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')),
heroDetail: element(by.css('app-root app-hero-detail > div')),
searchBox: element(by.css('#search-box')),
searchResults: element.all(by.css('.search-result li'))
};
}
async function toHeroArray(allHeroes: ElementArrayFinder): Promise<Hero[]> {
return await allHeroes.map(hero => Hero.fromLi(hero!));
}
async function updateHeroNameInDetailView(): Promise<void> {
// Assumes that the current view is the hero details view.
await addToHeroName(nameSuffix);
const page = getPageElts();
const hero = await Hero.fromDetail(page.heroDetail);
expect(hero.id).toEqual(targetHero.id);
expect(hero.name).toEqual(newHeroName.toUpperCase());
}
});

View file

@ -1,7 +0,0 @@
{
"projectType": "universal",
"e2e": [
{"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]},
{"cmd": "yarn", "args": ["run", "build:ssr"]}
]
}

View file

@ -1,9 +0,0 @@
# Instructions for Angular Universal Example Download
This is the downloaded sample code for the [Angular Universal (Standalone) guide](https://angular.io/guide/universal).
## Install and Run
1. `npm install` to install the `node_module` packages
2. `npm run dev:ssr` to launch the server and application
3. Launch the browser to `http://localhost:4200`

View file

@ -1,85 +0,0 @@
// #docplaster
// #docregion core
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import bootstrap from './src/main.server';
export default bootstrap;
// #enddocregion core
// The Express app is exported so that it can be used by serverless Functions.
// #docregion core
export function app(): express.Express {
const server = express();
const distFolder = join(process.cwd(), 'dist/browser');
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Angular's Universal express-engine
// #enddocregion core
// (see https://github.com/angular/universal/tree/main/modules/express-engine)
// #docregion core, ngExpressEngine
server.engine('html', ngExpressEngine({ bootstrap }));
// #enddocregion ngExpressEngine
server.set('view engine', 'html');
server.set('views', distFolder);
// #enddocregion core
// #docregion data-request
// TODO: implement data requests securely
// Serve data from URLS that begin "/api/"
server.get('/api/**', (req, res) => {
res.status(404).send('data requests are not yet supported');
});
// #enddocregion data-request
// #docregion static
// Serve static files (files with an extension) from the dist folder only
server.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
// #enddocregion static
// #docregion navigation-request
// All regular routes (no file extension) are rendered with the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
});
// #enddocregion navigation-request
// #docregion core
// ...
return server;
}
// #enddocregion core
function run(): void {
// eslint-disable-next-line @typescript-eslint/dot-notation, dot-notation
const port = process.env['PORT'] || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

View file

@ -1,23 +0,0 @@
/* AppComponent's private CSS styles */
h1 {
font-size: 1.2em;
margin-bottom: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #334953;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
}

View file

@ -1,7 +0,0 @@
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

View file

@ -1,61 +0,0 @@
/* DashboardComponent's private CSS styles */
[class*='col-'] {
float: left;
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *::after, *::before {
box-sizing: border-box;
}
h3 {
text-align: center;
margin-bottom: 0;
}
h4 {
position: relative;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #3f525c;
border-radius: 2px;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}

View file

@ -1,11 +0,0 @@
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4"
routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
<app-hero-search></app-hero-search>

View file

@ -1,30 +0,0 @@
/* HeroDetailComponent's private CSS styles */
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial, sans-serif;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}

View file

@ -1,11 +0,0 @@
<div *ngIf="hero">
<h2>{{hero.name | uppercase}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label for="name">name:
<input id="name" [(ngModel)]="hero.name" placeholder="name"/>
</label>
</div>
<button type="button" (click)="goBack()">go back</button>
<button type="button" (click)="save()">save</button>
</div>

View file

@ -1,39 +0,0 @@
/* HeroSearch private styles */
.search-result li {
border-bottom: 1px solid gray;
border-left: 1px solid gray;
border-right: 1px solid gray;
width: 195px;
height: 16px;
padding: 5px;
background-color: white;
cursor: pointer;
list-style-type: none;
}
.search-result li:hover {
background-color: #607D8B;
}
.search-result li a {
color: #888;
display: block;
text-decoration: none;
}
.search-result li a:hover {
color: white;
}
.search-result li a:active {
color: white;
}
#search-box {
width: 200px;
height: 20px;
}
ul.search-result {
margin-top: 0;
padding-left: 0;
}

View file

@ -1,13 +0,0 @@
<div id="search-component">
<h4><label for="search-box">Hero Search</label></h4>
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>

View file

@ -1,124 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { MessageService } from './message.service';
@Injectable({ providedIn: 'root' })
export class HeroService {
private heroesUrl = 'api/heroes'; // URL to web api
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
constructor(
private http: HttpClient,
private messageService: MessageService) { }
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
/** GET hero by id. Return `undefined` when id not found */
getHeroNo404<Data>(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? 'fetched' : 'did not find';
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return empty hero array.
return of([]);
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero(hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero(hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
/** PUT: update the hero on the server */
updateHero(hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Handle Http operation that failed.
* Let the app continue.
*
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
}

View file

@ -1,4 +0,0 @@
export interface Hero {
id: number;
name: string;
}

View file

@ -1,76 +0,0 @@
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes a {
color: #333;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroes a:hover {
color: #607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-family: Arial, sans-serif;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background-color: gray !important;
color: white;
margin: 0;
}

View file

@ -1,21 +0,0 @@
<h2>My Heroes</h2>
<div>
<label for="name">Hero name:
<input id="name" #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button type="button" (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button type="button" class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</li>
</ul>

View file

@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from './hero';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 12, name: 'Dr. Nice' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr. IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}

View file

@ -1,14 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}

View file

@ -1,20 +0,0 @@
/* MessagesComponent's private CSS styles */
h2 {
color: red;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
button.clear {
font-family: Arial, sans-serif;
color: #333;
background-color: #eee;
margin-bottom: 12px;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #cfd8dc;
}

View file

@ -1,9 +0,0 @@
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button type="button"
class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>

View file

@ -1,13 +0,0 @@
import { Hero } from './hero';
export const HEROES: Hero[] = [
{ id: 12, name: 'Dr. Nice' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr. IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

View file

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tour of Heroes</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View file

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/app-server",
"types": ["node"]
},
"files": [
"server.ts"
]
}

View file

@ -1,7 +0,0 @@
{
"files": [
"!dist/",
"!**/*.d.ts",
"!**/src/**/*.js"
]
}

View file

@ -33,7 +33,7 @@ Most applications still need to access a server using the `HttpClient` to access
For some platforms and applications, you might also want to use the PWA \(Progressive Web App\) model to improve the user experience.
* [HTTP](guide/understanding-communicating-with-http): Communicate with a server to get data, save data, and invoke server-side actions with an HTTP client.
* [Server-side rendering](guide/universal): Angular Universal generates static application pages on the server through server-side rendering \(SSR\). This allows you to run your Angular application on the server in order to improve performance and show the first page quickly on mobile and low-powered devices, and also facilitate web crawlers.
* [Server-side rendering](guide/ssr): Angular Universal generates static application pages on the server through server-side rendering \(SSR\). This allows you to run your Angular application on the server in order to improve performance and show the first page quickly on mobile and low-powered devices, and also facilitate web crawlers.
* [Service workers and PWA](guide/service-worker-intro): Use a service worker to reduce dependency on the network and significantly improve the user experience.
* [Web workers](guide/web-worker): Learn how to run CPU-intensive computations in a background thread.

View file

@ -57,9 +57,9 @@ To get the most out of these developer guides, you should review the following t
<p>Learn more about how to use a web worker to run a CPU-intensive computation in a background thread.</p>
<p class="card-footer">Web Workers</p>
</a>
<a href="guide/universal" class="docs-card" title="Server-side rendering">
<a href="guide/ssr" class="docs-card" title="Server-side rendering">
<section>Server-side rendering</section>
<p>Learn more about how to use Angular Universal to create a static application page.</p>
<p>Learn more about how to use Angular SSR to create a static application page.</p>
<p class="card-footer">Server-side rendering</p>
</a>
<a href="guide/prerendering" class="docs-card" title="Pre-rendering">

View file

@ -717,7 +717,7 @@ It can also pre-generate pages as HTML files that you serve later.
This technique can improve performance on mobile and low-powered devices and improve the user experience by showing a static first page quickly while the client-side application is loading.
The static version can also make your application more visible to web crawlers.
You can easily prepare an application for server-side rendering by using the [Angular CLI][AioGuideGlossaryCommandLineInterfaceCli] to run the [Angular Universal][AioGuideGlossaryUniversal] tool, using the `@nguniversal/express-engine` [schematic][AioGuideGlossarySchematic].
You can easily prepare an application for server-side rendering by using the [Angular CLI][AioGuideGlossaryCommandLineInterfaceCli] `ng add @angular/ssr` command.
## service
@ -889,12 +889,12 @@ In development mode, Angular throws the `ExpressionChangedAfterItHasBeenCheckedE
To avoid this error, a [lifecycle hook][AioGuideLifecycleHooks] method that seeks to make such a change should trigger a new change detection run.
The new run follows the same direction as before, but succeeds in picking up the new value.
## Universal
## Server-side rendering
A tool for implementing [server-side rendering][AioGuideGlossaryServerSideRendering] of an Angular application.
When integrated with an app, Universal generates and serves static pages on the server in response to requests from browsers.
The initial static page serves as a fast-loading placeholder while the full application is being prepared for normal execution in the browser.
To learn more, see [Angular Universal: server-side rendering][AioGuideUniversal].
To learn more, see [Angular server-side rendering][AioGuideSSR].
## view
@ -1117,7 +1117,7 @@ Learn more about zones in this [Brian Ford video][YoutubeWatchV3iqtmusceU].
[AioGuideGlossaryTree]: guide/glossary#tree "tree - Glossary | Angular"
[AioGuideGlossaryTypescript]: guide/glossary#typescript "TypeScript - Glossary | Angular"
[AioGuideGlossaryU]: guide/glossary#unidirectional-data-flow "U - Glossary | Angular"
[AioGuideGlossaryUniversal]: guide/glossary#universal "Universal - Glossary | Angular"
[AioGuideGlossarySSR]: guide/glossary#server-side-rendering "Server-side rendering - Glossary | Angular"
[AioGuideGlossaryV]: guide/glossary#view "V - Glossary | Angular"
[AioGuideGlossaryView]: guide/glossary#view "view - Glossary | Angular"
[AioGuideGlossaryViewHierarchy]: guide/glossary#view-hierarchy "view hierarchy - Glossary | Angular"
@ -1168,7 +1168,7 @@ Learn more about zones in this [Brian Ford video][YoutubeWatchV3iqtmusceU].
[AioGuideTypescriptConfiguration]: guide/typescript-configuration "TypeScript configuration | Angular"
[AioGuideUniversal]: guide/universal "Server-side rendering (SSR) with Angular Universal | Angular"
[AioGuideSSR]: guide/ssr "Server-side rendering (SSR) with Angular | Angular"
[AioGuideWorkspaceConfig]: guide/workspace-config "Angular workspace configuration | Angular"
[AioGuideWorkspaceConfigProjectToolConfigurationOptions]: guide/workspace-config#project-tool-configuration-options "Project tool configuration options - Angular workspace configuration | Angular"

View file

@ -6,7 +6,7 @@ Hydration is the process that restores the server side rendered application on t
## Why is hydration important?
Hydration improves application performance by avoiding extra work to re-create DOM nodes. Instead, Angular tries to match existing DOM elements to the applications structure at runtime and reuses DOM nodes when possible. This results in a performance improvement that can be measured using [Core Web Vitals (CWV)](https://web.dev/learn-core-web-vitals/) statistics, such as reducing the First Input Delay ([FID](https://web.dev/fid/)) and Largest Contentful Paint ([LCP](https://web.dev/lcp/)), as well as Cumulative Layout Shift ([CLS](https://web.dev/cls/)). Improving these numbers also affects things like SEO performance.
Hydration improves application performance by avoiding extra work to re-create DOM nodes. Instead, Angular tries to match existing DOM elements to the applications structure at runtime and reuses DOM nodes when possible. This results in a performance improvement that can be measured using [Core Web Vitals (CWV)](https://web.dev/learn-core-web-vitals/) statistics, such as reducing the First-contentful paint [FCP](https://developer.chrome.com/en/docs/lighthouse/performance/first-contentful-paint/) and Largest Contentful Paint ([LCP](https://web.dev/lcp/)), as well as Cumulative Layout Shift ([CLS](https://web.dev/cls/)). Improving these numbers also affects things like SEO performance.
Without hydration enabled, server side rendered Angular applications will destroy and re-render the application's DOM, which may result in a visible UI flicker. This re-rendering can negatively impact [Core Web Vitals](https://web.dev/learn-core-web-vitals/) like [LCP](https://web.dev/lcp/) and cause a layout shift. Enabling hydration allows the existing DOM to be re-used and prevents a flicker.
@ -14,7 +14,7 @@ Without hydration enabled, server side rendered Angular applications will destro
## How do you enable hydration in Angular
Before you can get started with hydration, you must have a server side rendered (SSR) application. Follow the [Angular Universal Guide](/guide/universal) to enable server side rendering first. Once you have SSR working with your application, you can enable hydration by visiting your main app component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
Before you can get started with hydration, you must have a server side rendered (SSR) application. Follow the [Angular SSR Guide](/guide/ssr) to enable server side rendering first. Once you have SSR working with your application, you can enable hydration by visiting your main app component or module and importing `provideClientHydration` from `@angular/platform-browser`. You'll then add that provider to your app's bootstrapping providers list.
```typescript
import {
@ -62,7 +62,7 @@ After you've followed these steps and have started up your server, load your app
While running an application in dev mode, you can confirm hydration is enabled by opening the Developer Tools in your browser and viewing the console. You should see a message that includes hydration-related stats, such as the number of components and nodes hydrated.
<div class="alert is-helpful">
Angular calculates the stats based on all components rendered on a page, including those that come from third-party libraries.
</div>
@ -95,7 +95,7 @@ It is best to refactor your component to avoid this sort of DOM manipulation. Tr
### Valid HTML structure
There are a few cases where if you have a component template that does not have valid HTML structure, this could result in a DOM mismatch error during hydration.
There are a few cases where if you have a component template that does not have valid HTML structure, this could result in a DOM mismatch error during hydration.
As an example, here are some of the most common cases of this issue.

View file

@ -63,7 +63,7 @@ Marking an image as `priority` applies the following optimizations:
* Sets `fetchpriority=high` (read more about priority hints [here](https://web.dev/priority-hints))
* Sets `loading=eager` (read more about native lazy loading [here](https://web.dev/browser-level-image-lazy-loading))
* Automatically generates a [preload link element](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload) if [rendering on the server](/guide/universal).
* Automatically generates a [preload link element](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload) if [rendering on the server](/guide/ssr).
Angular logs an error during development if the LCP element is an image that does not have the `priority` attribute, as this can hurt loading performance significantly. A pages LCP element can vary based on a number of factors - such as the dimensions of a user's screen, so a page may have multiple images that should be marked `priority`. See [CSS for Web Vitals](https://web.dev/css-web-vitals/#images-and-largest-contentful-paint-lcp) for more details.
@ -95,13 +95,13 @@ When you add the `fill` attribute to your image, you do not need and should not
</code-example>
You can use the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property to change how the image will fill its container. If you style your image with `object-fit: "contain"`, the image will maintain its aspect ratio and be "letterboxed" to fit the element. If you set `object-fit: "cover"`, the element will retain its aspect ratio, fully fill the element, and some content may be "cropped" off.
You can use the [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) CSS property to change how the image will fill its container. If you style your image with `object-fit: "contain"`, the image will maintain its aspect ratio and be "letterboxed" to fit the element. If you set `object-fit: "cover"`, the element will retain its aspect ratio, fully fill the element, and some content may be "cropped" off.
See visual examples of the above at the [MDN object-fit documentation.](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)
You can also style your image with the [object-position property](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) to adjust its position within its containing element.
**Important note:** For the "fill" image to render properly, its parent element **must** be styled with `position: "relative"`, `position: "fixed"`, or `position: "absolute"`.
**Important note:** For the "fill" image to render properly, its parent element **must** be styled with `position: "relative"`, `position: "fixed"`, or `position: "absolute"`.
### Adjusting image styling
@ -145,7 +145,7 @@ Defining a [`srcset` attribute](https://developer.mozilla.org/en-US/docs/Web/API
#### Fixed-size images
If your image should be "fixed" in size (i.e. the same size across devices, except for [pixel density](https://web.dev/codelab-density-descriptors/)), there is no need to set a `sizes` attribute. A `srcset` can be generated automatically from the image's width and height attributes with no further input required.
If your image should be "fixed" in size (i.e. the same size across devices, except for [pixel density](https://web.dev/codelab-density-descriptors/)), there is no need to set a `sizes` attribute. A `srcset` can be generated automatically from the image's width and height attributes with no further input required.
Example srcset generated: `<img ... srcset="image-400w.jpg 1x, image-800w.jpg 2x">`
@ -332,7 +332,7 @@ Here's a simple step-by-step process for migrating from `background-image` to `N
You can adjust how the background image fills the container as described in the [Using fill mode](#using-fill-mode) section.
### Why can't I use `src` with `NgOptimizedImage`?
The `ngSrc` attribute was chosen as the trigger for NgOptimizedImage due to technical considerations around how images are loaded by the browser. NgOptimizedImage makes programmatic changes to the `loading` attribute--if the browser sees the `src` attribute before those changes are made, it will begin eagerly downloading the image file, and the loading changes will be ignored.
The `ngSrc` attribute was chosen as the trigger for NgOptimizedImage due to technical considerations around how images are loaded by the browser. NgOptimizedImage makes programmatic changes to the `loading` attribute--if the browser sees the `src` attribute before those changes are made, it will begin eagerly downloading the image file, and the loading changes will be ignored.
### Can I use two different image domains in the same page?
The [image loaders](#configuring-an-image-loader-for-ngoptimizedimage) provider pattern is designed to be as simple as possible for the common use case of having only a single image CDN used within a component. However, it's still very possible to manage multiple image CDNs using a single provider.

View file

@ -2,7 +2,7 @@
Prerendering, commonly referred to as Static Site Generation (SSG), represents the method by which pages are rendered to static HTML files during the build process.
Prerendering maintains the same performance benefits of [server-side rendering (SSR)](/guide/universal/#why-do-it). But achieves a reduced Time to First Byte (TTFB), ultimately enhancing user experience. The key distinction lies in its approach that pages are served as static content, and there is no request-based rendering.
Prerendering maintains the same performance benefits of [server-side rendering (SSR)](/guide/ssr/#why-use-ssr). But achieves a reduced Time to First Byte (TTFB), ultimately enhancing user experience. The key distinction lies in its approach that pages are served as static content, and there is no request-based rendering.
When the data necessary for server-side rendering remains consistent across all users, the strategy of prerendering emerges as a valuable alternative. Rather than dynamically rendering pages for each user request, prerendering takes a proactive approach by rendering them in advance.

224
aio/content/guide/ssr.md Normal file
View file

@ -0,0 +1,224 @@
# Server-side rendering (SSR) with Angular Universal
Server-Side Rendering (SSR) is a process that involves rendering pages on the server, resulting in static HTML content that mirrors the application's state for each request. Once this server-generated HTML content is produced, Angular initializes the application and utilizes the data contained within the HTML.
The primary advantage of SSR is the enhanced speed at which applications typically render in a browser. This allows users to view the application's user interface before it becomes fully interactive. For more details, refer to the ["Why use Server-Side Rendering?"](#why-use-ssr) section below.
If you're interested in exploring additional techniques and concepts related to SSR, you can refer to this [article](https://developers.google.com/web/updates/2019/02/rendering-on-the-web).
To enable SSR in your Angular application, follow the steps outlined below.
<a id="the-example"></a>
## Tutorial
The [Tour of Heroes tutorial](tutorial/tour-of-heroes) is the foundation for this walkthrough.
In this example, the Angular application is server rendered using based on client requests.
<div class="alert is-helpful">
<live-example downloadOnly>Download the finished sample code</live-example>, which runs in a [Node.js® Express](https://expressjs.com) server.
</div>
<a id="ssr-cli-command"></a>
### Step 1. Enable Server-Side Rendering
To add SSR to an existing project, use the Angular CLI `ng add` command.
<code-example format="shell" language="shell">
ng add &commat;angular/ssr
</code-example>
<div class="alert is-helpful">
To create an application with server-side rendering capabilities from the beginning use the [ng new --ssr](cli/new) command.
</div>
The command updates the application code to enable SSR and adds extra files to the project structure.
<code-example language="text">
my-app
|-- server.ts # application server
└── src
|-- app
| └── app.config.server.ts # server application configuration
└── main.server.ts # main server application bootstrapping
</code-example>
### Step 2. Run your application in a browser
Start the development server.
<code-example language="shell">
ng serve
</code-example>
After starting the dev-server, open your web browser and visit `http://localhost:4200`.
You should see the familiar Tour of Heroes dashboard page.
Navigation using `routerLinks` works correctly because they use the built-in anchor \(`<a>`\) elements.
You can seamlessly move from the Dashboard to the Heroes page and back.
Additionally, clicking on a hero within the Dashboard page will display its Details page.
If you throttle your network speed so that the client-side scripts take longer to download \(instructions following\), you'll notice:
- You can't add or delete a hero
- The search box on the Dashboard page is ignored
- The _Back_ and _Save_ buttons on the Details page don't work
The transition from the server-rendered application to the client application happens quickly on a development machine, but you should always test your applications in real-world scenarios.
You can simulate a slower network to see the transition more clearly as follows:
1. Open the Chrome Dev Tools and go to the Network tab.
1. Find the [Network Throttling](https://developers.google.com/web/tools/chrome-devtools/network-performance/reference#throttling) dropdown on the far right of the menu bar.
1. Try one of the "3G" speeds.
The server-rendered application still launches quickly but the full client application might take seconds to load.
## Why use SSR?
Compared to a client side rendered (CSR) only application the main advantages of SSR are;
### Improve search engine optimization (SEO)
Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web.
These web crawlers might be unable to navigate and index your highly interactive Angular application as a human user could do.
You can generate a static version of your application that is easily searchable, linkable, and navigable without JavaScript and
make a site preview available because each URL returns a fully rendered page.
[Learn more about search engine optimization (SEO)](https://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf).
### Show the page quicker
Displaying the page quickly can be critical for user engagement.
Pages that load faster perform better, [even with changes as small as 100ms](https://web.dev/shopping-for-speed-on-ebay).
Your application might have to launch faster to engage these users before they decide to do something else.
With server-side rendering, the application doesn't need to wait until all JavaScript has been downloaded and executed to be displayed. In additional HTTP requests done using [`HttpClient`](api/common/http/HttpClient) are done once on the server, as there are cached. See the ["Caching data when using HttpClient"](#caching-data-when-using-httpclient) section below for additional information.
This results in a performance improvement that can be measured using [Core Web Vitals (CWV)](https://web.dev/learn-core-web-vitals/) statistics, such as reducing the First-contentful paint ([FCP](https://developer.chrome.com/en/docs/lighthouse/performance/first-contentful-paint/)) and Largest Contentful Paint ([LCP](https://web.dev/lcp/)), as well as Cumulative Layout Shift ([CLS](https://web.dev/cls/)).
### Caching data when using HttpClient
When [hydration](guide/hydration) is enabled, [`HttpClient`](api/common/http/HttpClient) responses are cached while running on the server and transferring this cache to the client to avoid extra HTTP requests. After that this information is serialized and transferred to a browser as a part of the initial HTML sent from the server after server-side rendering. In a browser, [`HttpClient`](api/common/http/HttpClient) checks whether it has data in the cache and if so, reuses it instead of making a new HTTP request during initial application rendering. HttpClient stops using the cache once an application becomes [stable](api/core/ApplicationRef#isStable) while running in a browser.
Caching is performed by default for every `HEAD` and `GET` requests. You can include `POST` or filter caching for requests by using the [`withHttpTransferCacheOptions`](/api/platform-browser/withHttpTransferCacheOptions). You can also enable or disable caching by the `transferCache` option in the [HttpClient](/api/common/http/HttpClient) [`post`](/api/common/http/HttpClient#post), [`get`](/api/common/http/HttpClient#get) and [`head`](/api/common/http/HttpClient#head) methods.
### Rendering engine
The `server.ts` file configures the SSR rendering engine with Node.js Express server.
The `CommonEngine` is used to construct the rendering engine.
<code-example path="ssr/server.ts" region="CommonEngine"></code-example>
The contructor accepts an object with the following properties:
| Properties | Details | Default Value |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------- |
| _`bootstrap`_ | A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an `NgModule`. | |
| _`providers`_ | A set of platform level providers for the all request. | |
| _`enablePeformanceProfiler`_ | Enable request performance profiling data collection and printing the results in the server console. | `false` |
The `commonEngine.render()` function which turns a client's requests for Angular pages into server-rendered HTML pages.
<code-example path="ssr/server.ts" region="navigation-request"></code-example>
The function accepts an object with the following properties:
| Properties | Details | Default Value |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------- |
| _`bootstrap`_ | A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an `NgModule`. | |
| _`providers`_ | A set of platform level providers for the current request. | |
| `url` | The url of the page to render. | |
| _`inlineCriticalCss`_ | Reduce render blocking requests by inlining critical CSS. | `true` |
| _`publicPath`_ | Base path for browser files and assets. | |
| _`document`_ | The initial DOM to use to bootstrap the server application. | |
| _`documentFilePath`_ | File path of the initial DOM to use to bootstrap the server application. | |
### Working around the browser APIs
Some of the browser APIs and capabilities might be missing on the server.
Applications cannot make use of browser-specific global objects like `window`, `document`, `navigator`, or `location`.
Angular provides some injectable abstractions over these objects, such as [`Location`](api/common/Location) or [`DOCUMENT`](api/common/DOCUMENT); it might substitute adequately for these APIs.
If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server \(also known as shimming\).
Server-side applications lack access to mouse or keyboard events, which means they can't depend on user interactions such as clicking a button to display a component.
In such cases, the application needs to determine what to render solely based on the client's incoming request.
This limitation underscores the importance of making the application [routable](guide/router), using a routing mechanism to navigate and display content as needed.
### Using Angular Service Worker
If you are using Angular on the server in combination with the Angular service worker, the behavior is deviates than the normal server-side rendering behavior. The initial server request will be rendered on the server as expected. However, after that initial request, subsequent requests are handled by the service worker. For subsequent requests, the `index.html` file is served statically and bypasses server-side rendering.
### Filtering request URLs
By default, if the application was only rendered by the server, _every_ application link clicked would arrive at the server as a navigation URL intended for the router.
However, most server implementations have to handle requests for at least three very different kinds of resources: _data_, _application pages_, and _static files_.
Fortunately, the URLs for these different requests are easily recognized.
| Routing request types | Details |
| :-------------------- | :------------------------------ |
| Data request | Request URL that begins `/api` |
| Static asset | Request URL with file extension |
| App navigation | All other requests |
The `server.ts` generated by the CLI already makes these basic distinctions.
You may have to modify it to satisfy your specific application needs.
#### Serving Data
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.
For data requests, you could configure the Node.js Express server pipeline with calls to `server.get()` as follows:
<code-example header="server.ts (data API)" path="ssr/server.ts" region="data-request"></code-example>
HELPFUL: This guide's `server.ts` _doesn't handle data requests_. It returns a `404 - Not Found` for all data API requests.
For demonstration purposes, this tutorial intercepts all HTTP data calls from the client _before they go to the server_ and simulates the behavior of a remote data server, using Angular's "in-memory web API" demo package.
In practice, you would remove the following "in-memory web API" code from `app.config.ts`.
<code-example header="app.config.ts (in-memory web API)" path="ssr/src/app/app.config.ts" region="in-mem"></code-example>
Then register your data API middleware in `server.ts`.
#### Serving Static Files Safely
All static asset requests such as for JavaScript, image, and style files have a file extension (examples: `main.js`, `assets/favicon.ico`, `src/app/styles.css`).
They won't be confused with navigation or data requests if you filter for files with an extension.
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in the `dist/my-app/browser` directory.
The following Node.js Express code routes all requests for files with an extension (`*.*`) to `/dist`, and returns a `404 - NOT FOUND` error if the
file isn't found.
<code-example header="server.ts (static files)" path="ssr/server.ts" region="static"></code-example>
<!-- links -->
<!-- external links -->
<!-- end links -->
@reviewed 2023-10-26

View file

@ -1,377 +0,0 @@
# Server-side rendering (SSR) with Angular Universal (NgModule Version)
This guide describes **Angular Universal**, a technology that allows Angular to render applications on the server.
<div class="alert is-helpful">
This version of the guide describes building Angular Universal applications with [NgModules](guide/ngmodules).
If yours is a Standalone application, see [Angular Universal with Standalone Applications](guide/universal).
</div>
By default, Angular renders applications only in a *browser*. Angular Universal allows Angular to render an application on the *server*, generating *static* HTML content, which represents an application state. Once the HTML content is rendered in a browser, Angular bootstraps an application and reuses the information available in the server-generated HTML.
With server-side rendering an application generally renders in a browser faster, giving users a chance to view the application UI before it becomes fully interactive. See the ["Why use Server-Side Rendering?"](#why-do-it) section below for additional information.
Also for a more detailed look at different techniques and concepts surrounding SSR, check out this [article](https://developers.google.com/web/updates/2019/02/rendering-on-the-web).
You can enable server-side rendering in your Angular application using the `@nguniversal/express-engine` package as described below.
<div class="alert is-helpful">
Angular Universal requires an [active LTS or maintenance LTS](https://nodejs.org/about/releases) version of Node.js.
For information see the [version compatibility](guide/versions) guide to learn about the currently supported versions.
</div>
<a id="the-example"></a>
## Universal tutorial
The [Tour of Heroes tutorial](tutorial/tour-of-heroes) is the foundation for this walkthrough.
In this example, the Angular CLI compiles and bundles the Universal version of the application with the [Ahead-of-Time (AOT) compiler](guide/aot-compiler).
A Node.js Express web server compiles HTML pages with Universal based on client requests.
<div class="alert is-helpful">
<live-example downloadOnly>Download the finished sample code</live-example>, which runs in a [Node.js® Express](https://expressjs.com) server.
</div>
### Step 1. Enable Server-Side Rendering
Run the following command to add SSR support into your application:
<code-example format="shell" language="shell">
ng add &commat;nguniversal/express-engine
</code-example>
The command updates the application code to enable SSR and adds extra files to the project structure (files that are marked with the `*` symbol).
<div class='filetree'>
<div class='file'>
src
</div>
<div class='children'>
<div class='file'>
index.html &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- app web page
</div>
<div class='file'>
main.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- bootstrapper for client app
</div>
<div class='file'>
main.server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; bootstrapper for server app
</div>
<div class='file'>
style.css &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- styles for the app
</div>
<div class='file'>
app/ &nbsp;&hellip; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- application code
</div>
<div class='children'>
<div class='file'>
app.config.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- client-side application configuration (standalone app only)
</div>
<div class='file'>
app.module.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- client-side application module (NgModule app only)
</div>
</div>
<div class='children'>
<div class='file'>
app.config.server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; server-side application configuration (standalone app only)
</div>
<div class='file'>
app.module.server.ts &nbsp;&nbsp;&nbsp; // &lt;-- &ast; server-side application module (NgModule app only)
</div>
</div>
<div class='file'>
server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; express web server
</div>
<div class='file'>
tsconfig.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript base configuration
</div>
<div class='file'>
tsconfig.app.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript browser application configuration
</div>
<div class='file'>
tsconfig.server.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript server application configuration
</div>
<div class='file'>
tsconfig.spec.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript tests configuration
</div>
</div>
</div>
### Step 2. Enable Client Hydration
<div class="alert is-important">
The hydration feature is available for [developer preview](/guide/releases#developer-preview). It's ready for you to try, but it might change before it is stable.
</div>
Hydration is the process that restores the server side rendered application on the client. This includes things like reusing the server rendered DOM structures, persisting the application state, transferring application data that was retrieved already by the server, and other processes. Learn more about hydration in [this guide](guide/hydration).
You can enable hydration by updating the `app.module.ts` file. Import the `provideClientHydration` function from `@angular/platform-browser` and add the function call to the `providers` section of the `AppModule` as shown below.
```typescript
import {provideClientHydration} from '@angular/platform-browser';
// ...
@NgModule({
// ...
providers: [ provideClientHydration() ], // add this line
bootstrap: [ AppComponent ]
})
export class AppModule {
// ...
}
```
### Step 3. Start the server
To start rendering your application with Universal on your local system, use the following command.
<code-example format="shell" language="shell">
npm run dev:ssr
</code-example>
### Step 4. Run your application in a browser
Once the web server starts, open a browser and navigate to `http://localhost:4200`.
You should see the familiar Tour of Heroes dashboard page.
Navigation using `routerLinks` works correctly because they use the built-in anchor \(`<a>`\) elements.
You can go from the Dashboard to the Heroes page and back.
Click a hero on the Dashboard page to display its Details page.
If you throttle your network speed so that the client-side scripts take longer to download \(instructions following\), you'll notice:
* You can't add or delete a hero
* The search box on the Dashboard page is ignored
* The *Back* and *Save* buttons on the Details page don't work
The transition from the server-rendered application to the client application happens quickly on a development machine, but you should always test your applications in real-world scenarios.
You can simulate a slower network to see the transition more clearly as follows:
1. Open the Chrome Dev Tools and go to the Network tab.
1. Find the [Network Throttling](https://developers.google.com/web/tools/chrome-devtools/network-performance/reference#throttling) dropdown on the far right of the menu bar.
1. Try one of the "3G" speeds.
The server-rendered application still launches quickly but the full client application might take seconds to load.
<a id="why-do-it"></a>
## Why use Server-Side Rendering?
There are three main reasons to create a Universal version of your application.
* Facilitate web crawlers through [search engine optimization (SEO)](https://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf)
* Improve performance on mobile and low-powered devices
* Show the first page quickly with a [first-contentful paint (FCP)](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint)
<a id="seo"></a>
<a id="web-crawlers"></a>
### Facilitate web crawlers (SEO)
Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web.
These web crawlers might be unable to navigate and index your highly interactive Angular application as a human user could do.
Angular Universal can generate a static version of your application that is easily searchable, linkable, and navigable without JavaScript.
Universal also makes a site preview available because each URL returns a fully rendered page.
<a id="no-javascript"></a>
### Improve performance on mobile and low-powered devices
Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable.
For these cases, you might require a server-rendered, no-JavaScript version of the application.
This version, however limited, might be the only practical alternative for people who otherwise couldn't use the application at all.
<a id="startup-performance"></a>
### Show the first page quickly
Displaying the first page quickly can be critical for user engagement.
Pages that load faster perform better, [even with changes as small as 100ms](https://web.dev/shopping-for-speed-on-ebay).
Your application might have to launch faster to engage these users before they decide to do something else.
With Angular Universal, you can generate landing pages for the application that look like the complete application.
The pages are pure HTML, and can display even if JavaScript is disabled.
The pages don't handle browser events, but they *do* support navigation through the site using [`routerLink`](guide/router-reference#router-link).
In practice, you'll serve a static version of the landing page to hold the user's attention.
At the same time, you'll load the full Angular application behind it.
The user perceives near-instant performance from the landing page and gets the full interactive experience after the full application loads.
<a id="how-does-it-work"></a>
## Universal web servers
A Universal web server responds to application page requests with static HTML rendered by the [Universal template engine](#universal-engine).
The server receives and responds to HTTP requests from clients \(usually browsers\), and serves static assets such as scripts, CSS, and images.
It might respond to data requests, either directly or as a proxy to a separate data server.
The sample web server for this guide is based on the popular [Express](https://expressjs.com) framework.
<div class="alert is-helpful">
**NOTE**: <br />
*Any* web server technology can serve a Universal application as long as it can call Angular `platform-server` package [`renderModule`](api/platform-server/renderModule) or [`renderApplication`](api/platform-server/renderApplication) functions.
The principles and decision points discussed here apply to any web server technology.
</div>
Universal applications use the Angular `platform-server` package \(as opposed to `platform-browser`\), which provides
server implementations of the DOM, `XMLHttpRequest`, and other low-level features that don't rely on a browser.
The server \([Node.js Express](https://expressjs.com) in this guide's example\) passes client requests for application pages to the NgUniversal `ngExpressEngine`.
Under the hood, the engine renders the app, while also providing caching and other helpful utilities.
The render function takes as inputs a *template* HTML page \(usually `index.html`\), and an Angular *module* containing components. Alternatively, it can take a function that when invoked returns a `Promise` that resolves to an `ApplicationRef`, and a *route* that determines which components to display. The route comes from the client's request to the server.
Each request results in the appropriate view for the requested route.
The render function renders the view within the `<app>` tag of the template, creating a finished HTML page for the client.
Finally, the server returns the rendered page to the client.
### Working around the browser APIs
Because a Universal application doesn't execute in the browser, some of the browser APIs and capabilities might be missing on the server.
For example, server-side applications can't reference browser-only global objects such as `window`, `document`, `navigator`, or `location`.
Angular provides some injectable abstractions over these objects, such as [`Location`](api/common/Location) or [`DOCUMENT`](api/common/DOCUMENT); it might substitute adequately for these APIs.
If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server \(also known as shimming\).
Similarly, without mouse or keyboard events, a server-side application can't rely on a user clicking a button to show a component.
The application must determine what to render based solely on the incoming client request.
This is a good argument for making the application [routable](guide/router).
<a id="service-worker"></a>
### Universal and the Angular Service Worker
If you are using Universal in conjunction with the Angular service worker, the behavior is different than the normal server side rendering behavior. The initial server request will be rendered on the server as expected. However, after that initial request, subsequent requests are handled by the service worker. For subsequent requests, the `index.html` file is served statically and bypasses server side rendering.
<a id="universal-engine"></a>
### Universal template engine
The important bit in the `server.ts` file is the `ngExpressEngine()` function.
<code-example header="server.ts" path="universal-ngmodule/server.ts" region="ngExpressEngine"></code-example>
The `ngExpressEngine()` function is a wrapper around the Angular `platform-server` package [`renderModule`](api/platform-server/renderModule) and [`renderApplication`](api/platform-server/renderApplication) functions which turns a client's requests into server-rendered HTML pages.
It accepts an object with the following properties:
| Properties | Details |
|:--- |:--- |
| `bootstrap` | The root `NgModule` or function that when invoked returns a `Promise` that resolves to an `ApplicationRef` of the application when rendering on the server. For the example application, it is `AppServerModule`. It's the bridge between the Universal server-side renderer and the Angular application. |
| `extraProviders` | This property is optional and lets you specify dependency providers that apply only when rendering the application on the server. Do this when your application needs information that can only be determined by the currently running server instance. |
The `ngExpressEngine()` function returns a `Promise` callback that resolves to the rendered page.
It's up to the engine to decide what to do with that page.
This engine's `Promise` callback returns the rendered page to the web server, which then forwards it to the client in the HTTP response.
### Filtering request URLs
<div class="alert is-helpful">
**NOTE**: <br />
The basic behavior described below is handled automatically when using the NgUniversal Express package.
This is helpful when trying to understand the underlying behavior or replicate it without using the package.
</div>
The web server must distinguish *app page requests* from other kinds of requests.
It's not as simple as intercepting a request to the root address `/`.
The browser could ask for one of the application routes such as `/dashboard`, `/heroes`, or `/detail:12`.
In fact, if the application were only rendered by the server, *every* application link clicked would arrive at the server as a navigation URL intended for the router.
Fortunately, application routes have something in common: their URLs lack file extensions.
\(Data requests also lack extensions but they can be recognized because they always begin with `/api`.\)
All static asset requests have a file extension \(such as `main.js` or `/node_modules/zone.js/bundles/zone.umd.js`\).
Because you use routing, you can recognize the three types of requests and handle them differently.
| Routing request types | Details |
|:--- |:--- |
| Data request | Request URL that begins `/api` |
| App navigation | Request URL with no file extension |
| Static asset | All other requests |
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.
You configure the Node.js Express server pipeline with calls to `server.get()` like this one for data requests:
<code-example header="server.ts (data URL)" path="universal-ngmodule/server.ts" region="data-request"></code-example>
<div class="alert is-helpful">
**NOTE**: <br />
This sample server doesn't handle data requests.
The tutorial's "in-memory web API" module, a demo and development tool, intercepts all HTTP calls and simulates the behavior of a remote data server.
In practice, you would remove that module and register your web API middleware on the server here.
</div>
The following code filters for request URLs with no extensions and treats them as navigation requests:
<code-example header="server.ts (navigation)" path="universal-ngmodule/server.ts" region="navigation-request"></code-example>
### Serving static files safely
A single `server.use()` treats all other URLs as requests for static assets such as JavaScript, image, and style files.
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in the `/dist` folder and only honor requests for files from the `/dist` folder.
The following Node.js Express code routes all remaining requests to `/dist`, and returns a `404 - NOT FOUND` error if the
file isn't found.
<code-example header="server.ts (static files)" path="universal-ngmodule/server.ts" region="static"></code-example>
### Using absolute URLs for HTTP (data) requests on the server
The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `HttpClient` module to fetch application data.
These services send requests to *relative* URLs such as `api/heroes`.
In a server-side rendered app, HTTP URLs must be *absolute* \(for example, `https://my-server.com/api/heroes`\).
This means that the URLs must be somehow converted to absolute when running on the server and be left relative when running in the browser.
If you are using one of the `@nguniversal/*-engine` packages \(such as `@nguniversal/express-engine`\), this is taken care for you automatically.
You don't need to do anything to make relative URLs work on the server.
If, for some reason, you are not using an `@nguniversal/*-engine` package, you might need to handle it yourself.
The recommended solution is to pass the full request URL to the `options` argument of [renderModule](api/platform-server/renderModule).
This option is the least intrusive as it does not require any changes to the application.
Here, "request URL" refers to the URL of the request as a response to which the application is being rendered on the server.
For example, if the client requested `https://my-server.com/dashboard` and you are rendering the application on the server to respond to that request, `options.url` should be set to `https://my-server.com/dashboard`.
Now, on every HTTP request made as part of rendering the application on the server, Angular can correctly resolve the request URL to an absolute URL, using the provided `options.url`.
### Useful scripts
| Scripts | Details |
|:--- |:--- |
| <code-example format="shell" language="shell"> npm run dev:ssr </code-example> | Similar to [`ng serve`](cli/serve), which offers live reload during development, but uses server-side rendering. The application runs in watch mode and refreshes the browser after every change. This command is slower than the actual `ng serve` command. |
| <code-example format="shell" language="shell"> ng build &amp;&amp; ng run app-name:server </code-example> | Builds both the server script and the application in production mode. Use this command when you want to build the project for deployment. |
| <code-example format="shell" language="shell"> npm run serve:ssr </code-example> | Starts the server script for serving the application locally with server-side rendering. It uses the build artifacts created by `npm run build:ssr`, so make sure you have run that command as well. <div class="alert is-helpful"> **NOTE**: <br /> `serve:ssr` is not intended to be used to serve your application in production, but only for testing the server-side rendered application locally. </div> |
| <code-example format="shell" language="shell"> npm run prerender </code-example> | Used to prerender an application's pages. Read more about prerendering [here](guide/prerendering). |
<!-- links -->
<!-- external links -->
<!-- end links -->
@reviewed 2023-08-29

View file

@ -1,400 +0,0 @@
# Server-side rendering (SSR) with Angular Universal
This guide describes **Angular Universal**, a technology that allows Angular to render applications on the server.
By default, Angular renders applications only in a *browser*. Angular Universal allows Angular to render an application on the *server*, generating *static* HTML content, which represents an application state. Once the HTML content is rendered in a browser, Angular bootstraps an application and reuses the information available in the server-generated HTML.
With server-side rendering an application generally renders in a browser faster, giving users a chance to view the application UI before it becomes fully interactive. See the ["Why use Server-Side Rendering?"](#why-do-it) section below for additional information.
Also for a more detailed look at different techniques and concepts surrounding SSR, check out this [article](https://developers.google.com/web/updates/2019/02/rendering-on-the-web).
You can enable server-side rendering in your Angular application using the `@nguniversal/express-engine` package as described below.
<div class="alert is-helpful">
Angular Universal requires an [active LTS or maintenance LTS](https://nodejs.org/about/releases) version of Node.js.
For information see the [version compatibility](guide/versions) guide to learn about the currently supported versions.
</div>
<a id="the-example"></a>
## Universal tutorial
The [Tour of Heroes tutorial](tutorial/tour-of-heroes) is the foundation for this walkthrough.
In this example, the Angular CLI compiles and bundles the Universal version of the application with the [Ahead-of-Time (AOT) compiler](guide/aot-compiler).
A Node.js Express web server compiles HTML pages with Universal based on client requests.
<div class="alert is-helpful">
<live-example downloadOnly>Download the finished sample code</live-example>, which runs in a [Node.js® Express](https://expressjs.com) server.
</div>
<div class="alert is-helpful">
This is the guide for Angular Universal with [Standalone Applications](guide/standalone-components).
If your application is built with [NgModules](guide/ngmodules), see [Angular Universal applications with NgModules](guide/universal-ngmodule).
</div>
<a id="universal-cli-command"></a>
### Step 1. Enable Server-Side Rendering
Run the following Angular CLI command to add SSR support into your application:
<code-example format="shell" language="shell">
ng add &commat;nguniversal/express-engine
</code-example>
The command updates the application code to enable SSR and adds extra files to the project structure (files that are marked with the `*` symbol).
</div>
<div class='filetree'>
<div class='file'>
src
</div>
<div class='children'>
<div class='file'>
index.html &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- app web page
</div>
<div class='file'>
main.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- bootstrapper for client app
</div>
<div class='file'>
main.server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; bootstrapper for server app
</div>
<div class='file'>
style.css &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- styles for the app
</div>
<div class='file'>
app/ &nbsp;&hellip; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- application code
</div>
<div class='children'>
<div class='file'>
app.config.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- client-side application configuration
</div>
</div>
<div class='children'>
<div class='file'>
app.config.server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; server-side application configuration
</div>
</div>
<div class='children'>
<div class='file'>
app.routes.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- client-side application routes
</div>
</div>
<div class='file'>
server.ts &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- &ast; express web server
</div>
<div class='file'>
tsconfig.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript base configuration
</div>
<div class='file'>
tsconfig.app.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript browser application configuration
</div>
<div class='file'>
tsconfig.server.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript server application configuration
</div>
<div class='file'>
tsconfig.spec.json &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // &lt;-- TypeScript tests configuration
</div>
</div>
</div>
<div class="alert is-important">
This CLI command succeeds _only if your standalone application is bootstrapped in the recommended way_ with `app.config.ts` and `app.routes.ts` files.
If your Angular application doesn't follow this practice, you can quickly and easily refactor to that practice, as [explained below](#boot-with-app-config), before you run the command.
</div>
### Step 2. Enable Client Hydration
Hydration is the process that restores the server side rendered application on the client. This includes things like reusing the server rendered DOM structures, persisting the application state, transferring application data that was retrieved already by the server, and other processes. Learn more about hydration in [this guide](guide/hydration).
You can enable hydration by updating the `app.config.ts` file.
Import the `provideClientHydration` function from `@angular/platform-browser` and add the function call to the `providers` section as shown below.
<code-example path="universal/src/app/app.config.ts" region="client-hydration"></code-example>
### Step 3. Start the server
To start rendering your application with Universal on your local system, use the following command.
<code-example format="shell" language="shell">
npm run dev:ssr
</code-example>
### Step 4. Run your application in a browser
Once the web server starts, open a browser and navigate to `http://localhost:4200`.
You should see the familiar Tour of Heroes dashboard page.
Navigation using `routerLinks` works correctly because they use the built-in anchor \(`<a>`\) elements.
You can go from the Dashboard to the Heroes page and back.
Click a hero on the Dashboard page to display its Details page.
If you throttle your network speed so that the client-side scripts take longer to download \(instructions following\), you'll notice:
* You can't add or delete a hero
* The search box on the Dashboard page is ignored
* The *Back* and *Save* buttons on the Details page don't work
The transition from the server-rendered application to the client application happens quickly on a development machine, but you should always test your applications in real-world scenarios.
You can simulate a slower network to see the transition more clearly as follows:
1. Open the Chrome Dev Tools and go to the Network tab.
1. Find the [Network Throttling](https://developers.google.com/web/tools/chrome-devtools/network-performance/reference#throttling) dropdown on the far right of the menu bar.
1. Try one of the "3G" speeds.
The server-rendered application still launches quickly but the full client application might take seconds to load.
<a id="why-do-it"></a>
## Why use Server-Side Rendering?
There are three main reasons to create a Universal version of your application.
* Facilitate web crawlers through [search engine optimization (SEO)](https://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf)
* Improve performance on mobile and low-powered devices
* Show the first page quickly with a [first-contentful paint (FCP)](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint)
<a id="seo"></a>
<a id="web-crawlers"></a>
### Facilitate web crawlers (SEO)
Google, Bing, Facebook, Twitter, and other social media sites rely on web crawlers to index your application content and make that content searchable on the web.
These web crawlers might be unable to navigate and index your highly interactive Angular application as a human user could do.
Angular Universal can generate a static version of your application that is easily searchable, linkable, and navigable without JavaScript.
Universal also makes a site preview available because each URL returns a fully rendered page.
<a id="no-javascript"></a>
### Improve performance on mobile and low-powered devices
Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable.
For these cases, you might require a server-rendered, no-JavaScript version of the application.
This version, however limited, might be the only practical alternative for people who otherwise couldn't use the application at all.
<a id="startup-performance"></a>
### Show the first page quickly
Displaying the first page quickly can be critical for user engagement.
Pages that load faster perform better, [even with changes as small as 100ms](https://web.dev/shopping-for-speed-on-ebay).
Your application might have to launch faster to engage these users before they decide to do something else.
With Angular Universal, you can generate landing pages for the application that look like the complete application.
The pages are pure HTML, and can display even if JavaScript is disabled.
The pages don't handle browser events, but they *do* support navigation through the site using [`routerLink`](guide/router-reference#router-link).
In practice, you'll serve a static version of the landing page to hold the user's attention.
At the same time, you'll load the full Angular application behind it.
The user perceives near-instant performance from the landing page and gets the full interactive experience after the full application loads.
<a id="how-does-it-work"></a>
## Universal web servers
A Universal web server responds to application page requests with static HTML rendered by the [Universal template engine](#universal-engine).
The server receives and responds to requests from clients \(usually browsers\), and serves static assets such as scripts, CSS, and images.
It might respond to data requests, either directly or as a proxy to a separate data server.
Universal applications use the Angular `platform-server` package \(as opposed to `platform-browser`\), which provides
server implementations of the DOM, `XMLHttpRequest`, and other low-level features that don't rely on a browser.
The web server for this guide is [Node.js Express](https://expressjs.com), configured to pass client requests for application pages to Angular's `ngExpressEngine`. This engine renders the app while also providing caching and other helpful utilities.
The engine's render function takes as inputs a *template* page \(usually `index.html`\), a function returning a `Promise` that resolves to an `ApplicationRef`, and a *route* that determines which components to display. The route comes from the client's request to the server.
Each request results in the appropriate view for the requested route.
The render function renders the view within the `<app>` tag of the template, creating a finished HTML page for the client.
Finally, the server returns the rendered page to the client.
### Caching data when using HttpClient
By default, the [`provideClientHydration()`](api/platform-browser/provideClientHydration) function enables the recommended set of features for the optimal performance for most of the applications. It includes the following features:
* Reconciling DOM hydration (learn more about it [here](guide/hydration)).
* [`HttpClient`](api/common/http/HttpClient) response caching while running on the server and transferring this cache to the client to avoid extra HTTP requests.
While running on the server, data caching is performed for every `HEAD` and `GET` requests done by the [`HttpClient`](api/common/http/HttpClient). After that this information is serialized and transferred to a browser as a part of the initial HTML sent from the server after server-side rendering. In a browser, [`HttpClient`](api/common/http/HttpClient) checks whether it has data in the cache and if so, reuses it instead of making a new HTTP request during initial application rendering. HttpClient stops using the cache once an application becomes [stable](api/core/ApplicationRef#isStable) while running in a browser.
### Working around the browser APIs
Because a Universal application doesn't execute in the browser, some of the browser APIs and capabilities might be missing on the server.
For example, server-side applications can't reference browser-only global objects such as `window`, `document`, `navigator`, or `location`.
Angular provides some injectable abstractions over these objects, such as [`Location`](api/common/Location) or [`DOCUMENT`](api/common/DOCUMENT); it might substitute adequately for these APIs.
If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server \(also known as shimming\).
Similarly, without mouse or keyboard events, a server-side application can't rely on a user clicking a button to show a component.
The application must determine what to render based solely on the incoming client request.
This is a good argument for making the application [routable](guide/router).
<a id="service-worker"></a>
### Universal and the Angular Service Worker
If you are using Universal in conjunction with the Angular service worker, the behavior is different than the normal server side rendering behavior. The initial server request will be rendered on the server as expected. However, after that initial request, subsequent requests are handled by the service worker. For subsequent requests, the `index.html` file is served statically and bypasses server side rendering.
<a id="universal-engine"></a>
### Universal template engine
The `server.ts` file configures the Universal template engine. The core of a typical implementation looks like this:
<code-example header="server.ts (excerpt)" path="universal/server.ts" region="core"></code-example>
Focus on the `ngExpressEngine()` function which turns a client's requests for Angular pages into server-rendered HTML pages.
<code-example path="universal/server.ts" region="ngExpressEngine"></code-example>
The function accepts an object with the following properties:
| Properties | Details |
|:--- |:--- |
| `bootstrap` | The bootstrapping function that returns a `Promise` resolving to an `ApplicationRef` for the application to render on the server. It's the bridge between the Universal server-side renderer and the Angular application. |
| _`extraProviders`_ | This _optional_ property lets you specify dependency providers that apply only when rendering the application on the server. Do this when your application needs information that can only be determined by the currently running server instance. |
The `bootstrap` function comes from `main.server.ts`, generated by the CLI command that created your universal app.
<code-example header="main.server.ts (excerpt)" path="universal/src/main.server.ts" region="bootstrap"></code-example>
The `ngExpressEngine()` function returns a `Promise` callback that resolves to the rendered page. The web server resolves the promise and forwards the page to the client.
### Filtering request URLs
By default, if the application were only rendered by the server, *every* application link clicked would arrive at the server as a navigation URL intended for the router.
However, most server implementations have to handle requests for at least three very different kinds of resources: _data_, _application pages_, and _static files_.
Fortunately, the URLs for these different requests are easily recognized.
| Routing request types | Details |
|:--- |:--- |
| Data request | Request URL that begins `/api` |
| App navigation | Request URL with no file extension |
| Static asset | Request URL with file extension |
The `server.ts` generated by the CLI already makes these basic distinctions.
You may have to modify it to satisfy your specific application needs.
#### Serving Data
A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other.
For data requests, you could configure the Node.js Express server pipeline with calls to `server.get()` as follows:
<code-example header="server.ts (data API)" path="universal/server.ts" region="data-request"></code-example>
<div class="alert is-helpful">
This guide's `server.ts` _doesn't handle data requests_. It returns a `404 - Not Found` for all data API requests.
For demonstration purposes, this tutorial intercepts all HTTP data calls from the client _before they go to the server_ and simulates the behavior of a remote data server, using Angular's "in-memory web API" demo package.
In practice, you would remove the following "in-memory web API" code from `app.config.ts`.
<code-example header="app.config.ts (in-memory web API)" path="universal/src/app/app.config.ts" region="in-mem"></code-example>
Then register your data API middleware in `server.ts`.
</div>
#### App Navigation
Application routes look like this: `/dashboard`, `/heroes`, `/detail:12`.
While they have a lot of variety, what they all have in common is `no file extension`.
The following code filters for request URLs with no extensions and treats them as navigation requests:
<code-example header="server.ts (navigation)" path="universal/server.ts" region="navigation-request"></code-example>
#### Serving Static Files Safely
All static asset requests such as for JavaScript, image, and style files have a file extension (examples: `main.js`, `assets/favicon.ico`, `src/app/styles.css`).
They won't be confused with navigation or data requests if you filter for files with an extension.
To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in the `/dist` folder and only honor requests for files from the `/dist` folder.
The following Node.js Express code routes all requests for files with an extension (`*.*`) to `/dist`, and returns a `404 - NOT FOUND` error if the
file isn't found.
<code-example header="server.ts (static files)" path="universal/server.ts" region="static"></code-example>
### Useful scripts
| Scripts | Details |
|:--- |:--- |
| <code-example format="shell" language="shell"> npm run dev:ssr </code-example> | Similar to [`ng serve`](cli/serve), which offers live reload during development, but uses server-side rendering. The application runs in watch mode and refreshes the browser after every change. This command is slower than the actual `ng serve` command. |
| <code-example format="shell" language="shell"> ng build &amp;&amp; ng run app-name:server </code-example> | Builds both the server script and the application in production mode. Use this command when you want to build the project for deployment. |
| <code-example format="shell" language="shell"> npm run serve:ssr </code-example> | Starts the server script for serving the application locally with server-side rendering. It uses the build artifacts created by `npm run build:ssr`, so make sure you have run that command as well. <div class="alert is-helpful">`serve:ssr` is not intended to be used to serve your application in production, but only for testing the server-side rendered application locally. </div> |
| <code-example format="shell" language="shell"> npm run prerender </code-example> | Used to prerender an application's pages. Read more about prerendering [here](guide/prerendering). |
<a id="boot-with-app-config"></a>
## Bootstrapping the client with `app.config.ts`
The [CLI command](#universal-cli-command) to generate a Universal Application assumes that your standalone application's`main.ts` bootstraps with the `appConfig` object exported from your `app.config.ts` like this.
<code-example header="main.ts (extract)" path="universal/src/main.ts" region="bootstrap"></code-example>
If your existing app doesn't follow this practice, you can quickly refactor before running the CLI command.
### Refactoring Example
Suppose your standalone application has a do-everything`main.ts` something like this:
<code-example header="main.ts (before refactoring)" path="universal/doc-files/main.old.ts"></code-example>
It defines all of the application routes and bootstraps with an inline `ApplicationConfig` object that lists providers.
You'll need to extract these configuration details into their own files.
1. Extract the routes into a separate file called `app.routes.ts`:
<code-example header="app/app.routes.ts" path="universal/src/app/app.routes.ts"></code-example>
2. Extract the configuration object into a separate file called `app.config.ts`:
<code-example header="app/app.config.ts" path="universal/src/app/app.config.ts" region="core"></code-example>
3. Reduce `main.ts` to a streamlined form that references the `appConfig` object exported by `app.config.ts`:
<code-example header="main.ts" path="universal/src/main.ts"></code-example>
### Why this is a good idea
The Universal server needs the same configuration as your client.
You'd prefer to maintain that configuration _in one place_ so that configuration stays in sync on both the client and the universal server.
This simplicity also enables the CLI command to generate a `main.server.ts` that looks so much like `main.ts`:
<code-example header="main.server.ts" path="universal/src/main.server.ts"></code-example>
The exported `bootstrap` function is exactly what the `ngExpressEngine()` function requires in `server.ts`.
<code-example path="universal/server.ts" region="ngExpressEngine"></code-example>
<!-- links -->
<!-- external links -->
<!-- end links -->
@reviewed 2023-08-29

View file

@ -64,7 +64,7 @@ After you create this initial scaffold, you must refactor your code to use the w
<div class="alert is-important">
Some environments or platforms, such as `@angular/platform-server` used in [Server-side Rendering](guide/universal), don't support web workers.
Some environments or platforms, such as `@angular/platform-server` used in [Server-side Rendering](guide/ssr), don't support web workers.
To ensure that your application works in these environments, you must provide a fallback mechanism to perform the computations that the worker would otherwise perform.
</div>

View file

@ -488,7 +488,7 @@ You can supply a value such as the following to apply optimization to one or the
<div class="alert is-helpful">
For [Universal](guide/glossary#universal), you can reduce the code rendered in the HTML page by setting styles optimization to `true`.
For [SSR](guide/glossary#server-side-rendering), you can reduce the code rendered in the HTML page by setting styles optimization to `true`.
</div>

View file

@ -260,8 +260,8 @@
"b1": {
"desc": "Server-side Rendering for Angular apps.",
"logo": "https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png",
"title": "Angular Universal",
"url": "https://angular.io/guide/universal"
"title": "Angular SSR",
"url": "https://angular.io/guide/ssr"
},
"c1": {
"desc": "Lightweight development only Node.js® server",

View file

@ -733,9 +733,9 @@
"tooltip": "Using web workers for background processing."
},
{
"url": "guide/universal",
"url": "guide/ssr",
"title": "Server-side rendering",
"tooltip": "Render HTML server-side with Angular Universal."
"tooltip": "Render HTML server-side."
},
{
"url": "guide/prerendering",
@ -1246,11 +1246,6 @@
"title": "NgModule API",
"tooltip": "Understand the details of NgModules."
},
{
"url": "guide/universal-ngmodule",
"title": "Server-side rendering with NgModule applications",
"tooltip": "Render HTML server-side with Angular Universal (using NgModule)."
},
{
"url": "guide/ngmodule-faq",
"title": "NgModule FAQs",

View file

@ -218,7 +218,10 @@
{"type": 301, "source": "/analytics", "destination": "/cli/analytics"},
// Deprecated error messages
{"type": 301, "source": "/errors/NG6999", "destination": "https://v13.angular.io/errors/NG6999"}
{"type": 301, "source": "/errors/NG6999", "destination": "https://v13.angular.io/errors/NG6999"},
// Universal has been renamed to ssr
{"type": 301, "source": "/guide/universal", "destination": "/guide/ssr"}
],
"rewrites": [
{

View file

@ -242,4 +242,5 @@
/tutorial/toh-pt6 --> /tutorial/tour-of-heroes/toh-pt6
/cli/usage-analytics-gathering --> /cli/analytics
/analytics --> /cli/analytics
/errors/NG6999 --> https://v13.angular.io/errors/NG6999
/errors/NG6999 --> https://v13.angular.io/errors/NG6999
/guide/universal --> /guide/ssr

View file

@ -73,7 +73,7 @@ export class ExampleZipper {
// rename a custom main.ts or index.html file
_renameFile(file, exampleType) {
if (/src\/main[-.]\w+\.ts$/.test(file) && exampleType !== 'universal') {
if (/src\/main[-.]\w+\.ts$/.test(file) && exampleType !== 'ssr') {
return 'src/main.ts';
}

View file

@ -76,16 +76,16 @@ describe('example-boilerplate tool', () => {
]);
});
it('should copy all the source boilerplate files for universal (on top of the cli ones)', () => {
it('should copy all the source boilerplate files for ssr (on top of the cli ones)', () => {
const boilerplateDir = path.resolve(sharedDir, 'boilerplate');
exampleBoilerPlate.loadJsonFile.and.returnValue({projectType: 'universal'});
exampleBoilerPlate.loadJsonFile.and.returnValue({projectType: 'ssr'});
exampleBoilerPlate.add(exampleFolder, outputDir);
expect(exampleBoilerPlate.copyDirectoryContents).toHaveBeenCalledTimes(3);
expect(exampleBoilerPlate.copyDirectoryContents.calls.allArgs()).toEqual([
[`${boilerplateDir}/cli`, outputDir, jasmine.any(Function)],
[`${boilerplateDir}/universal`, outputDir, jasmine.any(Function)],
[`${boilerplateDir}/ssr`, outputDir, jasmine.any(Function)],
[`${boilerplateDir}/common`, outputDir, jasmine.any(Function)],
]);
});

View file

@ -1,8 +1,7 @@
/* To learn more about this file see: https://angular.io/guide/typescript-configuration. */
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
@ -10,15 +9,16 @@
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"

View file

@ -16,6 +16,6 @@
"exclude": [
"**/*-aot.ts",
"node_modules/",
"universal/"
"ssr/"
]
}

View file

@ -10,13 +10,18 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/browser",
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"browser": "src/main.ts",
"server": "src/main.server.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"ssr": {
"entry": "server.ts"
},
"prerender": true,
"assets": [
"src/favicon.ico",
"src/assets"
@ -43,12 +48,9 @@
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
"sourceMap": true
}
},
"defaultConfiguration": "production"
@ -57,10 +59,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "angular.io-example:build:production"
"buildTarget": "angular.io-example:build:production"
},
"development": {
"browserTarget": "angular.io-example:build:development"
"buildTarget": "angular.io-example:build:development"
}
},
"defaultConfiguration": "development"
@ -68,7 +70,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "angular.io-example:build"
"buildTarget": "angular.io-example:build"
}
},
"test": {
@ -97,58 +99,6 @@
"devServerTarget": "angular.io-example:serve:production"
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media"
},
"development": {
"optimization": false,
"sourceMap": true,
"extractLicenses": false
}
},
"defaultConfiguration": "production"
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"configurations": {
"development": {
"browserTarget": "angular.io-example:build:development",
"serverTarget": "angular.io-example:server:development"
},
"production": {
"browserTarget": "angular.io-example:build:production",
"serverTarget": "angular.io-example:server:production"
}
},
"defaultConfiguration": "development"
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"routes": [
"/"
]
},
"configurations": {
"production": {
"browserTarget": "angular.io-example:build:production",
"serverTarget": "angular.io-example:server:production"
},
"development": {
"browserTarget": "angular.io-example:build:development",
"serverTarget": "angular.io-example:server:development"
}
},
"defaultConfiguration": "production"
}
}
}

Some files were not shown because too many files have changed in this diff Show more