Skip to content

Instantly share code, notes, and snippets.

@jhades
Last active June 13, 2021 21:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jhades/c96635b2b48135516e7ba04534758608 to your computer and use it in GitHub Desktop.
Save jhades/c96635b2b48135516e7ba04534758608 to your computer and use it in GitHub Desktop.
Angular Universal Guide
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist-server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
}
}
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
@NgModule({
declarations: [
AppComponent,
...
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
...
],
providers: [
...
],
bootstrap: [AppComponent],
})
export class AppModule {
}
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {renderModuleFactory} from '@angular/platform-server';
import {writeFileSync} from 'fs';
const {AppServerModuleNgFactory} = require('./dist-server/bundle');
renderModuleFactory(AppServerModuleNgFactory, {
document: '<app-root></app-root>',
url: '/'
})
.then(html => {
console.log('Pre-rendering successful, saving prerender.html');
writeFileSync('./prerender.html', html);
})
.catch(error => {
console.error('Error occurred:', error);
const {AppServerModuleNgFactory} = require('./dist-server/bundle');
...
renderModuleFactory(AppServerModuleNgFactory, {
document: '<app-root></app-root>',
url: '/'
})
...
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {renderModuleFactory} from '@angular/platform-server';
import * as express from 'express';
import { readFileSync } from 'fs';
import { enableProdMode } from '@angular/core';
const {AppServerModuleNgFactory} = require('./dist-server/bundle');
enableProdMode();
const app = express();
const indexHtml = readFileSync(__dirname + '/dist/index.html', 'utf-8').toString();
app.get('*.*', express.static(__dirname + '/dist', {
maxAge: '1y'
}));
app.route('*').get((req, res) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: indexHtml,
url: req.url
})
.then(html => {
res.status(200).send(html);
})
.catch(err => {
console.log(err);
res.sendStatus(500);
});
});
app.listen(9000, () => {
console.log(`Angular Universal Node Express server listening on http://localhost:9000`);
});
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular Universal Course</title>
<base href="/">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono:300" rel="stylesheet">
<link href="styles.faa5dcd931f22d491e1f.bundle.css" rel="stylesheet"/>
</head>
<body>
<app-root></app-root>
<script type="text/javascript" src="inline.6a5615aec11679fd2f49.bundle.js"></script>
<script type="text/javascript" src="polyfills.9afba58cc5c5e906563e.bundle.js"></script>
<script type="text/javascript" src="main.f9c77e3090fdfb4a85b6.bundle.js"></script>
</body>
</html>
...
const {AppServerModuleNgFactory} = require('./dist-server/bundle');
enableProdMode();
const app = express();
const indexHtml = readFileSync(__dirname + '/dist/index.html', 'utf-8').toString();
...
app.route('*').get((req, res) => {
renderModuleFactory(AppServerModuleNgFactory, {
document: indexHtml,
url: req.url
})
.then(html => {
res.status(200).send(html);
})
.catch(err => {
console.log(err);
res.sendStatus(500);
});
});
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Learn Angular</title>
<base href="/">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono:300" rel="stylesheet">
<link href="styles.faa5dcd931f22d491e1f.bundle.css" rel="stylesheet"/>
.... we will find here a LOT of inline style tags, for styling Angular components ....
</head>
<body>
<app-root>
.... This is no longer empty, there is a LOT of HTML here ....
</app-root>
<script type="text/javascript" src="inline.6a5615aec11679fd2f49.bundle.js"></script>
<script type="text/javascript" src="polyfills.9afba58cc5c5e906563e.bundle.js"></script>
<script type="text/javascript" src="main.f9c77e3090fdfb4a85b6.bundle.js"></script>
</body>
</html>
<script type="text/javascript" src="main.f9c77e3090fdfb4a85b6.bundle.js"></script>
app.get('*.*', express.static(__dirname + '/dist', {
maxAge: '1y'
}));
app.listen(9000, () => {
console.log(`Angular Universal Node Express server listening on http://localhost:9000`);
});
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {renderModuleFactory} from '@angular/platform-server';
import * as express from 'express';
import { readFileSync } from 'fs';
import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist-server/bundle');
enableProdMode();
const app = express();
const distFolder = __dirname + '/dist';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)]
}));
app.set('view engine', 'html');
app.set('views', distFolder);
app.get('*.*', express.static(distFolder, {
maxAge: '1y'
}));
app.get('*', (req, res) => {
res.render('index', {req});
});
app.listen(9000, () => {
console.log(`Angular Universal Node Express server listening on http://localhost:9000`);
});
@Component({
selector: 'course',
templateUrl: './course.component.html',
styleUrls: ['./course.component.css']
})
export class CourseComponent implements OnInit {
course: Course;
constructor(private title: Title,
private meta: Meta) {}
ngOnInit() {
this.course = this.route.snapshot.data['course'];
....
// SEO metadata
this.title.setTitle(this.course.description);
this.meta.addTag({name: 'description', content: this.course.longDescription});
}
}
ngOnInit() {
this.course = this.route.snapshot.data['course'];
....
// SEO metadata
this.title.setTitle(this.course.description);
this.meta.addTag({name: 'description', content: this.course.longDescription});
// Twitter metadata
this.meta.addTag({name: 'twitter:card', content: 'summary'});
this.meta.addTag({name: 'twitter:site', content: '@AngularUniv'});
this.meta.addTag({name: 'twitter:title', content: this.course.description});
this.meta.addTag({name: 'twitter:description', content: this.course.description});
this.meta.addTag({name: 'twitter:text:description', content: this.course.description});
this.meta.addTag({name: 'twitter:image', content: 'https://avatars3.githubusercontent.com/u/16628445?v=3&s=200'});
}
}
<mat-sidenav-container fullscreen>
...
<router-outlet></router-outlet>
<div class="spinner-container" *appShellRender>
<mat-spinner></mat-spinner>
</div>
</mat-sidenav-container>
<div class="course">
<h2>{{course?.description}}</h2>
<img class="course-thumbnail" [src]="course?.iconUrl">
<mat-table [dataSource]="dataSource" *appShellNoRender>
....
</mat-table>
</div>
@Directive({
selector: '[appShellRender]'
})
export class AppShellRenderDirective implements OnInit {
constructor(
private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<any>,
@Inject(PLATFORM_ID) private platformId) {}
ngOnInit() {
if (isPlatformServer(this.platformId)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
else {
this.viewContainer.clear();
}
}
}
<div class="spinner-container">
<mat-spinner></mat-spinner>
</div>
ngOnInit() {
if (isPlatformServer(this.platformId)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
else {
this.viewContainer.clear();
}
}
@Directive({
selector: '[appShellNoRender]'
})
export class AppShellNoRenderDirective implements OnInit {
constructor(
private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<any>,
@Inject(PLATFORM_ID) private platformId) {}
ngOnInit() {
if (isPlatformServer(this.platformId)) {
this.viewContainer.clear();
}
else {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
}
@Injectable()
export class CourseResolver implements Resolve<Course> {
constructor(private coursesService: CoursesService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Course> {
const courseId = route.params['id'];
return this.coursesService.findCourseById(courseId);
}
}
import {PLATFORM_ID} from '@angular/core';
import {isPlatformServer} from '@angular/common';
import {makeStateKey, TransferState} from '@angular/platform-browser';
@Injectable()
export class CourseResolver implements Resolve<Course> {
constructor(
private coursesService: CoursesService,
@Inject(PLATFORM_ID) private platformId,
private transferState:TransferState) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Course> {
const courseId = route.params['id'];
const COURSE_KEY = makeStateKey<Course>('course-' + courseId);
if (this.transferState.hasKey(COURSE_KEY)) {
const course = this.transferState.get<Course>(COURSE_KEY, null);
this.transferState.remove(COURSE_KEY);
return of(course);
}
else {
return this.coursesService.findCourseById(courseId)
.pipe(
tap(course => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(COURSE_KEY, course);
}
})
);
}
}
}
const COURSE_KEY = makeStateKey<Course>('course-' + courseId);
if (this.transferState.hasKey(COURSE_KEY)) {
....
}
else {
....
}
...
return this.coursesService.findCourseById(courseId)
.pipe(
tap(course => {
if (isPlatformServer(this.platformId)) {
this.transferState.set(COURSE_KEY, course);
}
})
);
...
...
if (this.transferState.hasKey(COURSE_KEY)) {
const course = this.transferState.get<Course>(COURSE_KEY, null);
this.transferState.remove(COURSE_KEY);
return of(course);
}
...
@marie-dk
Copy link

marie-dk commented Mar 6, 2020

Reading/using your Angular Universal guide. It's fantastic. Thanks!
Just want to mention that the file 05.ts is missing }); at the end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment