docs: add universal guide (#18707)
- based on original effort in PR 17573 PR Close #18707
This commit is contained in:
committed by
Chuck Jazdzewski
parent
963a4d0dc8
commit
0b0d25fa33
@@ -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
|
||||
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* #docregion */
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
color: #999;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// #docplaster
|
||||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* #docregion */
|
||||
[class*='col-'] {
|
||||
float: left;
|
||||
padding-right: 20px;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<!-- #docregion -->
|
||||
<h3>Top Heroes</h3>
|
||||
<div class="grid grid-pad">
|
||||
<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">
|
||||
<a *ngFor="let hero of heroes | async" [routerLink]="['/detail', hero.id]" class="col-1-4">
|
||||
<div class="module hero">
|
||||
<h4>{{hero.name}}</h4>
|
||||
</div>
|
||||
|
||||
@@ -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<Hero[]>;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- #docregion -->
|
||||
<div *ngIf="hero">
|
||||
<h2>{{hero.name}} details!</h2>
|
||||
<div>
|
||||
@@ -8,7 +7,5 @@
|
||||
<input [(ngModel)]="hero.name" placeholder="name" />
|
||||
</div>
|
||||
<button (click)="goBack()">Back</button>
|
||||
<!-- #docregion save -->
|
||||
<button (click)="save()">Save</button>
|
||||
<!-- #enddocregion save -->
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* #docregion */
|
||||
.search-result{
|
||||
border-bottom: 1px solid gray;
|
||||
border-left: 1px solid gray;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<!-- #docregion -->
|
||||
<div id="search-component">
|
||||
<h4>Hero Search</h4>
|
||||
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
|
||||
|
||||
@@ -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<Hero[]>;
|
||||
// #enddocregion search
|
||||
// #docregion searchTerms
|
||||
private searchTerms = new Subject<string>();
|
||||
// #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<Hero[]>([]);
|
||||
});
|
||||
}
|
||||
// #enddocregion search
|
||||
|
||||
gotoDetail(hero: Hero): void {
|
||||
let link = ['/detail', hero.id];
|
||||
|
||||
@@ -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<Hero[]> {
|
||||
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
|
||||
|
||||
@@ -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<Hero[]> {
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
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<Hero> {
|
||||
getHero(id: number): Observable<Hero> {
|
||||
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<void> {
|
||||
delete(id: number): Observable<void> {
|
||||
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<Hero> {
|
||||
create(name: string): Observable<Hero> {
|
||||
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<Hero> {
|
||||
update(hero: Hero): Observable<Hero> {
|
||||
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<any> {
|
||||
private handleError(error: any): Observable<any> {
|
||||
console.error('An error occurred', error); // for demo purposes only
|
||||
return Promise.reject(error.message || error);
|
||||
throw error;
|
||||
}
|
||||
// #enddocregion getHeroes, handleError
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base href="/">
|
||||
<title>Angular Tour of Heroes</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<my-app>Loading...</my-app>
|
||||
</body>
|
||||
<script src="node_modules/core-js/client/shim.min.js"></script>
|
||||
<script src="node_modules/zone.js/dist/zone.min.js"></script>
|
||||
<script src="dist/build.js"></script>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
// #docregion
|
||||
import { platformBrowser } from '@angular/platform-browser';
|
||||
import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory';
|
||||
|
||||
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
|
||||
@@ -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...');
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
+6
-8
@@ -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 {}
|
||||
@@ -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}...`);
|
||||
});
|
||||
@@ -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}/`;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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": {
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/src/**/*.js",
|
||||
"!**/universal/**/*.js"
|
||||
],
|
||||
"removeSystemJsConfig": false,
|
||||
"type": "universal"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user