This code implements a server-side rendering engine for Angular applications using Express, allowing for pre-rendered HTML to be sent to the client. It leverages Angular's platformDynamicServer
and renderModuleFactory
to compile and render the application for a given URL.
npm run import -- "render Angular modules instead of using express"
// mostly taken from ngRenderEngine which requires express
// polyfills
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import 'rxjs/Rx';
// angular
import {
APP_INITIALIZER,
Compiler,
CompilerFactory,
enableProdMode,
InjectionToken,
Injector,
NgModuleFactory,
Type
} from '@angular/core';
import {ResourceLoader} from '@angular/compiler';
import {INITIAL_CONFIG, platformDynamicServer, renderModuleFactory} from '@angular/platform-server';
import {AppServerModule} from './app/app.server.module';
import {Router} from '@angular/router';
import {APP_BASE_HREF} from '@angular/common';
enableProdMode();
/**
* Map of Module Factories
*/
const factoryCacheMap = new Map < Type < {} >, NgModuleFactory
<
{
}
>>
();
/**
* This is an express engine for handling Angular Applications
*/
function bootstrapRender(boot: any) {
const compilerFactory: CompilerFactory = platformDynamicServer().injector.get(CompilerFactory);
const compiler: Compiler = compilerFactory.createCompiler([
{
providers: []
}
]);
return function (url: string, callback: (err?: Error | null, html ? : string
) =>
void
)
{
try {
if (!boot) {
return callback(new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'));
}
getFactory(boot, compiler)
.then(factory => {
return renderModuleFactory(factory, {
document: '<app-root></app-root>',
url: url,
extraProviders: [
{
provide: APP_INITIALIZER,
useFactory: (injector: Injector) => {
return () => {
const router: Router = injector.get(Router);
router.navigate([url]); // => This has NO effect
console.log(router.url); // this logs: '/' instead of the 'domain.com/de-de/contact'
};
},
deps: [Injector],
multi: true
},
{
provide: INITIAL_CONFIG,
useValue: {
document: '<app-root></app-root>',
url: url
}
}
]
});
})
.then((html: string) => {
callback(null, html);
}, (err) => {
callback(err);
});
} catch (err) {
callback(err);
}
}
;
}
/**
* Get a factory from a bootstrapped module/ module factory
*/
function getFactory(moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler): Promise<NgModuleFactory<{}
>>
{
return new Promise < NgModuleFactory < {} >> ((resolve, reject) => {
// If module has been compiled AoT
if (moduleOrFactory instanceof NgModuleFactory) {
resolve(moduleOrFactory);
return;
} else {
let moduleFactory = factoryCacheMap.get(moduleOrFactory);
// If module factory is cached
if (moduleFactory) {
resolve(moduleFactory);
return;
}
// Compile the module and cache it
compiler.compileModuleAsync(moduleOrFactory)
.then((factory) => {
factoryCacheMap.set(moduleOrFactory, factory);
resolve(factory);
}, (err => {
reject(err);
}));
}
});
}
// Hacky way to get this function out of the Zone context?
( < any > global
).
renderer = bootstrapRender(AppServerModule);
// Import required modules
import 'zone.js/dist/zone-node';
import'reflect-metadata';
import 'rxjs/Rx';
import {
APP_INITIALIZER,
Compiler,
CompilerFactory,
enableProdMode,
InjectionToken,
Injector,
NgModuleFactory,
Type
} from '@angular/core';
import {ResourceLoader} from '@angular/compiler';
import {INITIAL_CONFIG, platformDynamicServer, renderModuleFactory} from '@angular/platform-server';
import {AppServerModule} from './app/app.server.module';
import {Router} from '@angular/router';
import {APP_BASE_HREF} from '@angular/common';
// Enable production mode
enableProdMode();
// Map of Module Factories
const factoryCacheMap = new Map, NgModuleFactory<{}>>();
/**
* Express engine for handling Angular Applications
* @param boot NgModule or NgModuleFactory to be bootstrapped
* @param url URL for rendering the application
* @param callback Callback function for handling the response
*/
function bootstrapRender(boot: any, url: string, callback: (err?: Error | null, html?: string) => void) {
try {
if (!boot) {
return callback(new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'));
}
// Get the compiler and factory from the platform
const compilerFactory: CompilerFactory = platformDynamicServer().injector.get(CompilerFactory);
const compiler: Compiler = compilerFactory.createCompiler([
{
providers: []
}
]);
// Get the factory from the module or factory
getFactory(boot, compiler)
.then(factory => {
// Render the module factory
return renderModuleFactory(factory, {
document: ' ',
url,
extraProviders: [
{
provide: APP_INITIALIZER,
useFactory: (injector: Injector) => {
return () => {
const router: Router = injector.get(Router);
router.navigate([url]); // => This has NO effect
console.log(router.url); // this logs: '/' instead of the 'domain.com/de-de/contact'
};
},
deps: [Injector],
multi: true
},
{
provide: INITIAL_CONFIG,
useValue: {
document: ' ',
url
}
}
]
});
})
.then((html: string) => {
callback(null, html);
}, (err) => {
callback(err);
});
} catch (err) {
callback(err);
}
}
/**
* Get a factory from a bootstrapped module/ module factory
* @param moduleOrFactory NgModule or NgModuleFactory to get the factory from
* @param compiler Compiler to use for compiling the module
* @returns Promise of the NgModuleFactory
*/
function getFactory(moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler): Promise> {
return new Promise>((resolve, reject) => {
// If module has been compiled AoT
if (moduleOrFactory instanceof NgModuleFactory) {
resolve(moduleOrFactory);
return;
} else {
// Check if the module factory is cached
let moduleFactory = factoryCacheMap.get(moduleOrFactory);
if (moduleFactory) {
resolve(moduleFactory);
return;
}
// Compile the module and cache it
compiler.compileModuleAsync(moduleOrFactory)
.then((factory) => {
factoryCacheMap.set(moduleOrFactory, factory);
resolve(factory);
}, (err => {
reject(err);
}));
}
});
}
// Set the global renderer to the bootstrapRender function
// This is a hacky way to get the function out of the Zone context
(global).renderer = (url: string, callback: (err?: Error | null, html?: string) => void) =>
bootstrapRender(AppServerModule, url, callback);
This code sets up a server-side rendering engine for an Angular application using Express.
Here's a breakdown:
Imports: It imports necessary modules from Angular, Express, and other libraries for server-side rendering, polyfills, and dependency injection.
Configuration: It enables production mode and defines a factoryCacheMap
to store compiled module factories for efficiency.
bootstrapRender
Function: This function takes a module or module factory as input and renders the Angular application for a given URL. It uses platformDynamicServer
to create a server-side platform and renderModuleFactory
to render the application.
Rendering Process:
CompilerFactory
and Compiler
from the server-side platform's injector.renderModuleFactory
, providing the URL and a placeholder <app-root>
element for the root component.Error Handling: It includes basic error handling to catch cases where no module or module factory is provided.
Let me know if you have any more questions.