diff --git a/aio/content/examples/.gitignore b/aio/content/examples/.gitignore index 17584c4f09..aa10a7c858 100644 --- a/aio/content/examples/.gitignore +++ b/aio/content/examples/.gitignore @@ -74,8 +74,7 @@ aot-compiler/**/*.factory.d.ts !styleguide/src/systemjs.custom.js # universal -!universal/webpack.config.client.js -!universal/webpack.config.universal.js +!universal/webpack.server.config.js # plunkers *plnkr.no-link.html diff --git a/aio/content/examples/universal/example-config.json b/aio/content/examples/universal/example-config.json index 61227cc07a..2c13a4178b 100644 --- a/aio/content/examples/universal/example-config.json +++ b/aio/content/examples/universal/example-config.json @@ -1,3 +1,3 @@ { - "projectType": "systemjs" + "projectType": "universal" } diff --git a/aio/content/examples/universal/server.ts b/aio/content/examples/universal/server.ts new file mode 100644 index 0000000000..fd5f3e1259 --- /dev/null +++ b/aio/content/examples/universal/server.ts @@ -0,0 +1,61 @@ +// These are important and needed before anything else +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; + +import { enableProdMode } from '@angular/core'; + +import * as express from 'express'; +import { join } from 'path'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Express server +const app = express(); + +const PORT = process.env.PORT || 4000; +const DIST_FOLDER = join(process.cwd(), 'dist'); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle'); + +// Express Engine +import { ngExpressEngine } from '@nguniversal/express-engine'; +// Import module map for lazy loading +import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; + +// #docregion ngExpressEngine +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP) + ] +})); +// #enddocregion ngExpressEngine + +app.set('view engine', 'html'); +app.set('views', join(DIST_FOLDER, 'browser')); + +// #docregion data-request +// TODO: implement data requests securely +app.get('/api/*', (req, res) => { + res.status(404).send('data requests are not supported'); +}); +// #enddocregion data-request + +// #docregion static +// Server static files from /browser +app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); +// #enddocregion static + +// #docregion navigation-request +// All regular routes use the Universal engine +app.get('*', (req, res) => { + res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req }); +}); +// #enddocregion navigation-request + +// Start up the Node server +app.listen(PORT, () => { + console.log(`Node server listening on http://localhost:${PORT}`); +}); 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 96b5a4a691..969e78b276 100644 --- a/aio/content/examples/universal/src/app/app-routing.module.ts +++ b/aio/content/examples/universal/src/app/app-routing.module.ts @@ -1,16 +1,15 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { DashboardComponent } from './dashboard.component'; -import { HeroesComponent } from './heroes.component'; -import { HeroDetailComponent } from './hero-detail.component'; +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: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, - { path: 'heroes', component: HeroesComponent }, - { path: '**', redirectTo: '/dashboard' } + { path: 'heroes', component: HeroesComponent } ]; @NgModule({ diff --git a/aio/content/examples/universal/src/app/app.component.css b/aio/content/examples/universal/src/app/app.component.css index 40e1aba36d..bf741e4575 100644 --- a/aio/content/examples/universal/src/app/app.component.css +++ b/aio/content/examples/universal/src/app/app.component.css @@ -1,3 +1,4 @@ +/* AppComponent's private CSS styles */ h1 { font-size: 1.2em; color: #999; diff --git a/aio/content/examples/universal/src/app/app.component.html b/aio/content/examples/universal/src/app/app.component.html new file mode 100644 index 0000000000..49c7d171b6 --- /dev/null +++ b/aio/content/examples/universal/src/app/app.component.html @@ -0,0 +1,7 @@ +

{{title}}

+ + + diff --git a/aio/content/examples/universal/src/app/app.component.ts b/aio/content/examples/universal/src/app/app.component.ts index 68079538a2..c41599e92a 100644 --- a/aio/content/examples/universal/src/app/app.component.ts +++ b/aio/content/examples/universal/src/app/app.component.ts @@ -1,15 +1,8 @@ -import { Component } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ - selector: 'my-app', - template: ` -

{{title}}

- - - `, + selector: 'app-root', + templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { diff --git a/aio/content/examples/universal/src/app/app.module.ts b/aio/content/examples/universal/src/app/app.module.ts index 5de3de00c0..6d07040733 100644 --- a/aio/content/examples/universal/src/app/app.module.ts +++ b/aio/content/examples/universal/src/app/app.module.ts @@ -1,62 +1,60 @@ // #docplaster -// #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 { 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'; - -// Imports for loading & configuring the in-memory web api import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; -import { InMemoryDataService } from './in-memory-data.service'; +import { InMemoryDataService } from './in-memory-data.service'; + +import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { DashboardComponent } from './dashboard.component'; -import { HeroesComponent } from './heroes.component'; -import { HeroDetailComponent } from './hero-detail.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 { HeroService } from './hero.service'; -import { HeroSearchComponent } from './hero-search.component'; +import { MessageService } from './message.service'; +import { MessagesComponent } from './messages/messages.component'; -// #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: [ // #docregion browsermodule - BrowserModule.withServerTransition({ appId: 'uni' }), + BrowserModule.withServerTransition({ appId: 'tour-of-heroes' }), // #enddocregion browsermodule FormsModule, + AppRoutingModule, HttpClientModule, - HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService), - AppRoutingModule + HttpClientInMemoryWebApiModule.forRoot( + InMemoryDataService, { dataEncapsulation: false } + ) ], declarations: [ AppComponent, DashboardComponent, - HeroDetailComponent, HeroesComponent, + HeroDetailComponent, + MessagesComponent, HeroSearchComponent ], - providers: [ HeroService ], + providers: [ HeroService, MessageService ], bootstrap: [ AppComponent ] }) export class AppModule { - -// #enddocregion simple -// #docregion platform-detection -constructor( + // #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 platform-detection } -// #enddocregion simple diff --git a/aio/content/examples/universal/src/app/app.server.module.ts b/aio/content/examples/universal/src/app/app.server.module.ts new file mode 100644 index 0000000000..3d1c0fa9be --- /dev/null +++ b/aio/content/examples/universal/src/app/app.server.module.ts @@ -0,0 +1,19 @@ +import {NgModule} from '@angular/core'; +import {ServerModule} from '@angular/platform-server'; +import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader'; + +import {AppModule} from './app.module'; +import {AppComponent} from './app.component'; + +@NgModule({ + imports: [ + AppModule, + ServerModule, + ModuleMapLoaderModule + ], + providers: [ + // Add universal-only providers here + ], + bootstrap: [ AppComponent ], +}) +export class AppServerModule {} diff --git a/aio/content/examples/universal/src/app/dashboard.component.ts b/aio/content/examples/universal/src/app/dashboard.component.ts deleted file mode 100644 index 2f9cbeb3ec..0000000000 --- a/aio/content/examples/universal/src/app/dashboard.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -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' ] -}) -export class DashboardComponent implements OnInit { - heroes: Observable; - - constructor(private heroService: HeroService) { } - - ngOnInit(): void { - this.heroes = this.heroService.getHeroes() - .map(heroes => heroes.slice(1, 5)); - } -} diff --git a/aio/content/examples/universal/src/app/dashboard.component.css b/aio/content/examples/universal/src/app/dashboard/dashboard.component.css similarity index 95% rename from aio/content/examples/universal/src/app/dashboard.component.css rename to aio/content/examples/universal/src/app/dashboard/dashboard.component.css index 096cec7621..3822cc56bb 100644 --- a/aio/content/examples/universal/src/app/dashboard.component.css +++ b/aio/content/examples/universal/src/app/dashboard/dashboard.component.css @@ -1,3 +1,4 @@ +/* DashboardComponent's private CSS styles */ [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/dashboard.component.html similarity index 62% rename from aio/content/examples/universal/src/app/dashboard.component.html rename to aio/content/examples/universal/src/app/dashboard/dashboard.component.html index 8852696c84..36e86053a6 100644 --- a/aio/content/examples/universal/src/app/dashboard.component.html +++ b/aio/content/examples/universal/src/app/dashboard/dashboard.component.html @@ -1,9 +1,11 @@

Top Heroes

- +

{{hero.name}}

+ diff --git a/aio/content/examples/universal/src/app/dashboard/dashboard.component.spec.ts b/aio/content/examples/universal/src/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000000..fea6bfb4db --- /dev/null +++ b/aio/content/examples/universal/src/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DashboardComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/universal/src/app/dashboard/dashboard.component.ts b/aio/content/examples/universal/src/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000000..f152c10c7e --- /dev/null +++ b/aio/content/examples/universal/src/app/dashboard/dashboard.component.ts @@ -0,0 +1,23 @@ +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)); + } +} diff --git a/aio/content/examples/universal/src/app/hero-detail.component.html b/aio/content/examples/universal/src/app/hero-detail.component.html deleted file mode 100644 index af377615c3..0000000000 --- a/aio/content/examples/universal/src/app/hero-detail.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-

{{hero.name}} details!

-
- {{hero.id}}
-
- - -
- - -
diff --git a/aio/content/examples/universal/src/app/hero-detail.component.ts b/aio/content/examples/universal/src/app/hero-detail.component.ts deleted file mode 100644 index 6e6cdace49..0000000000 --- a/aio/content/examples/universal/src/app/hero-detail.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import 'rxjs/add/operator/switchMap'; -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, ParamMap } from '@angular/router'; -import { Location } from '@angular/common'; - -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -@Component({ - selector: 'my-hero-detail', - templateUrl: './hero-detail.component.html', - styleUrls: [ './hero-detail.component.css' ] -}) -export class HeroDetailComponent implements OnInit { - hero: Hero; - - constructor( - private heroService: HeroService, - private route: ActivatedRoute, - private location: Location - ) {} - - ngOnInit(): void { - this.route.paramMap - .switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id'))) - .subscribe(hero => this.hero = hero); - } - - save(): void { - this.heroService - .update(this.hero) - .subscribe(() => this.goBack()); - } - - goBack(): void { - this.location.back(); - } -} diff --git a/aio/content/examples/universal/src/app/hero-detail.component.css b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.css similarity index 90% rename from aio/content/examples/universal/src/app/hero-detail.component.css rename to aio/content/examples/universal/src/app/hero-detail/hero-detail.component.css index f6139ba274..062544af48 100644 --- a/aio/content/examples/universal/src/app/hero-detail.component.css +++ b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.css @@ -1,3 +1,4 @@ +/* HeroDetailComponent's private CSS styles */ label { display: inline-block; width: 3em; diff --git a/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.html b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.html new file mode 100644 index 0000000000..6fa498ee4f --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.html @@ -0,0 +1,11 @@ +
+

{{ hero.name | uppercase }} Details

+
id: {{hero.id}}
+
+ +
+ + +
diff --git a/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts new file mode 100644 index 0000000000..5745fb8677 --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-detail/hero-detail.component.ts @@ -0,0 +1,40 @@ +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 = +this.route.snapshot.paramMap.get('id'); + this.heroService.getHero(id) + .subscribe(hero => this.hero = hero); + } + + goBack(): void { + this.location.back(); + } + + save(): void { + this.heroService.updateHero(this.hero) + .subscribe(() => this.goBack()); + } +} diff --git a/aio/content/examples/universal/src/app/hero-search.component.css b/aio/content/examples/universal/src/app/hero-search.component.css deleted file mode 100644 index 85f7da408e..0000000000 --- a/aio/content/examples/universal/src/app/hero-search.component.css +++ /dev/null @@ -1,20 +0,0 @@ -.search-result{ - 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; -} - -.search-result:hover { - color: #eee; - background-color: #607D8B; -} - -#search-box{ - width: 200px; - height: 20px; -} diff --git a/aio/content/examples/universal/src/app/hero-search.component.html b/aio/content/examples/universal/src/app/hero-search.component.html deleted file mode 100644 index 43186da3b3..0000000000 --- a/aio/content/examples/universal/src/app/hero-search.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
-

Hero Search

- -
-
- {{hero.name}} -
-
-
diff --git a/aio/content/examples/universal/src/app/hero-search.component.ts b/aio/content/examples/universal/src/app/hero-search.component.ts deleted file mode 100644 index 465f73834c..0000000000 --- a/aio/content/examples/universal/src/app/hero-search.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; - -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; - -import 'rxjs/add/observable/of'; - -import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/distinctUntilChanged'; - -import { HeroSearchService } from './hero-search.service'; -import { Hero } from './hero'; - -@Component({ - selector: 'hero-search', - templateUrl: './hero-search.component.html', - styleUrls: [ './hero-search.component.css' ], - providers: [HeroSearchService] -}) -export class HeroSearchComponent implements OnInit { - heroes: Observable; - private searchTerms = new Subject(); - - constructor( - private heroSearchService: HeroSearchService, - private router: Router) {} - - // Push a search term into the observable stream. - search(term: string): void { - this.searchTerms.next(term); - } - - ngOnInit(): void { - 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 - // return the http search observable - ? this.heroSearchService.search(term) - // or the observable of empty heroes if there was no search term - : Observable.of([])) - .catch(error => { - // TODO: add real error handling - console.log(error); - return Observable.of([]); - }); - } - - gotoDetail(hero: Hero): void { - let link = ['/detail', hero.id]; - this.router.navigate(link); - } -} diff --git a/aio/content/examples/universal/src/app/hero-search.service.ts b/aio/content/examples/universal/src/app/hero-search.service.ts deleted file mode 100644 index 2843d4e3b1..0000000000 --- a/aio/content/examples/universal/src/app/hero-search.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 'rxjs/add/operator/map'; - -import { Hero } from './hero'; - -// #docregion class -@Injectable() -export class HeroSearchService { - - 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(this.searchUrl + term) - .map((data: any) => data.data as Hero[]); - } -} -// #enddocregion class diff --git a/aio/content/examples/universal/src/app/hero-search/hero-search.component.css b/aio/content/examples/universal/src/app/hero-search/hero-search.component.css new file mode 100644 index 0000000000..0f2c8cdd10 --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-search/hero-search.component.css @@ -0,0 +1,39 @@ +/* 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; +} diff --git a/aio/content/examples/universal/src/app/hero-search/hero-search.component.html b/aio/content/examples/universal/src/app/hero-search/hero-search.component.html new file mode 100644 index 0000000000..cd0c3ffb9b --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-search/hero-search.component.html @@ -0,0 +1,13 @@ +
+

Hero Search

+ + + + +
diff --git a/aio/content/examples/universal/src/app/hero-search/hero-search.component.spec.ts b/aio/content/examples/universal/src/app/hero-search/hero-search.component.spec.ts new file mode 100644 index 0000000000..901bb7f2ab --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-search/hero-search.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroSearchComponent } from './hero-search.component'; + +describe('HeroSearchComponent', () => { + let component: HeroSearchComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HeroSearchComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/universal/src/app/hero-search/hero-search.component.ts b/aio/content/examples/universal/src/app/hero-search/hero-search.component.ts new file mode 100644 index 0000000000..5ce425a446 --- /dev/null +++ b/aio/content/examples/universal/src/app/hero-search/hero-search.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { of } from 'rxjs/observable/of'; + +import { + debounceTime, distinctUntilChanged, switchMap + } from 'rxjs/operators'; + +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'hero-search', + templateUrl: './hero-search.component.html', + styleUrls: [ './hero-search.component.css' ] +}) +export class HeroSearchComponent implements OnInit { + heroes: Observable; + private searchTerms = new Subject(); + + 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)), + ); + } +} diff --git a/aio/content/examples/universal/src/app/hero.service.ts b/aio/content/examples/universal/src/app/hero.service.ts index 6dc05481e5..ba263e5636 100644 --- a/aio/content/examples/universal/src/app/hero.service.ts +++ b/aio/content/examples/universal/src/app/hero.service.ts @@ -1,15 +1,17 @@ import { Injectable, Inject, Optional } from '@angular/core'; import { APP_BASE_HREF } from '@angular/common'; -import { HttpHeaders, HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders }from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/map'; +import { of } from 'rxjs/observable/of'; +import { catchError, map, tap } from 'rxjs/operators'; import { Hero } from './hero'; +import { MessageService } from './message.service'; -const headers = new HttpHeaders({'Content-Type': 'application/json'}); +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }) +}; @Injectable() export class HeroService { @@ -19,46 +21,109 @@ export class HeroService { // #docregion ctor constructor( private http: HttpClient, + private messageService: MessageService, @Optional() @Inject(APP_BASE_HREF) origin: string) { - this.heroesUrl = (origin || '') + this.heroesUrl; - } + this.heroesUrl = `${origin}${this.heroesUrl}`; + } // #enddocregion ctor - getHeroes(): Observable { - return this.http.get(this.heroesUrl) - .map((data: any) => data.data as Hero[]) - .catch(this.handleError); + /** GET heroes from the server */ + getHeroes (): Observable { + return this.http.get(this.heroesUrl) + .pipe( + tap(heroes => this.log(`fetched heroes`)), + catchError(this.handleError('getHeroes', [])) + ); } + /** GET hero by id. Return `undefined` when id not found */ + getHeroNo404(id: number): Observable { + const url = `${this.heroesUrl}/?id=${id}`; + return this.http.get(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(`getHero id=${id}`)) + ); + } + + /** GET hero by id. Will 404 if id not found */ getHero(id: number): Observable { const url = `${this.heroesUrl}/${id}`; - return this.http.get(url) - .map((data: any) => data.data as Hero) - .catch(this.handleError); + return this.http.get(url).pipe( + tap(_ => this.log(`fetched hero id=${id}`)), + catchError(this.handleError(`getHero id=${id}`)) + ); } - delete(id: number): Observable { + /* GET heroes whose name contains search term */ + searchHeroes(term: string): Observable { + if (!term.trim()) { + // if not search term, return empty hero array. + return of([]); + } + return this.http.get(`api/heroes/?name=${term}`).pipe( + tap(_ => this.log(`found heroes matching "${term}"`)), + catchError(this.handleError('searchHeroes', [])) + ); + } + + //////// Save methods ////////// + + /** POST: add a new hero to the server */ + addHero (name: string): Observable { + const hero = { name }; + + return this.http.post(this.heroesUrl, hero, httpOptions).pipe( + tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)), + catchError(this.handleError('addHero')) + ); + } + + /** DELETE: delete the hero from the server */ + deleteHero (hero: Hero | number): Observable { + const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; - return this.http.delete(url, { headers }) - .catch(this.handleError); + + return this.http.delete(url, httpOptions).pipe( + tap(_ => this.log(`deleted hero id=${id}`)), + catchError(this.handleError('deleteHero')) + ); } - create(name: string): Observable { - return this.http - .post(this.heroesUrl, { name: name }, { headers }) - .map((data: any) => data.data) - .catch(this.handleError); + /** PUT: update the hero on the server */ + updateHero (hero: Hero): Observable { + return this.http.put(this.heroesUrl, hero, httpOptions).pipe( + tap(_ => this.log(`updated hero id=${hero.id}`)), + catchError(this.handleError('updateHero')) + ); } - update(hero: Hero): Observable { - const url = `${this.heroesUrl}/${hero.id}`; - return this.http - .put(url, hero, { headers }) - .catch(this.handleError); + /** + * 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 (operation = 'operation', result?: T) { + return (error: any): Observable => { + + // 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); + } } - private handleError(error: any): Observable { - console.error('An error occurred', error); // for demo purposes only - throw error; + /** Log a HeroService message with the MessageService */ + private log(message: string) { + this.messageService.add('HeroService: ' + message); } } diff --git a/aio/content/examples/universal/src/app/heroes.component.html b/aio/content/examples/universal/src/app/heroes.component.html deleted file mode 100644 index 392d241d52..0000000000 --- a/aio/content/examples/universal/src/app/heroes.component.html +++ /dev/null @@ -1,29 +0,0 @@ - -

My Heroes

- -
- - -
- -
    - -
  • - {{hero.id}} - {{hero.name}} - - - -
  • - -
-
-

- {{selectedHero.name | uppercase}} is my hero -

- -
diff --git a/aio/content/examples/universal/src/app/heroes.component.ts b/aio/content/examples/universal/src/app/heroes.component.ts deleted file mode 100644 index acd2dde1d2..0000000000 --- a/aio/content/examples/universal/src/app/heroes.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -// #docregion -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; - -import { Hero } from './hero'; -import { HeroService } from './hero.service'; - -@Component({ - selector: 'my-heroes', - templateUrl: './heroes.component.html', - styleUrls: [ './heroes.component.css' ] -}) -export class HeroesComponent implements OnInit { - heroes: Hero[]; - selectedHero: Hero; - - constructor( - private heroService: HeroService, - private router: Router) { } - - getHeroes(): void { - this.heroService - .getHeroes() - .subscribe(heroes => this.heroes = heroes); - } - - add(name: string): void { - name = name.trim(); - if (!name) { return; } - this.heroService.create(name) - .subscribe(hero => { - this.heroes.push(hero); - this.selectedHero = null; - }); - } - - delete(hero: Hero): void { - this.heroService - .delete(hero.id) - .subscribe(() => { - this.heroes = this.heroes.filter(h => h !== hero); - if (this.selectedHero === hero) { this.selectedHero = null; } - }); - } - - ngOnInit(): void { - this.getHeroes(); - } - - onSelect(hero: Hero): void { - this.selectedHero = hero; - } - - gotoDetail(): void { - this.router.navigate(['/detail', this.selectedHero.id]); - } -} diff --git a/aio/content/examples/universal/src/app/heroes.component.css b/aio/content/examples/universal/src/app/heroes/heroes.component.css similarity index 74% rename from aio/content/examples/universal/src/app/heroes.component.css rename to aio/content/examples/universal/src/app/heroes/heroes.component.css index d2c958a911..b51469d1b6 100644 --- a/aio/content/examples/universal/src/app/heroes.component.css +++ b/aio/content/examples/universal/src/app/heroes/heroes.component.css @@ -1,8 +1,4 @@ -/* #docregion */ -.selected { - background-color: #CFD8DC !important; - color: white; -} +/* HeroesComponent's private CSS styles */ .heroes { margin: 0 0 2em 0; list-style-type: none; @@ -10,28 +6,33 @@ width: 15em; } .heroes li { - cursor: pointer; position: relative; - left: 0; + 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 li.selected:hover { - background-color: #BBD8DC !important; - color: white; -} -.heroes .text { + +.heroes a { + color: #888; + text-decoration: none; position: relative; - top: -3px; + display: block; + width: 250px; } + +.heroes a:hover { + color:#607D8B; +} + .heroes .badge { display: inline-block; font-size: small; @@ -43,26 +44,31 @@ left: -1px; top: -4px; height: 1.8em; + min-width: 16px; + text-align: right; margin-right: .8em; border-radius: 4px 0 0 4px; } -button { - font-family: Arial; + +.button { background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; + font-family: Arial; } + button:hover { background-color: #cfd8dc; } -/* #docregion additions */ + button.delete { - float:right; - margin-top: 2px; - margin-right: .8em; + position: relative; + left: 194px; + top: -32px; background-color: gray !important; - color:white; + color: white; } + diff --git a/aio/content/examples/universal/src/app/heroes/heroes.component.html b/aio/content/examples/universal/src/app/heroes/heroes.component.html new file mode 100644 index 0000000000..8ab944cba0 --- /dev/null +++ b/aio/content/examples/universal/src/app/heroes/heroes.component.html @@ -0,0 +1,21 @@ +

My Heroes

+ +
+ + + +
+ + diff --git a/aio/content/examples/universal/src/app/heroes/heroes.component.spec.ts b/aio/content/examples/universal/src/app/heroes/heroes.component.spec.ts new file mode 100644 index 0000000000..7ec3956eac --- /dev/null +++ b/aio/content/examples/universal/src/app/heroes/heroes.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroesComponent } from './heroes.component'; + +describe('HeroesComponent', () => { + let component: HeroesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HeroesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/aio/content/examples/universal/src/app/heroes/heroes.component.ts b/aio/content/examples/universal/src/app/heroes/heroes.component.ts new file mode 100644 index 0000000000..0c99c98e4e --- /dev/null +++ b/aio/content/examples/universal/src/app/heroes/heroes.component.ts @@ -0,0 +1,41 @@ +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) + .subscribe(hero => { + this.heroes.push(hero); + }); + } + + delete(hero: Hero): void { + this.heroService.deleteHero(hero) + .subscribe(() => { + this.heroes = this.heroes.filter(h => h !== hero); + }); + } + +} diff --git a/aio/content/examples/universal/src/app/in-memory-data.service.ts b/aio/content/examples/universal/src/app/in-memory-data.service.ts index c915955e22..ff1f190f83 100644 --- a/aio/content/examples/universal/src/app/in-memory-data.service.ts +++ b/aio/content/examples/universal/src/app/in-memory-data.service.ts @@ -1,18 +1,18 @@ -// #docregion , init import { InMemoryDbService } from 'angular-in-memory-web-api'; + export class InMemoryDataService implements InMemoryDbService { createDb() { - let heroes = [ - {id: 11, name: 'Mr. Nice'}, - {id: 12, name: 'Narco'}, - {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'} + const heroes = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { 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}; } diff --git a/aio/content/examples/universal/src/app/message.service.spec.ts b/aio/content/examples/universal/src/app/message.service.spec.ts new file mode 100644 index 0000000000..63ecfd8ff6 --- /dev/null +++ b/aio/content/examples/universal/src/app/message.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MessageService] + }); + }); + + it('should be created', inject([MessageService], (service: MessageService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/aio/content/examples/universal/src/app/message.service.ts b/aio/content/examples/universal/src/app/message.service.ts new file mode 100644 index 0000000000..9b3a2cac05 --- /dev/null +++ b/aio/content/examples/universal/src/app/message.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MessageService { + messages: string[] = []; + + add(message: string) { + this.messages.push(message); + } + + clear() { + this.messages.length = 0; + } +} diff --git a/aio/content/examples/universal/src/app/messages/messages.component.css b/aio/content/examples/universal/src/app/messages/messages.component.css new file mode 100644 index 0000000000..d08d9be581 --- /dev/null +++ b/aio/content/examples/universal/src/app/messages/messages.component.css @@ -0,0 +1,35 @@ +/* MessagesComponent's private CSS styles */ +h2 { + color: red; + font-family: Arial, Helvetica, sans-serif; + font-weight: lighter; +} +body { + margin: 2em; +} +body, input[text], button { + color: crimson; + font-family: Cambria, Georgia; +} + +button.clear { + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; +} +button:hover { + background-color: #cfd8dc; +} +button:disabled { + background-color: #eee; + color: #aaa; + cursor: auto; +} +button.clear { + color: #888; + margin-bottom: 12px; +} diff --git a/aio/content/examples/universal/src/app/messages/messages.component.html b/aio/content/examples/universal/src/app/messages/messages.component.html new file mode 100644 index 0000000000..1df7dfd989 --- /dev/null +++ b/aio/content/examples/universal/src/app/messages/messages.component.html @@ -0,0 +1,8 @@ +
+ +

Messages

+ +
{{message}}
+ +
diff --git a/aio/content/examples/universal/src/app/messages/messages.component.spec.ts b/aio/content/examples/universal/src/app/messages/messages.component.spec.ts new file mode 100644 index 0000000000..3c2b2b1537 --- /dev/null +++ b/aio/content/examples/universal/src/app/messages/messages.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessagesComponent } from './messages.component'; + +describe('MessagesComponent', () => { + let component: MessagesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MessagesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/aio/content/examples/universal/src/app/messages/messages.component.ts b/aio/content/examples/universal/src/app/messages/messages.component.ts new file mode 100644 index 0000000000..17c8b2701f --- /dev/null +++ b/aio/content/examples/universal/src/app/messages/messages.component.ts @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { MessageService } from '../message.service'; + +@Component({ + selector: 'app-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.css'] +}) +export class MessagesComponent implements OnInit { + + constructor(public messageService: MessageService) {} + + ngOnInit() { + } + +} diff --git a/aio/content/examples/universal/src/app/mock-heroes.ts b/aio/content/examples/universal/src/app/mock-heroes.ts new file mode 100644 index 0000000000..e84c2fd2b0 --- /dev/null +++ b/aio/content/examples/universal/src/app/mock-heroes.ts @@ -0,0 +1,14 @@ +import { Hero } from './hero'; + +export const HEROES: Hero[] = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { 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' } +]; diff --git a/aio/content/examples/universal/src/index-universal.html b/aio/content/examples/universal/src/index-universal.html deleted file mode 100644 index 7938536bcc..0000000000 --- a/aio/content/examples/universal/src/index-universal.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Angular Universal Tour of Heroes - - - - - - - - - - - Loading... - - - - - diff --git a/aio/content/examples/universal/src/index.html b/aio/content/examples/universal/src/index.html index 2ce59e8d44..1c4106381f 100644 --- a/aio/content/examples/universal/src/index.html +++ b/aio/content/examples/universal/src/index.html @@ -1,27 +1,14 @@ - - - - - - Angular Universal Tour of Heroes - - - - + + + + + Tour of Heroes + - - - - - - - - - - - Loading... - + + + + + + diff --git a/aio/content/examples/universal/src/main.server.ts b/aio/content/examples/universal/src/main.server.ts new file mode 100644 index 0000000000..d7c01cde7b --- /dev/null +++ b/aio/content/examples/universal/src/main.server.ts @@ -0,0 +1 @@ +export { AppServerModule } from './app/app.server.module'; diff --git a/aio/content/examples/universal/src/main.ts b/aio/content/examples/universal/src/main.ts index f332d1d245..a9ca1caf8c 100644 --- a/aio/content/examples/universal/src/main.ts +++ b/aio/content/examples/universal/src/main.ts @@ -1,6 +1,11 @@ -// #docregion +import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/aio/content/examples/universal/src/tsconfig.server.json b/aio/content/examples/universal/src/tsconfig.server.json new file mode 100644 index 0000000000..4401f4ca65 --- /dev/null +++ b/aio/content/examples/universal/src/tsconfig.server.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "baseUrl": "./", + "module": "commonjs", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ], + "angularCompilerOptions": { + "entryModule": "app/app.server.module#AppServerModule" + } +} diff --git a/aio/content/examples/universal/src/universal/app-server.module.ts b/aio/content/examples/universal/src/universal/app-server.module.ts deleted file mode 100644 index 1ae883e2ed..0000000000 --- a/aio/content/examples/universal/src/universal/app-server.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -// #docregion -import { NgModule } from '@angular/core'; -import { ServerModule } from '@angular/platform-server'; -import { AppComponent } from '../app/app.component'; -import { AppModule } from '../app/app.module'; - -@NgModule({ - imports: [ - AppModule, - ServerModule, - ], - providers: [ - // Add universal-only providers here - ], - bootstrap: [ - AppComponent - ] -}) -export class AppServerModule {} diff --git a/aio/content/examples/universal/src/universal/server.ts b/aio/content/examples/universal/src/universal/server.ts deleted file mode 100644 index f6248c11ac..0000000000 --- a/aio/content/examples/universal/src/universal/server.ts +++ /dev/null @@ -1,68 +0,0 @@ -// 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 webpack plug-in, -// exists in-memory during build. -// It is not available in the file system 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 in 'dist/' by default -server.set('views', 'dist'); -// #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-universal.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 dist - const root = 'dist'; - - 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 deleted file mode 100644 index 8f0e506f7c..0000000000 --- a/aio/content/examples/universal/src/universal/universal-engine.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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 - -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; - - let template = templateCache[filePath]; - if (!template) { - template = fs.readFileSync(filePath).toString(); - templateCache[filePath] = template; - } - - const { appModuleFactory } = setupOptions; - const origin = getOrigin(req); - - // #docregion render - // render the page - renderModuleFactory(appModuleFactory, { - document: template, - url: routeUrl, - extraProviders: [ - { provide: APP_BASE_HREF, useValue: origin } - ] - }) - .then(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.client.json b/aio/content/examples/universal/tsconfig.client.json deleted file mode 100644 index 3f4458f02f..0000000000 --- a/aio/content/examples/universal/tsconfig.client.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.universal.json", - - "files": [ - "src/main.ts" - ], - - "angularCompilerOptions": { - "genDir": "aot", - "entryModule": "./src/app/app.module#AppModule", - "skipMetadataEmit" : true - } -} diff --git a/aio/content/examples/universal/tsconfig.universal.json b/aio/content/examples/universal/tsconfig.universal.json deleted file mode 100644 index 4bbda5e956..0000000000 --- a/aio/content/examples/universal/tsconfig.universal.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/universal/app-server.module.ts", - "src/universal/server.ts" - ], - - "angularCompilerOptions": { - "genDir": "aot", - "entryModule": "./src/app/app.module#AppModule", - "skipMetadataEmit" : true - } -} diff --git a/aio/content/examples/universal/webpack.config.client.js b/aio/content/examples/universal/webpack.config.client.js deleted file mode 100644 index 88c352359e..0000000000 --- a/aio/content/examples/universal/webpack.config.client.js +++ /dev/null @@ -1,34 +0,0 @@ -// #docregion -const ngtools = require('@ngtools/webpack'); -const webpack = require('webpack'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin') - -module.exports = { - devtool: 'source-map', - entry: { - main: [ './src/main.ts' ] - }, - resolve: { - extensions: ['.ts', '.js'] - }, - output: { - path: __dirname + '/dist', - filename: 'client.js' - }, - plugins: [ - // compile with AOT - new ngtools.AotPlugin({ - tsConfigPath: './tsconfig.client.json' - }), - - // minify - new UglifyJSPlugin() - ], - module: { - rules: [ - { test: /\.css$/, loader: 'raw-loader' }, - { test: /\.html$/, loader: 'raw-loader' }, - { test: /\.ts$/, loader: '@ngtools/webpack' } - ] - } -} diff --git a/aio/content/examples/universal/webpack.config.universal.js b/aio/content/examples/universal/webpack.config.universal.js deleted file mode 100644 index b74c71904c..0000000000 --- a/aio/content/examples/universal/webpack.config.universal.js +++ /dev/null @@ -1,43 +0,0 @@ -// #docregion -const ngtools = require('@ngtools/webpack'); -const webpack = require('webpack'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); - -module.exports = { - devtool: 'source-map', - entry: { - main: [ - './src/universal/app-server.module.ts', - './src/universal/server.ts' - ] - }, - resolve: { - extensions: ['.ts', '.js'] - }, - target: 'node', - output: { - path: __dirname + '/dist', - filename: 'server.js' - }, - plugins: [ - // compile with AOT - new ngtools.AotPlugin({ - tsConfigPath: './tsconfig.universal.json' - }), - - // copy assets to the output (/dist) folder - new CopyWebpackPlugin([ - {from: 'src/index-universal.html'}, - {from: 'src/styles.css'}, - {from: 'node_modules/core-js/client/shim.min.js'}, - {from: 'node_modules/zone.js/dist/zone.min.js'}, - ]) - ], - module: { - rules: [ - { test: /\.css$/, loader: 'raw-loader' }, - { test: /\.html$/, loader: 'raw-loader' }, - { test: /\.ts$/, loader: '@ngtools/webpack' } - ] - } -} diff --git a/aio/content/examples/universal/webpack.server.config.js b/aio/content/examples/universal/webpack.server.config.js new file mode 100644 index 0000000000..ac9e51225b --- /dev/null +++ b/aio/content/examples/universal/webpack.server.config.js @@ -0,0 +1,31 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: { server: './server.ts' }, + resolve: { extensions: ['.js', '.ts'] }, + target: 'node', + // this makes sure we include node_modules and other 3rd party libraries + externals: [/(node_modules|main\..*\.js)/], + output: { + path: path.join(__dirname, 'dist'), + filename: '[name].js' + }, + module: { + rules: [{ test: /\.ts$/, loader: 'ts-loader' }] + }, + plugins: [ + // Temporary Fix for issue: https://github.com/angular/angular/issues/11580 + // for 'WARNING Critical dependency: the request of a dependency is an expression' + new webpack.ContextReplacementPlugin( + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + {} + ) + ] +}; diff --git a/aio/content/guide/universal.md b/aio/content/guide/universal.md index 7482570f79..3fecd8c9a8 100644 --- a/aio/content/guide/universal.md +++ b/aio/content/guide/universal.md @@ -2,14 +2,6 @@ This guide describes **Angular Universal**, a technology that runs your Angular application on the server. -
- -This is a **preview guide**. -The Angular CLI is adding support for universal apps and -we will realign this guide with the CLI as soon as possible. - -
- 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_ @@ -19,7 +11,7 @@ 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. 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. +Meanwhile, the browser downloads the full client version and switches to it automatically after the code loads.
@@ -35,21 +27,21 @@ which runs in a [node express](https://expressjs.com/) server. 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. 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. +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. +Angular Universal can generate a static version of your app that is easily 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 +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} @@ -58,14 +50,14 @@ Enabling web crawlers is often referred to as 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 +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. +Displaying the first page quickly can be critical for user engagement. [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 may have to launch faster to engage these users before they decide to do something else. @@ -89,10 +81,10 @@ You compile the client application with the `platform-server` module instead of 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. +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, +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. @@ -109,11 +101,11 @@ Because a Universal `platform-server` app doesn't execute in the browser, you ma 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`; +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. +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). @@ -129,45 +121,35 @@ The _Tour of Heroes_ tutorial is the foundation for the Universal sample describ 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 +In this example, the Angular CLI compiles and bundles 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 entry point for the server-side, `main.server.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` + * a TypeScript config file, `tsconfig.server.json` + * a Webpack config file for the server, `webpack.server.config.js` When you're done, the folder structure will look like this: -src/ +src/ index.html app web page - index-universal.html * universal app web page template main.ts bootstrapper for client app + main.server.ts * bootstrapper for server app + tsconfig.app.json TypeScript client configuration + tsconfig.server.json * TypeScript server configuration + tsconfig.spec.json TypeScript spec configuration 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/ * Post-build files - client.js * AOT-compiled client bundle - server.js * express server & universal app bundle - index-universal.html * copy of the app web page template - ... * copies of other asset files - 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 + app.server.module.ts * server-side application module +server.ts * express web server +tsconfig.json TypeScript client configuration package.json npm configuration -tsconfig.client.json * TypeScript client AOT configuration -tsconfig.universal.json * TypeScript Universal configuration -webpack.config.aot.js * Webpack client AOT configuration -webpack.config.universal.js * Webpack Universal configuration +webpack.server.config.js * Webpack server configuration The files marked with `*` are new and not in the original tutorial sample. @@ -177,26 +159,23 @@ This guide covers them in the sections below. ## Preparation +Download the [Tour of Heroes](generated/zips/toh-pt6/toh-pt6.zip) project and install the dependencies from it. + {@a install-the-tools} ### Install the tools -To get started, install these Universal and Webpack packages. +To get started, install these 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. - * `copy-webpack-plugin` - Webpack plugin to copy asset files to the output folder. - * `raw-loader` - Webpack loader for text files. - * `express` - node web server. - * `@types/express` - TypeScript type definitions for express. + * `@nguniversal/module-map-ngfactory-loader` - For handling lazy-loading in the context of a server-render. + * `@nguniversal/express-engine` - An express engine for Universal applications. + * `ts-loader` - To transpile the server application Install them with the following commands: -npm install @angular/compiler-cli @angular/platform-server express --save -npm install webpack @ngtools/webpack copy-webpack-plugin raw-loader @types/express --save-dev +npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine {@a transition} @@ -219,7 +198,7 @@ Replace that import with this one: -Angular adds the `appId` value (which can be _any_ string) to the style-names of the server-rendered pages, +Angular adds the `appId` value (which can be _any_ string) to the style-names of the server-rendered pages, so that they can be identified and removed when the client app starts. You can get runtime information about the current platform and the `appId` by injection. @@ -234,7 +213,7 @@ You can get runtime information about the current platform and the `appId` by in The tutorial's `HeroService` and `HeroSearchService` delegate to the Angular `Http` module to fetch application data. These services send requests to _relative_ URLs such as `api/heroes`. -In a Universal app, `Http` URLs must be _absolute_ (e.g., `https://my-server.com/api/heroes`) +In a Universal app, `Http` URLs must be _absolute_ (e.g., `https://my-server.com/api/heroes`) even when the Universal web server is capable of handling those requests. You'll have to change the services to make requests with absolute URLs when running on the server @@ -254,55 +233,17 @@ You don't provide `APP_BASE_HREF` in the browser version, so the `heroesUrl` rem
-You can ignore `APP_BASE_HREF` in the browser if you've specified `` in the `index.html` +You can ignore `APP_BASE_HREF` in the browser if you've specified `` in the `index.html` to satisfy the router's need for a base address, as the tutorial sample does.
-You will provide the `APP_BASE_HREF` in the universal version of the app (see how [below](#provide-origin)), -so the `heroesUrl` becomes absolute. - -Do the same thing to the `HttpSearchService` constructor. -You'll have to adjust the `http.get` call in the `search()` method as well. -Here's the revised class. - - - - -{@a build-client-app} - -#### Try locally first - -Open a terminal window and confirm that the client app still works in the browser. - - -npm start - - -When you are done, shut down the server with `ctrl-C`. - -
- -If you get a "Cannot find module" error, see the explanation and resolution [below](#cannot-find-module) - -
- -
- {@a server-code} ## Server code To run an Angular Universal application, you'll need a server that accepts client requests and returns rendered pages. -Create a `universal/` folder as a sibling to the `app/` folder. - -Add to it the following three universal parts: - - 1. the [app server module](#app-server-module) - 2. the [Universal engine](#universal-engine) - 3. the [web server](#web-server) - {@a app-server-module} ### App server module @@ -310,126 +251,90 @@ Add to it the following three universal parts: The app server module class (conventionally named `AppServerModule`) is an Angular module that wraps the application's root module (`AppModule`) so that Universal can mediate between your application and the server. `AppServerModule` also tells Angular how to bootstrap your application when running as a Universal app. -Create an `app-server.module.ts` file in the `src/universal` directory with the following `AppServerModule` code: +Create an `app.server.module.ts` file in the `src/app/` directory with the following `AppServerModule` code: - + -Notice that it imports first the client app's `AppModule` and then Angular Universal's `ServerModule`. +Notice that it imports first the client app's `AppModule`, the Angular Universal's `ServerModule` and the `ModuleMapLoaderModule`. + +The `ModuleMapLoaderModule` is a server-side module that allows lazy-loading of routes. This is also the place to register providers that are specific to running your app under Universal. -{@a universal-engine} - -### Universal template engine - -The Universal `renderModuleFactory` function turns a client's requests into server-rendered HTML pages. -You'll call that function within a _template engine_ that's appropriate for your server stack. - -This guide's sample is written for [Node Express](https://expressjs.com/) -so the engine takes the form of [Express template engine middleware](https://expressjs.com/en/guide/using-template-engines.html). - -Create a `universal-engine.ts` file in the `src/universal` directory with the following code. - - - - -{@a render-module-factory} - -#### Rendering the page -The call to Universal's `renderModuleFactory` is where the rendering magic happens. - - - - -The first parameter is the `AppServerModule` that you wrote [earlier](#app-server-module). -It's the bridge between the Universal server-side renderer and your application. - -The second parameter is an options object - -* `document` is the template for the page to render (typically `index.html`). - - -* `url` is the application route (e.g., `/dashboard`), extracted from the client's request. -Universal should render the appropriate page for that route. - - -* `extraProviders` are optional Angular dependency injection providers, applicable when running on this server - -{@a provide-origin} - -You supply `extraProviders` when your app needs information that can only be determined by the currently running server instance. - -The required information in this case is the running server's origin, provided under the `APP_BASE_HREF` token, so that the app can [calculate absolute HTTP URLs](#http-urls). - -The `renderModuleFactory` function returns a _promise_ that resolves to the rendered page. - -It's up to your engine to decide what to do with that page. -_This engine's_ promise callback returns the rendered page to the [web server](#web-server), -which then forwards it to the client in the HTTP response. - {@a web-server} ### Universal web server A _Universal_ web server responds to application _page_ requests with static HTML rendered by the [Universal template engine](#universal-engine). -It receives and responds to HTTP requests from clients (usually browsers). -It serves static assets such as scripts, css, and images. +It receives and responds to HTTP requests from clients (usually browsers). +It serves static assets such as scripts, css, and images. It may respond to data requests, perhaps 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.
-_Any_ web server technology can serve a Universal app as long as it can call Universal's `renderModuleFactory`. -The principles and decision points discussed below apply to any web server technology that you chose. + _Any_ web server technology can serve a Universal app as long as it can call Universal's `renderModuleFactory`. + The principles and decision points discussed below apply to any web server technology that you chose.
-Create a `server.ts` file in the `src/universal` directory and add the following code: +Create a `server.ts` file in the root directory and add the following code: - +
-**This sample server is not secure!** -Be sure to add middleware to authenticate and authorize users -just as you would for a normal Angular application server. + **This sample server is not secure!** + Be sure to add middleware to authenticate and authorize users + just as you would for a normal Angular application server.
-{@a import-app-server-module-factory} +{@a universal-engine} +#### Universal template engine -#### Import AppServerModule factory +The important bit in this file is the `ngExpressEngine` function: -Most of this server code is re-usable across many applications. -The import of the `AppServerModule` couples it specifically to a single application. - - + -Your code editor may tell you that this import is incorrect. -It refers to the source file for the `AppServerModule` factory which doesn't exist at design time. +The `ngExpressEngine` is a wrapper around the universal's `renderModuleFactory` function that turns a client's requests into server-rendered HTML pages. +You'll call that function within a _template engine_ that's appropriate for your server stack. -That file _will exist_, briefly, during compilation. But it's never physically in the file system when you're editing `server.ts` and you must tell the compiler to generate this module factory file _before_ it compiles `server.ts`. -[Learn how below](#universal-typescript-configuration). +The first parameter is the `AppServerModule` that you wrote [earlier](#app-server-module). +It's the bridge between the Universal server-side renderer and your application. -#### Add the Universal template engine +The second parameter is the `extraProviders`. It is an optional Angular dependency injection providers, applicable when running on this server. -Express supports template engines such as the [Universal template engine](#universal-engine) you wrote earlier. -You import that engine and register it with Express like this: +{@a provide-origin} - - +You supply `extraProviders` when your app needs information that can only be determined by the currently running server instance. + +The required information in this case is the running server's origin, provided under the `APP_BASE_HREF` token, so that the app can [calculate absolute HTTP URLs](#http-urls). + +The `ngExpressEngine` function returns a _promise_ that resolves to the rendered page. + +It's up to your engine to decide what to do with that page. +_This engine's_ promise callback returns the rendered page to the [web server](#web-server), +which then forwards it to the client in the HTTP response. + +
+ + This wrappers are very useful to hide the complexity of the `renderModuleFactory`. There are more wrappers for different backend technologies + at the [Universal repository](https://github.com/angular/universal). + +
#### Filter request URLs 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`. +The browser could ask for one of the application routes such as `/dashboard`, `/heroes`, or `/detail:12`. In fact, if the app were _only_ rendered by the server, _every_ app link clicked would arrive at the server as a navigation URL intended for the router. @@ -447,9 +352,9 @@ So we can easily recognize the three types of requests and handle them different An Express server is a pipeline of middleware that filters and processes URL requests one after the other. -You configure the Express server pipeline with calls to `server.get()` like this one for data requests. +You configure the Express server pipeline with calls to `app.get()` like this one for data requests. - +
@@ -478,12 +383,12 @@ If your server handles HTTP requests, you'll have to add your own security plumb The following code filters for request URLs with no extensions and treats them as navigation requests. - + #### Serve static files safely -A single `server.use()` treats all other URLs as requests for static assets +A single `app.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, you will [put all client-facing asset files in the `/dist` folder](#universal-webpack-configuration) @@ -491,118 +396,42 @@ and will only honor requests for files from the `/dist` folder. The following express code routes all remaining requests to `/dist`; it returns a `404 - NOT FOUND` if the file is not found. - + {@a universal-configuration} ## Configure for Universal -The server application requires its own web page and its own build configuration. - -{@a index-universal} - -### Universal web page - -The universal app renders pages based on a host web page template. -Simple universal apps make do with a slightly modified copy of the original `index.html`. - -
- -If you build a production version of the client app with the CLI's `ng build --prod` command, you do not need a separate universal `index.html`. -The CLI constructs a suitable `index.html` for you. You can skip this subsection and continue to [universal TypeScript configuration](#universal-typescript-configuration). - -Read on if you're building the app without the CLI. - -
- -Create an `index-universal.html` as follows, shown next to the development `index.html` for comparison. - - - - - - - - - - - -The differences are few. - -* Load the minified versions of the `shim` and `zone` polyfills from the root (which will be `/dist`) - -* You won't use SystemJS for universal nor to load the client app. - -* Instead you'll load the [production version of the client app](#build-client), `client.js`, which is the result of AOT compilation, minification, and bundling. - -That's it for `index-universal.html`. -Next you'll create two universal configuration files, one for TypeScript and one for Webpack. +The server application requires its own build configuration. {@a universal-typescript-configuration} ### Universal TypeScript configuration -Create a `tsconfig.universal.json` file in the project root directory to configure TypeScript and AOT compilation of the universal app. +Create a `tsconfig.server.json` file in the project root directory to configure TypeScript and AOT compilation of the universal app. - + -Certain settings are noteworthy for their difference from the `tsconfig.json` in the `src/` folder. - -* The `module` property must be **es2015** because - the transpiled JavaScript will use `import` statements instead of `require()` calls. - - -* Point `"typeRoots"` to `"./node_modules/@types/"` - - -* Set the `files` property (instead of `exclude`) to compile the `app-server.module` before the `universal-engine`, - for the reason [explained above](#import-app-server-module-factory). +This config extends from the root's `tsconfig.json` file. Certain settings are noteworthy for their differences. +* The `module` property must be **commonjs** which can be require()'d into our server application. * The `angularCompilerOptions` section guides the AOT compiler: - - * `genDir` - the temporary output directory for AOT compiled code. - * `entryModule` - the root module of the client application, expressed as `path/to/file#ClassName`. - * `skipMetadataEmit` - set `true` because you don't need metadata in the bundled application. + * `entryModule` - the root module of the server application, expressed as `path/to/file#ClassName`. ### Universal Webpack configuration -Create a `webpack.config.universal.js` file in the project root directory with the following code. +Universal applications doesn't need any extra Webpack configuration, the CLI takes care of that for you, +but since the server is a typescript application, you will use Webpack to transpile it. - +Create a `webpack.server.config.js` file in the project root directory with the following code. + + **Webpack configuration** is a rich topic beyond the scope of this guide. -A few observations may clarify some of the choices. - -* Webpack walks the dependency graph from the two entry points to find all necessary universal application files. - - -* The `@ngtools/webpack` loader loads and prepares the TypeScript files for compilation. - - -* The `AotPlugin` runs the AOT compiler (`ngc`) over the prepared TypeScript, guided by the `tsconfig.universal.json` you created [above](#universal-typescript-configuration). - - -* The `raw-loader` loads imported CSS and HTML files as strings. -You may need additional loaders or configuration for other file types. - - -* The compiled output is bundled into `dist/server.js`. - - -* The `CopyWebpackPlugin` copies specific static files from their source locations into the `/dist` folder. -These files include the universal app's web page template, `index-universal.html`, -and the JavaScript and CSS files mentioned in it -... with the notable exception of `client.js` [to be discussed below](#build-client). - -
- -The `CopyWebpackPlugin` step is unnecessary if you [build the client](#build-client) with the CLI. - -
## Build and run with universal @@ -613,8 +442,10 @@ First add the _build_ and _serve_ commands to the `scripts` section of the `pack "scripts": { ... - "build:uni": "webpack --config webpack.config.universal.js", - "serve:uni": "node dist/server.js", + "build:universal": "npm run build:client-and-server-bundles && npm run webpack:server", + "serve:universal": "node dist/server.js", + "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false", + "webpack:server": "webpack --config webpack.server.config.js --progress --colors" ... } @@ -626,32 +457,30 @@ First add the _build_ and _serve_ commands to the `scripts` section of the `pack From the command prompt, type -npm run build:uni +npm run build:universal -Webpack compiles and bundles the universal app into a single output file, `dist/server.js`, per the [configuration above](#universal-configuration). -It also generates a [source map](https://webpack.js.org/configuration/devtool/), `dist/server.js.map` that correlates the bundle code to the source code. - -Source maps are primarily for the browser's [dev tools](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps), but on the server they help locate compilation errors in your components. +The Angular CLI compiles and bundles the universal app into two different folders, `browser` and `server`. +Webpack transpiles the `server.ts` file into Javascript. {@a serve} #### Serve -After building the server bundle, start the server. +After building the application, start the server. -npm run serve:uni +npm run serve:universal -The console window should say +The console window should say -listening on port 3200... +Node server listening on http://localhost:4000 ## Universal in action -Open a browser to http://localhost:3200/. +Open a browser to http://localhost:4000/. You should see the familiar Tour of Heroes dashboard page. Navigation via `routerLinks` works correctly. @@ -668,150 +497,24 @@ But clicks, mouse-moves, and keyboard entries are inert. User events other than `routerLink` clicks aren't supported. The user must wait for the full client app to arrive. -It will never arrive until you compile the client app +It will never arrive until you compile the client app and move the output into the `dist/` folder, a step you'll take in just a moment. -#### Review the console log - -Open the browser's development tools. -In the console window you should see output like the following: - - -listening on port 3200... -Running in the browser with appId=uni -/styles.css -/shim.min.js -/zone.min.js -/client.js -Error: ENOENT: no such file or directory, stat '... dist/client.js' ... - - -Most of the console log lines report requests for static files coming from the `` and `