diff --git a/aio/content/examples/.gitignore b/aio/content/examples/.gitignore index 50e978c89f..87b99e2128 100644 --- a/aio/content/examples/.gitignore +++ b/aio/content/examples/.gitignore @@ -72,6 +72,9 @@ aot-compiler/**/*.factory.d.ts # styleguide !styleguide/src/systemjs.custom.js +# universal +!universal/**/webpack.config.universal.js + # plunkers *plnkr.no-link.html diff --git a/aio/content/examples/universal/package.steve.json b/aio/content/examples/universal/package.steve.json deleted file mode 100644 index 15ef45e849..0000000000 --- a/aio/content/examples/universal/package.steve.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "toh-universal", - "version": "1.0.0", - "description": "Tour-of-Heroes application with ng-universal server-side rendering", - "scripts": { - "build": "tsc -p src/", - "build:watch": "tsc -w", - "build:aot": "webpack --config webpack.config.aot.js", - "build:uni": "webpack --config webpack.config.uni.js", - - "serve": "lite-server -c=bs-config.json", - "serve:aot": "lite-server -c=bs-config.aot.js", - "serve:uni": "node src/dist/server.js", - "serve:uni2": "lite-server -c bs-config.uni.js", - - "prestart": "npm run build", - "start": "concurrently \"npm run build:watch\" \"npm run serve\"", - - "lint": "tslint ./src/**/*.ts -t verbose", - "ngc": "ngc", - "clean": "rimraf src/dist && rimraf src/app/*.js* && rimraf src/uni/*.js* && rimraf src/main.js*" - }, - "keywords": [], - "author": "", - "license": "MIT", - "dependencies": { - "@angular/common": "angular/common-builds", - "@angular/compiler": "angular/compiler-builds", - "@angular/compiler-cli": "angular/compiler-cli-builds", - "@angular/core": "angular/core-builds", - "@angular/forms": "angular/forms-builds", - "@angular/http": "angular/http-builds", - "@angular/platform-browser": "angular/platform-browser-builds", - "@angular/platform-browser-dynamic": "angular/platform-browser-dynamic-builds", - "@angular/platform-server": "angular/platform-server-builds", - "@angular/router": "angular/router-builds", - - "angular-in-memory-web-api": "^0.3.1", - "systemjs": "0.19.40", - "core-js": "^2.4.1", - "rxjs": "5.1.1", - "zone.js": "^0.7.7" - }, - "devDependencies": { - "concurrently": "^3.2.0", - "lite-server": "^2.2.2", - "typescript": "~2.1.6", - - "canonical-path": "0.0.2", - "tslint": "^3.15.1", - "lodash": "^4.16.4", - "rimraf": "^2.5.4", - - "@types/node": "^6.0.46", - - "@ngtools/webpack": "^1.2.11", - "@types/express": "^4.0.35", - "raw-loader": "^0.5.1", - "webpack": "^2.2.1" - }, - "repository": {} -} diff --git a/aio/content/examples/universal/src/app/app-routing.module.ts b/aio/content/examples/universal/src/app/app-routing.module.ts index bc070f6c31..96b5a4a691 100644 --- a/aio/content/examples/universal/src/app/app-routing.module.ts +++ b/aio/content/examples/universal/src/app/app-routing.module.ts @@ -9,7 +9,8 @@ const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, - { path: 'heroes', component: HeroesComponent } + { path: 'heroes', component: HeroesComponent }, + { path: '**', redirectTo: '/dashboard' } ]; @NgModule({ diff --git a/aio/content/examples/universal/src/app/app.component.css b/aio/content/examples/universal/src/app/app.component.css index 071e665767..40e1aba36d 100644 --- a/aio/content/examples/universal/src/app/app.component.css +++ b/aio/content/examples/universal/src/app/app.component.css @@ -1,4 +1,3 @@ -/* #docregion */ h1 { font-size: 1.2em; color: #999; diff --git a/aio/content/examples/universal/src/app/app.component.ts b/aio/content/examples/universal/src/app/app.component.ts index a9fe05a9a8..68079538a2 100644 --- a/aio/content/examples/universal/src/app/app.component.ts +++ b/aio/content/examples/universal/src/app/app.component.ts @@ -1,5 +1,3 @@ -// #docplaster -// #docregion import { Component } from '@angular/core'; @Component({ diff --git a/aio/content/examples/universal/src/app/app.module.ts b/aio/content/examples/universal/src/app/app.module.ts index e078044928..5de3de00c0 100644 --- a/aio/content/examples/universal/src/app/app.module.ts +++ b/aio/content/examples/universal/src/app/app.module.ts @@ -1,54 +1,62 @@ // #docplaster -// #docregion -// #docregion v1, v2 -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; +// #docregion simple +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; import { AppRoutingModule } from './app-routing.module'; -// #enddocregion v1 // Imports for loading & configuring the in-memory web api -import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; -import { InMemoryDataService } from './in-memory-data.service'; +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; -// #docregion v1 import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; -// #enddocregion v1, v2 import { HeroSearchComponent } from './hero-search.component'; -// #docregion v1, v2 +// #enddocregion simple +// #docregion platform-detection +import { PLATFORM_ID, APP_ID, Inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; + +// #enddocregion platform-detection +// #docregion simple @NgModule({ imports: [ - BrowserModule.withServerTransition({ - appId: 'toh-universal' - }), + // #docregion browsermodule + BrowserModule.withServerTransition({ appId: 'uni' }), + // #enddocregion browsermodule FormsModule, - HttpModule, - // #enddocregion v1 - // #docregion in-mem-web-api - InMemoryWebApiModule.forRoot(InMemoryDataService), - // #enddocregion in-mem-web-api - // #docregion v1 + HttpClientModule, + HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService), AppRoutingModule ], - // #docregion search declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, - // #enddocregion v1, v2 HeroSearchComponent - // #docregion v1, v2 ], - // #enddocregion search providers: [ HeroService ], bootstrap: [ AppComponent ] }) -export class AppModule { } +export class AppModule { + +// #enddocregion simple +// #docregion platform-detection +constructor( + @Inject(PLATFORM_ID) private platformId: Object, + @Inject(APP_ID) private appId: string) { + const platform = isPlatformBrowser(platformId) ? + 'on the server' : 'in the browser'; + console.log(`Running ${platform} with appId=${appId}`); + } +// #enddocregion platform-detection +// #docregion simple +} +// #enddocregion simple diff --git a/aio/content/examples/universal/src/app/dashboard.component.css b/aio/content/examples/universal/src/app/dashboard.component.css index dc7fb7ce06..096cec7621 100644 --- a/aio/content/examples/universal/src/app/dashboard.component.css +++ b/aio/content/examples/universal/src/app/dashboard.component.css @@ -1,4 +1,3 @@ -/* #docregion */ [class*='col-'] { float: left; padding-right: 20px; diff --git a/aio/content/examples/universal/src/app/dashboard.component.html b/aio/content/examples/universal/src/app/dashboard.component.html index db8546ccd2..8852696c84 100644 --- a/aio/content/examples/universal/src/app/dashboard.component.html +++ b/aio/content/examples/universal/src/app/dashboard.component.html @@ -1,7 +1,6 @@ -

Top Heroes

- +

{{hero.name}}

diff --git a/aio/content/examples/universal/src/app/dashboard.component.ts b/aio/content/examples/universal/src/app/dashboard.component.ts index 9960aa77d4..2f9cbeb3ec 100644 --- a/aio/content/examples/universal/src/app/dashboard.component.ts +++ b/aio/content/examples/universal/src/app/dashboard.component.ts @@ -1,22 +1,23 @@ -// #docregion , search import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; +import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; + @Component({ selector: 'my-dashboard', templateUrl: './dashboard.component.html', styleUrls: [ './dashboard.component.css' ] }) -// #enddocregion search export class DashboardComponent implements OnInit { - heroes: Hero[] = []; + heroes: Observable; constructor(private heroService: HeroService) { } ngOnInit(): void { - this.heroService.getHeroes() - .then(heroes => this.heroes = heroes.slice(1, 5)); + this.heroes = this.heroService.getHeroes() + .map(heroes => heroes.slice(1, 5)); } } diff --git a/aio/content/examples/universal/src/app/hero-detail.component.css b/aio/content/examples/universal/src/app/hero-detail.component.css index ab2437efd8..f6139ba274 100644 --- a/aio/content/examples/universal/src/app/hero-detail.component.css +++ b/aio/content/examples/universal/src/app/hero-detail.component.css @@ -1,4 +1,3 @@ -/* #docregion */ label { display: inline-block; width: 3em; @@ -25,6 +24,6 @@ button:hover { } button:disabled { background-color: #eee; - color: #ccc; + color: #ccc; cursor: auto; } diff --git a/aio/content/examples/universal/src/app/hero-detail.component.html b/aio/content/examples/universal/src/app/hero-detail.component.html index 32fe6d4391..af377615c3 100644 --- a/aio/content/examples/universal/src/app/hero-detail.component.html +++ b/aio/content/examples/universal/src/app/hero-detail.component.html @@ -1,4 +1,3 @@ -

{{hero.name}} details!

@@ -8,7 +7,5 @@
- -
diff --git a/aio/content/examples/universal/src/app/hero-detail.component.ts b/aio/content/examples/universal/src/app/hero-detail.component.ts index 676c4c21b0..6e6cdace49 100644 --- a/aio/content/examples/universal/src/app/hero-detail.component.ts +++ b/aio/content/examples/universal/src/app/hero-detail.component.ts @@ -1,4 +1,3 @@ -// #docregion import 'rxjs/add/operator/switchMap'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; @@ -27,12 +26,11 @@ export class HeroDetailComponent implements OnInit { .subscribe(hero => this.hero = hero); } - // #docregion save save(): void { - this.heroService.update(this.hero) - .then(() => this.goBack()); + this.heroService + .update(this.hero) + .subscribe(() => this.goBack()); } - // #enddocregion save goBack(): void { this.location.back(); diff --git a/aio/content/examples/universal/src/app/hero-search.component.css b/aio/content/examples/universal/src/app/hero-search.component.css index 9bf8d13457..85f7da408e 100644 --- a/aio/content/examples/universal/src/app/hero-search.component.css +++ b/aio/content/examples/universal/src/app/hero-search.component.css @@ -1,4 +1,3 @@ -/* #docregion */ .search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; diff --git a/aio/content/examples/universal/src/app/hero-search.component.html b/aio/content/examples/universal/src/app/hero-search.component.html index 08c0560c5b..43186da3b3 100644 --- a/aio/content/examples/universal/src/app/hero-search.component.html +++ b/aio/content/examples/universal/src/app/hero-search.component.html @@ -1,4 +1,3 @@ -

Hero Search

diff --git a/aio/content/examples/universal/src/app/hero-search.component.ts b/aio/content/examples/universal/src/app/hero-search.component.ts index 8b2d32f06b..465f73834c 100644 --- a/aio/content/examples/universal/src/app/hero-search.component.ts +++ b/aio/content/examples/universal/src/app/hero-search.component.ts @@ -1,20 +1,14 @@ -// #docplaster -// #docregion import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -// #docregion rxjs-imports import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; -// Observable class extensions import 'rxjs/add/observable/of'; -// Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; -// #enddocregion rxjs-imports import { HeroSearchService } from './hero-search.service'; import { Hero } from './hero'; @@ -26,27 +20,20 @@ import { Hero } from './hero'; providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { - // #docregion search heroes: Observable; - // #enddocregion search - // #docregion searchTerms private searchTerms = new Subject(); - // #enddocregion searchTerms constructor( private heroSearchService: HeroSearchService, private router: Router) {} - // #docregion searchTerms // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } - // #enddocregion searchTerms - // #docregion search ngOnInit(): void { - this.heroes = this.searchTerms + this.heroes = this.searchTerms.asObservable() .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes @@ -60,7 +47,6 @@ export class HeroSearchComponent implements OnInit { return Observable.of([]); }); } - // #enddocregion search gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; diff --git a/aio/content/examples/universal/src/app/hero-search.service.ts b/aio/content/examples/universal/src/app/hero-search.service.ts index 52bf95a158..2843d4e3b1 100644 --- a/aio/content/examples/universal/src/app/hero-search.service.ts +++ b/aio/content/examples/universal/src/app/hero-search.service.ts @@ -1,20 +1,28 @@ -// #docregion -import { Injectable } from '@angular/core'; -import { Http } from '@angular/http'; +import { Inject, Injectable, Optional } from '@angular/core'; +import { APP_BASE_HREF } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; -import { Hero } from './hero'; +import { Hero } from './hero'; +// #docregion class @Injectable() export class HeroSearchService { - constructor(private http: Http) {} + private searchUrl = 'api/heroes/?name='; // URL to web api + + constructor( + private http: HttpClient, + @Optional() @Inject(APP_BASE_HREF) origin: string) { + this.searchUrl = (origin || '') + this.searchUrl; + } search(term: string): Observable { return this.http - .get(`api/heroes/?name=${term}`) - .map(response => response.json().data as Hero[]); + .get(this.searchUrl + term) + .map((data: any) => data.data as Hero[]); } } +// #enddocregion class diff --git a/aio/content/examples/universal/src/app/hero.service.ts b/aio/content/examples/universal/src/app/hero.service.ts index 18af476123..6dc05481e5 100644 --- a/aio/content/examples/universal/src/app/hero.service.ts +++ b/aio/content/examples/universal/src/app/hero.service.ts @@ -1,87 +1,64 @@ -// #docplaster -// #docregion , imports -import { Injectable } from '@angular/core'; -import { Headers, Http } from '@angular/http'; +import { Injectable, Inject, Optional } from '@angular/core'; +import { APP_BASE_HREF } from '@angular/common'; +import { HttpHeaders, HttpClient } from '@angular/common/http'; -// #docregion rxjs -import 'rxjs/add/operator/toPromise'; -// #enddocregion rxjs +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/map'; import { Hero } from './hero'; -// #enddocregion imports + +const headers = new HttpHeaders({'Content-Type': 'application/json'}); @Injectable() export class HeroService { - // #docregion update - private headers = new Headers({'Content-Type': 'application/json'}); - // #enddocregion update - // #docregion getHeroes private heroesUrl = 'api/heroes'; // URL to web api - constructor(private http: Http) { } + // #docregion ctor + constructor( + private http: HttpClient, + @Optional() @Inject(APP_BASE_HREF) origin: string) { + this.heroesUrl = (origin || '') + this.heroesUrl; + } + // #enddocregion ctor - getHeroes(): Promise { + getHeroes(): Observable { return this.http.get(this.heroesUrl) - // #docregion to-promise - .toPromise() - // #enddocregion to-promise - // #docregion to-data - .then(response => response.json().data as Hero[]) - // #enddocregion to-data - // #docregion catch + .map((data: any) => data.data as Hero[]) .catch(this.handleError); - // #enddocregion catch } - // #enddocregion getHeroes - - // #docregion getHero - getHero(id: number): Promise { + getHero(id: number): Observable { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) - .toPromise() - .then(response => response.json().data as Hero) + .map((data: any) => data.data as Hero) .catch(this.handleError); } - // #enddocregion getHero - // #docregion delete - delete(id: number): Promise { + delete(id: number): Observable { const url = `${this.heroesUrl}/${id}`; - return this.http.delete(url, {headers: this.headers}) - .toPromise() - .then(() => null) + return this.http.delete(url, { headers }) .catch(this.handleError); } - // #enddocregion delete - // #docregion create - create(name: string): Promise { + create(name: string): Observable { return this.http - .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) - .toPromise() - .then(res => res.json().data) + .post(this.heroesUrl, { name: name }, { headers }) + .map((data: any) => data.data) .catch(this.handleError); } - // #enddocregion create - // #docregion update - update(hero: Hero): Promise { + update(hero: Hero): Observable { const url = `${this.heroesUrl}/${hero.id}`; return this.http - .put(url, JSON.stringify(hero), {headers: this.headers}) - .toPromise() - .then(() => hero) + .put(url, hero, { headers }) .catch(this.handleError); } - // #enddocregion update - // #docregion getHeroes, handleError - private handleError(error: any): Promise { + private handleError(error: any): Observable { console.error('An error occurred', error); // for demo purposes only - return Promise.reject(error.message || error); + throw error; } - // #enddocregion getHeroes, handleError } - diff --git a/aio/content/examples/universal/src/app/heroes.component.ts b/aio/content/examples/universal/src/app/heroes.component.ts index 6350b803c4..acd2dde1d2 100644 --- a/aio/content/examples/universal/src/app/heroes.component.ts +++ b/aio/content/examples/universal/src/app/heroes.component.ts @@ -21,31 +21,27 @@ export class HeroesComponent implements OnInit { getHeroes(): void { this.heroService .getHeroes() - .then(heroes => this.heroes = heroes); + .subscribe(heroes => this.heroes = heroes); } - // #docregion add add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.create(name) - .then(hero => { + .subscribe(hero => { this.heroes.push(hero); this.selectedHero = null; }); } - // #enddocregion add - // #docregion delete delete(hero: Hero): void { this.heroService .delete(hero.id) - .then(() => { + .subscribe(() => { this.heroes = this.heroes.filter(h => h !== hero); if (this.selectedHero === hero) { this.selectedHero = null; } }); } - // #enddocregion delete ngOnInit(): void { this.getHeroes(); diff --git a/aio/content/examples/universal/src/index-aot.html b/aio/content/examples/universal/src/index-aot.html deleted file mode 100644 index 096bfbb512..0000000000 --- a/aio/content/examples/universal/src/index-aot.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Angular Tour of Heroes - - - - - - - - Loading... - - - - - diff --git a/aio/content/examples/universal/src/main-aot.ts b/aio/content/examples/universal/src/main-aot.ts deleted file mode 100644 index fcb35dc9dc..0000000000 --- a/aio/content/examples/universal/src/main-aot.ts +++ /dev/null @@ -1,5 +0,0 @@ -// #docregion -import { platformBrowser } from '@angular/platform-browser'; -import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory'; - -platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); diff --git a/aio/content/examples/universal/src/uni/server-aot.ts b/aio/content/examples/universal/src/uni/server-aot.ts deleted file mode 100644 index 38eb3f6b33..0000000000 --- a/aio/content/examples/universal/src/uni/server-aot.ts +++ /dev/null @@ -1,40 +0,0 @@ -import 'zone.js/dist/zone-node'; -import { enableProdMode } from '@angular/core'; -// import { AppServerModule } from './app.server'; -import { AppServerModuleNgFactory } from '../../aot/src/uni/app.server.ngfactory'; -import * as express from 'express'; -import { ngUniversalEngine } from './universal-engine'; - -enableProdMode(); - -const server = express(); - -// set our angular engine as the handler for html files, so it will be used to render them. -server.engine('html', ngUniversalEngine({ - bootstrap: [AppServerModuleNgFactory] -})); - -// set default view directory -server.set('views', 'src'); - -// handle requests for routes in the app. ngExpressEngine does the rendering. -server.get(['/', '/dashboard', '/heroes', '/detail/:id'], (req, res) => { - res.render('index-aot.html', {req}); -}); - -// handle requests for static files -server.get(['/*.js', '/*.css'], (req, res, next) => { - let fileName: string = req.originalUrl; - console.log(fileName); - let root = fileName.startsWith('/node_modules/') ? '.' : 'src'; - res.sendFile(fileName, { root: root }, function (err) { - if (err) { - next(err); - } - }); -}); - -// start the server -server.listen(3200, () => { - console.log('listening on port 3200...'); -}); diff --git a/aio/content/examples/universal/src/uni/universal-engine.ts b/aio/content/examples/universal/src/uni/universal-engine.ts deleted file mode 100644 index 3c0ac6b528..0000000000 --- a/aio/content/examples/universal/src/uni/universal-engine.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Express/Connect middleware for rendering pages using Angular Universal - */ -import * as fs from 'fs'; -import { renderModuleFactory } from '@angular/platform-server'; - -const templateCache = {}; // cache for page templates -const outputCache = {}; // cache for rendered pages - -export function ngUniversalEngine(setupOptions: any) { - - return function (filePath: string, options: { req: Request }, callback: (err: Error, html: string) => void) { - let url: string = options.req.url; - let html: string = outputCache[url]; - if (html) { - // return already-built page for this url - console.log('from cache: ' + url); - callback(null, html); - return; - } - - console.log('building: ' + url); - if (!templateCache[filePath]) { - let file = fs.readFileSync(filePath); - templateCache[filePath] = file.toString(); - } - - // render the page via angular platform-server - let appModuleFactory = setupOptions.bootstrap[0]; - renderModuleFactory(appModuleFactory, { - document: templateCache[filePath], - url: url - }).then(str => { - outputCache[url] = str; - callback(null, str); - }); - }; -} diff --git a/aio/content/examples/universal/src/uni/app.server.ts b/aio/content/examples/universal/src/universal/app-server.module.ts similarity index 59% rename from aio/content/examples/universal/src/uni/app.server.ts rename to aio/content/examples/universal/src/universal/app-server.module.ts index 585470cd09..1ae883e2ed 100644 --- a/aio/content/examples/universal/src/uni/app.server.ts +++ b/aio/content/examples/universal/src/universal/app-server.module.ts @@ -1,21 +1,19 @@ +// #docregion import { NgModule } from '@angular/core'; -import { APP_BASE_HREF } from '@angular/common'; import { ServerModule } from '@angular/platform-server'; import { AppComponent } from '../app/app.component'; import { AppModule } from '../app/app.module'; @NgModule({ imports: [ + AppModule, ServerModule, - AppModule + ], + providers: [ + // Add universal-only providers here ], bootstrap: [ AppComponent - ], - providers: [ - {provide: APP_BASE_HREF, useValue: '/'} - // { provide: NgModuleFactoryLoader, useClass: ServerRouterLoader } ] }) -export class AppServerModule { -} +export class AppServerModule {} diff --git a/aio/content/examples/universal/src/universal/server.ts b/aio/content/examples/universal/src/universal/server.ts new file mode 100644 index 0000000000..e307c7d622 --- /dev/null +++ b/aio/content/examples/universal/src/universal/server.ts @@ -0,0 +1,65 @@ +// Express Server for Angular Universal app +import 'zone.js/dist/zone-node'; +import * as express from 'express'; +import { enableProdMode } from '@angular/core'; + +// #docregion import-app-server-factory +// AppServerModuleNgFactory, generated by AOT compiler, is not available at design time +import { AppServerModuleNgFactory } from '../../aot/src/universal/app-server.module.ngfactory'; +// #enddocregion import-app-server-factory + +import { universalEngine } from './universal-engine'; + +enableProdMode(); + +const port = 3200; +const server = express(); + +// #docregion universal-engine +// Render HTML files with the universal template engine +server.engine('html', universalEngine({ + appModuleFactory: AppServerModuleNgFactory +})); + +// engine should find templates (like index.html) in 'src/' by default +server.set('views', 'src'); +// #enddocregion universal-engine + +// CRITICAL TODO: add authentication/authorization middleware + +// #docregion data-request +// TODO: implement data requests securely +server.get('/api/*', (req, res) => { + res.status(404).send('data requests are not supported'); +}); +// #enddocregion data-request + +// #docregion navigation-request +// simplistic regex matches any path without a '.' +const pathWithNoExt = /^([^.]*)$/; + +// treat any path without an extension as in-app navigation +server.get(pathWithNoExt, (req, res) => { + // render with the universal template engine + res.render('index.html', { req }); +}); +// #enddocregion navigation-request + +// #docregion static +// remaining requests are for static files +server.use((req, res, next) => { + const fileName = req.originalUrl; + console.log(fileName); + + // security: only serve files from node_modules or src + const root = fileName.startsWith('/node_modules/') ? '.' : 'src'; + res.sendFile(fileName, { root }, err => { + if (err) { next(err); } + }); +}); +// #enddocregion static + +// start the server +server.listen(port, () => { + console.log(`listening on port ${port}...`); +}); diff --git a/aio/content/examples/universal/src/universal/universal-engine.ts b/aio/content/examples/universal/src/universal/universal-engine.ts new file mode 100644 index 0000000000..bab955b8de --- /dev/null +++ b/aio/content/examples/universal/src/universal/universal-engine.ts @@ -0,0 +1,62 @@ +/** + * Node Express template engine for Universal apps + */ +import * as fs from 'fs'; +import { Request } from 'express'; + +import { renderModuleFactory } from '@angular/platform-server'; +import { APP_BASE_HREF } from '@angular/common'; + +const templateCache: { [key: string]: string } = {}; // page templates +const outputCache: { [key: string]: string } = {}; // rendered pages + +export function universalEngine(setupOptions: any) { + + // Express template engine middleware + return function ( + filePath: string, + options: { req: Request }, + callback: (err: Error, html: string) => void) { + + const { req } = options; + const routeUrl = req.url; + + const html = outputCache[routeUrl]; + if (html) { + // return already-built page for this url + console.log('from cache: ' + routeUrl); + callback(null, html); + return; + } + + console.log('building: ' + routeUrl); + let template = templateCache[filePath]; + if (!template) { + template = fs.readFileSync(filePath).toString(); + templateCache[filePath] = template; + } + + const { appModuleFactory } = setupOptions; + const origin = getOrigin(req); + + // render the page + // #docregion render + renderModuleFactory(appModuleFactory, { + document: template, + url: routeUrl, + extraProviders: [ + { provide: APP_BASE_HREF, useValue: origin } + ] + }) + .then(page => { + outputCache[routeUrl] = page; + callback(null, page); + }); + // #enddocregion render + }; +} + +function getOrigin(req: Request) { + // e.g., http://localhost:3200/ + return `${req.protocol}://${req.hostname}:${req.connection.address().port}/`; +} diff --git a/aio/content/examples/universal/tsconfig-aot.json b/aio/content/examples/universal/tsconfig-aot.json deleted file mode 100644 index fd4632d05f..0000000000 --- a/aio/content/examples/universal/tsconfig-aot.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "es2015", - "moduleResolution": "node", - "sourceMap": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "lib": ["es2015", "dom"], - "noImplicitAny": true, - "suppressImplicitAnyIndexErrors": true, - "typeRoots": [ - "./node_modules/@types/" - ] - }, - - "files": [ - "src/app/app.module.ts", - "src/main-aot.ts" - ], - - "angularCompilerOptions": { - "genDir": "aot", - "entryModule": "./src/app/app.module#AppModule", - "skipMetadataEmit" : true - } -} diff --git a/aio/content/examples/universal/tsconfig-uni.json b/aio/content/examples/universal/tsconfig-universal.json similarity index 87% rename from aio/content/examples/universal/tsconfig-uni.json rename to aio/content/examples/universal/tsconfig-universal.json index fcb6a7c92d..f23de99091 100644 --- a/aio/content/examples/universal/tsconfig-uni.json +++ b/aio/content/examples/universal/tsconfig-universal.json @@ -15,8 +15,8 @@ }, "files": [ - "src/uni/app.server.ts", - "src/uni/server-aot.ts" + "src/universal/app-server.module.ts", + "src/universal/server.ts" ], "angularCompilerOptions": { diff --git a/aio/content/examples/universal/webpack.config.universal.js b/aio/content/examples/universal/webpack.config.universal.js new file mode 100644 index 0000000000..f2d31f85ee --- /dev/null +++ b/aio/content/examples/universal/webpack.config.universal.js @@ -0,0 +1,40 @@ +const ngtools = require('@ngtools/webpack'); +const webpack = require('webpack'); + +module.exports = { + devtool: 'source-map', +// #docregion entry + entry: { + main: [ + './src/universal/app-server.module.ts', + './src/universal/server.ts' + ] + }, +// #enddocregion entry + resolve: { + extensions: ['.ts', '.js'] + }, + target: 'node', +// #docregion output + output: { + path: 'src/dist', + filename: 'server.js' + }, +// #enddocregion output +// #docregion plugins + plugins: [ + new ngtools.AotPlugin({ + tsConfigPath: './tsconfig-universal.json' + }) + ], +// #enddocregion plugins +// #docregion rules + module: { + rules: [ + { test: /\.css$/, loader: 'raw-loader' }, + { test: /\.html$/, loader: 'raw-loader' }, + { test: /\.ts$/, loader: '@ngtools/webpack' } + ] + } +// #enddocregion rules +} diff --git a/aio/content/examples/universal/zipper.json b/aio/content/examples/universal/zipper.json new file mode 100644 index 0000000000..a493091fd5 --- /dev/null +++ b/aio/content/examples/universal/zipper.json @@ -0,0 +1,9 @@ +{ + "files":[ + "!**/*.d.ts", + "!**/src/**/*.js", + "!**/universal/**/*.js" + ], + "removeSystemJsConfig": false, + "type": "universal" +} diff --git a/aio/content/guide/universal.md b/aio/content/guide/universal.md new file mode 100644 index 0000000000..31d72768de --- /dev/null +++ b/aio/content/guide/universal.md @@ -0,0 +1,849 @@ +# Angular Universal + +This guide describes **Angular Universal**, a technology that runs your Angular application on the server. + +A normal Angular application executes in the _browser_, rendering pages in the DOM in response to user actions. + +**Angular Universal** generates _static_ application pages on the _server_ +through a process called **server-side rendering (SSR)**. + +It can generate and serve those pages in response to requests from browsers. +It can also pre-generate pages as HTML files that you serve later. + +Universal's server-side rendering has several potential benefits: + +* [Facilitate web crawlers (SEO)](#web-crawlers). +* [Show content sooner](#startup-performance). +* [Perform well on mobile and low power devices](#no-javascript). + +This guide describes a Universal sample application that launches quickly as a server-rendered page. +Meanwhile, the browser downloads the full client version and switches to it automatically after the code loads. + +
+ +[Download the finished sample code](generated/zips/universal/universal.zip), +which runs in a [node express](https://expressjs.com/) server. + +Almost _any_ web server technology can serve a Universal app. +See this advanced example written for +[ASP.NET Core](https://github.com/MarkPieszak/aspnetcore-angular2-universal). +
+ +
+ +The build setup described in this guide is experimental and subject to change. + +
+ +## Overview + +This overview explains the benefits of a Universal application, how it works, and the limitations of server-side rendering. Then it describes the sample application that goes with this guide. + +Subsequent sections describe a sample Universal application derived from the Tour of Heroes tutorial +and explain how to build and run that app. + +{@a why-do-it} + +### Why Universal + +There are three main reasons to create a Universal version of your app. + +1. Facilitate web crawlers (SEO) +1. Improve performance on mobile and low-powered devices +1. Show the first page quickly + +{@a seo} +{@a web-crawlers} +#### Facilitate web crawlers + +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 may 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 app that is easy searchable, linkable, and navigable without JavaScript. +It also makes a site preview available since each URL returns a fully-rendered page. + +Enabling web crawlers is often referred to as +[Search Engine Optimization (SEO)](https://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf). + +{@a no-javascript} + +#### Performance on mobile and low performance devices + +Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. +For these cases, you may require a server-rendered, no-JavaScript version of the app. +This version, however limited, may be the only practical alternative for +people who otherwise would not be able to use the app at all. + +{@a startup-performance} + +#### Show the first page quickly + +Displaying the first page quickly can be critical for user engagement. + +Captive users of a line-of-business app may have to wait. +But a casual visitor will switch to a faster site if your app takes "too long" to show the first page. + +While [AOT](guide/aot-compiler) compilation speeds up application start times, it may not be fast enough, especially on mobile devices with slow connections. +[53% of mobile site visits are abandoned](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) if pages take longer than 3 seconds to load. +Your app needs to load quickly, to engage users before they decide to do something else. + +With Angular Universal, you can generate landing pages for the app that look like the complete app. +The pages are pure HTML, and can display even if JavaScript is disabled. +The pages do not handle browser events, but they _do_ support navigation through the site using [routerLink](guide/router.html#router-link). + +Of course most Angular apps are highly interactive. +The landing page looks real and is far more useful than a "loading" spinner. +But it won't fool anyone for long. + +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 app behind it in the manner [explained below](#transition). +The user perceives near-instant performance from the landing page +and gets the full interactive experience after the full app loads. + +
+Another tool called Preboot can record browser events such as user keystrokes during the transition and play them back in the full Angular app once it is loaded. +
+ +{@a how-does-it-work} +### How it works + +To make a Universal app, you install the `platform-server` package. +The `platform-server` package has server implementations of the DOM, `XMLHttpRequest`, and other low-level features that do not rely on a browser. + +You compile the client application with the `platform-server` module instead of the `platform-browser` module. +and run the resulting Universal app on a web server. + +The server (a [Node Express](https://expressjs.com/) server in _this_ guide's example) +passes client requests for application pages to Universal's `renderModuleFactory` function. + +The `renderModuleFactory` function takes as inputs a *template* HTML page (usually `index.html`), +an Angular *module* containing components, +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 `renderModuleFactory` renders that view within the `` tag of the template, creating a finished HTML page for the client. + +Finally, the server returns the rendered page to the client. + +{@a limitations} + +### Working around the browser APIs + +Because a Universal `platform-server` app doesn't execute in the browser, you may have to work around some of the APIs and capabilities that you otherwise take for granted on the client. + +You won't be able reference browser-only native objects such as `window`, `document`, `navigator` or `location`. +If you don't need them on the server-rendered page, side-step them with conditional logic. + +Alternatively, look for an injectable Angular abstraction over the object you need such as `Location` or `Document`; +it may substitute adequately for the specific API that you're calling. +If Angular doesn't provide it, you may be able to write your own abstraction that delegates to the browser API while in the browser and to a satisfactory alternative implementation while on the server. + +Without mouse or keyboard events, a universal app can't rely on a user clicking a button to show a component. +A universal app should determine what to render based solely on the incoming client request. +This is a good argument for making the app [routeable](guide/router). + +Http requests with _relative_ URLs don't work. +You should convert them to _absolute_ URLs on the server which means you'll need to know the server origin. +You can pass the server origin into your app with a [provider](guide/dependency-injection#injector-providers) "universal/*" + as you'll see in the [example below](#http-urls). + +Because the user of a server-rendered page can't do much more than click links, +you should [swap in the real client app](#transition) as quickly as possible for a proper interactive experience. + +{@a the-example} + +## The example + +The _Tour of Heroes_ tutorial is the foundation for the Universal sample described in this guide. + +The core application files are mostly untouched, with a few exceptions described below. +You'll add more files to support building and serving with Universal. + +In this example, Webpack tools compile and bundle the Universal version of the app with the +[AOT (Ahead-of-Time) compiler](guide/aot-compiler). +A node/express web server turns client requests into the HTML pages rendered by Universal. + +You will create: + + * a server-side app module, `app.server.module.ts` + * a Universal app renderer, `universal-engine.ts` + * an express web server to handle requests, `server.ts` + * a TypeScript config file, `tsconfig-universal.json` + * a Webpack config file, `webpack.config.universal.js` + +When you're done, the folder structure will look like this: + + +src/ + index.html app web page + main.ts bootstrapper for client app + style.css styles for the app + systemjs.config.js SystemJS client configuration + systemjs-angular-loader.js SystemJS add-in + tsconfig.json TypeScript client configuration + app/ ... application code + dist/ + server.js * AOT-compiled server bundle + universal/ * folder for universal code + app-server.module.ts * server-side application module + server.ts * express web server + universal-engine.ts * express template engine +bs-config.json config file for lite server +package.json npm configuration +tsconfig-universal.json * TypeScript Universal configuration +webpack.config.universal.js * Webpack Universal configuration + + +The files marked with `*` are new and not in the original tutorial sample. +This guide covers them in the sections below. + +{@a preparation} + +## Preparation + +{@a install-the-tools} + +### Install the tools + +To get started, install these Universal and Webpack packages. + + * `@angular/compiler-cli` - contains the AOT compiler + * `@angular/platform-server` - Universal server-side components + * `webpack` - Webpack JavaScript bundler + * `@ngtools/webpack` - Webpack loader and plugin for bundling compiled applications + * `raw-loader` - Webpack loader for text files + * `express` - node web server + * `@types/express` - TypeScript type definitions for express + +Install them with the following commands: + + +npm install @angular/compiler-cli @angular/platform-server express --save +npm install webpack @ngtools/webpack raw-loader @types/express --save-dev + + +### Modify the client app + +You'll have to modify the client application in a few small ways to enable server-side rendering and +to facilitate the transition from the Universal app to the client app. + +{@a transition} + +#### Enable transition to the client app + +A Universal app can act as a dynamic "splash screen" that shows a view of your app while the real client app loads behind it. +This gives the appearance of a near-instant application. + +Meanwhile, the browser downloads the client app scripts in background. +Once loaded, Angular transitions from the static server-rendered page to the dynamically rendered views of the live client app. + +To make this work, the template for server-side rendering contains the `