angular render service | | Cell 1 | Search

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.

Run example

npm run import -- "render Angular modules instead of using express"

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);

What the code could have been:

// 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:

  1. Imports: It imports necessary modules from Angular, Express, and other libraries for server-side rendering, polyfills, and dependency injection.

  2. Configuration: It enables production mode and defines a factoryCacheMap to store compiled module factories for efficiency.

  3. 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.

  4. Rendering Process:

  5. 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.