Security News
PyPI’s New Archival Feature Closes a Major Security Gap
PyPI now allows maintainers to archive projects, improving security and helping users make informed decisions about their dependencies.
angular-ssr
Advanced tools
The purpose of this library is to allow your application to support server-side rendering of your Angular 4+ applications with minimal code changes and mimimal difficulty. It supports both Angular CLI projects and projects that use custom webpack configurations. It works out of the box with @angular/material
with no hot-fixes or workarounds! It also requires zero changes to your application code: you won't have to create separate @NgModule
s, one for the server-side rendered application and one for the regular client application. You can just take your Angular code as-is and follow the steps below to get server-side rendering working.
There are two ways you can use angular-ssr
:
ng build
, and then invoke ng-render
from node_modules/.bin
. This will result in several steps being taken:
tsconfig.json
and some other configuration elements to compile your application to a temporary directory and load the resulting JavaScript code (application code + .ngfactory.js
files) into memory./
, /foo
, /bar
).html
file in dist
(or, if you specified an alternate output directory using --output
, it will write the files there). It instantiates the application using the existing dist/index.html
file that was produced as part of your normal application build as a template. The pre-rendered content will be inserted into that template and written out as a new .html
file based on the route: e.g., /foo/index.html
.ng-render
. But that's alright. It just means that you will have to build a separate webpack program output: either a NodeJS HTTP server, or a NodeJS application whose sole purpose is to do prerendering. You will follow these rough steps:
angular-ssr
as a dependency: npm install angular-ssr --save
angular-ssr
.webpack.config.js
and just add an additional output, or you create an entirely new webpack-server.config.js
which will serve as your SSR webpack configuration. Regardless of how you accomplish it, you will ultimately need to produce two programs from webpack:
angular-ssr
will not integrate directly with the HTTP server anyway. It just exposes a very simple API to get pre-rendered HTML documents, and you can integrate this with your server in whichever way makes the most sense for your application./a
, /b
, /c
, etc.). In this case, since all routes are known in advance and none of them take any URL parameters, we can just pre-render each route at build time and spit out some .html
files./blog/post/1
, /blog/user/3
, etc. In this case, you will need to do on-demand server-side rendering. No problem! In this use-case, it makes sense to build a small HTTP server using express or koa and to write a few lines of code to integrate with angular-ssr
. Then from inside your server, you can demand render and cache particular routes with arguments like /blog/post/1
. I will show you some examples of how to do this below.If your application was generated by ng new
and does not use any custom webpack configuration, then you will be able to use the ng-render
CLI tooll to automatically pre-render your application routes into static .html
files. It is worth emphasizing that this use case is the easiest, but also the least flexible. If you need on-demand rendering, or if you have custom webpack configurations, then you should skip down to the examples below as they will cover your use-case better than this section.
But, in the event that you do have a simple ng cli
application, you can give angular-ssr
a whirl just by doing:
npm install angular-ssr --save
ng build
./node_modules/.bin/ng-render
It should spit out some messages like:
[info] Writing rendered route / to /Users/bond/proj/dist/index.html
[info] Writing rendered route /foo to /Users/bond/proj/dist/foo/index.html
[info] Writing rendered route /bar to /Users/bond/proj/dist/bar/index.html
You can then do cd dist
and run:
npm install -g http-server
http-server .
Then when you load the application by hitting http://localhost:8080
, you should see the pre-rendered document in the initial HTTP response (for each route in your application).
Let's get this use-case out of the way first, because I think it is likely to be the most common usage of angular-ssr
.
You have an HTTP server application that you build as part of your application using webpack. Your HTTP server is written in TypeScript. (If your HTTP server is written in JavaScript, the library will still work in the same way, but you won't be able to copy-paste the code below.)
When you build your application, you are outputting two targets: your actual Angular client application, and your HTTP server application. We are going to focus on the server application here because there will be zero changes to your application code.
Your actual HTTP server code will look something like the following:
import 'reflect-metadata';
import {
ApplicationFromModule,
DocumentStore,
fileFromString,
routeToPath,
} from 'angular-ssr';
import {join} from 'path';
import {AppModule} from '../src/app/app.module';
const dist = join(process.cwd(), 'dist');
const templateDocument = fileFromString(join(dist, 'index.html'));
if (templateDocument.exists() === false) {
throw new Error('dist/index.html must exist because it is used as an SSR template');
}
const application = new ApplicationFromModule(AppModule);
application.templateDocument(templateDocument.content());
// Pre-render all routes that do not take parameters (angular-ssr will discover automatically)
const prerender = async () => {
const snapshots = await application.prerender();
return snapshots.subscribe(
snapshot => {
app.get(snapshot.uri, (req, res) => res.send(snapshot.renderedDocument));
})
.toPromise();
};
prerender();
const documentStore = new DocumentStore(application);
// Demand render and cache all other routes (eg /blog/post/12)
app.get('*',
async (req, res) => {
try {
const snapshot = await documentStore.load(req.url);
res.send(snapshot.renderedDocument);
}
catch (exception) {
res.send(templateDocument.content()); // fall back on client-side rendering
}
});
If your application does not fall into the categories described above (i.e., you do not need on-demand server-side rendering of all URLs), then perhaps your application falls into another category: single-use server-side rendering as part of the application build process.
In this case, your code will look similar to the HTTP server code above, but instead of integrating with express, you will simply use ApplicationRenderer
to pre-render all application routes and write them to static .html
files, which you can then serve with the HTTP server of your choosing. Again: this case only makes sense if you do not need on-demand rendering of all application routes.
In this case, your code will look something like this:
import 'reflect-metadata';
import {
ApplicationFromModule,
ApplicationRenderer,
HtmlOutput,
fileFromString,
pathFromString
} from 'angular-ssr';
import {join} from 'path';
import {AppModule} from '../src/app.module';
const dist = join(process.cwd(), 'dist');
const templateDocument = fileFromString(join(dist, 'index.html'));
if (templateDocument.exists() === false) {
throw new Error('Build output dist/index.html must exist prior to prerender');
}
const application = new ApplicationFromModule(ServerModule);
application.templateDocument(templateDocument.content());
const html = new HtmlOutput(pathFromString(dist));
const renderer = new ApplicationRenderer(application);
renderer.renderTo(html)
.catch(exception => {
console.error('Failed to render due to uncaught exception', exception);
});
Now we arrive at the most complex use case. Here we wish to do prerendering and demand rendering inside a NodeJS HTTP server, but we also wish to render variants of each page. For example, our application may support multiple languages. angular-ssr
supports this using a concept called a variant. A variant is essentially a key, a set of unique values, and a transition function which can place the application in the specified state.
To illustrate, let's again use the example of locales / languages. Your application has multiple languages and you want to support server-side rendering for each of them. The first time someone loads your application, we set the current language selection to the value of navigator.language
(eg, "en-US"). We set an application cookie using document.cookie
so that subsequent loads of the application will include as part of the request the language that the user wishes to view the application in. Assume we have some simple code like this somewhere in the application:
import {Component, Injectable, OnInit} from '@angular/core';
@Component({
selector: 'app',
template: `<locale-selector [value]="localeService.locale" (select)="onLocaleChanged($event)"></locale-selector>`
})
export class LocaleSelector implements OnInit {
constructor(public localeService: LocaleService) {}
onLocaleChanged(locale: string) {
this.localeService.locale = locale;
}
}
@Injectable()
export class LocaleService {
get locale(): string {
return this.extractFromCookie('locale') || (() => {
this.setInCookie('locale', navigator.language);
return navigator.language;
});
}
set locale(locale: string) {
this.setInCookie('locale', locale);
}
private getCookies(): Map<string, string> {
return new Map<string, string>(document.cookie.split(/; /g).map(c => c.split(/=/)));
}
private extractFromCookie(key: string): string {
return this.getCookies().get(key);
}
private setInCookie(key: string, value: string) {
const cookies = this.getCookies();
cookies.set(key, value);
document.cookie = Array.from(cookies.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
}
}
Essentially what this code is doing is setting a cookie in two events:
navigator.language
, to respect their system locale settings.document.cookie
with the new locale
setting.The code above means that our HTTP requests will match one of two cases:
navigator.language
to select the system-correct localelocale
cookie which we can use to determine which language we should return when we are querying our document store.We can handle this by rendering different variants of our application. Let's assume that our application supports en-US
, en-CA
and fr-FR
locales. This is how we would configure the server:
import {Injector, Injectable} from '@angular/core';
@Injectable()
export class LocaleTransition {
constructor(private localeService: LocaleService) {}
// This is the bit of code that actually executes the transition to set the locale
// to whichever value is being rendered (but value is guaranteed to be one of the
// values from the Set we created when we first described the locale variant below).
// Note that this class can use the ng dependency injection system to retrieve any
// services that it needs in order to execute the state transition.
execute(value: string) {
this.localeService.locale = value;
}
}
const application = new ApplicationFromModule(AppModule);
application.templateDocument(templateDocument.content());
application.variants({
locale: {
values: new Set<string>([
'en-CA',
'en-US',
'fr-FR'
]),
transition: LocaleTransition
}
});
type ApplicationVariants = {locale: string};
// DocumentVariantStore is a variant-aware special-case of DocumentStore. When you
// query it, you must provide values for each variant key that we described in the
// call to variants() above. But in this application, there is only one key: locale.
const documentStore = new DocumentVariantStore<ApplicationVariants>(application);
app.get('*', async (req, res) => {
try {
// Remember that we set locale in document.cookie, so all requests after the
// first-ever application load will have a locale cookie that we can use to
// decide whether to give the user an English or French pre-rendered page.
const snapshot = await documentStore.load(req.url, {locale: req.cookies.locale});
res.send(snapshot.renderedDocument);
}
catch (exception) {
res.send(templateDocument.content()); // fall back on client-side rendering
}
});
Voila! Now whenever the user reloads our application or comes back to it in a few days, we are going to hand them a pre-rendered document that is in the language of their choosing! Simple.
Many applications may wish to transfer some state from the server to the client as part of application bootstrap. angular-ssr
makes this easy. Simply tell your ApplicationBuilder
object about your state reader class or function, and any state returned from it will be made available in a global variable called bootstrapApplicationState
:
const application = new ApplicationFromModule(AppModule);
application.stateReader(ServerStateReader);
And your ServerStateReader
class implementation might look like this:
import {Injectable} from '@angular/core';
import {Store} from '@ngrx/store';
import {StateReader} from 'angular-ssr';
@Injectable()
export class ServerStateReader implements StateReader {
constructor(private store: Store<AppState>) {}
getState(): Promise<MyState> {
return this.store.select(s => s.someState).toPromise();
}
}
Note that you can inject any service you wish into your state reader. angular-ssr
will query the constructor arguments using the ng dependency injector the same way it works in application code. Alternatively, you can supply a function which just accepts a bare Injector
and you can query the DI yourself:
application.stateReader((injector: Injector) => {
const service = injector.get(MyService);
return service.getState();
});
Both solutions are functionally equivalent.
Note that your state reader will not be called until your application zone becomes stable. That is to say, when all macro and microtasks have finished. (For example, if your application has some pending HTTP requests, angular-ssr
will wait for those to finish before asking your state reader for its state. This ensures that your application has finished initializing itself by the time the state reader is invoked.)
The main contract that you use to define your application in a server context is called ApplicationBuilder
. It has thorough comments and explains all the ways that you can configure your application when doing server-side rendering.
But ApplicationBuilder
is an interface. It has three concrete implementations:
ApplicationFromModule<V, M>
@NgModule
of your application, then this is probably the ApplicationBuilder
that you want to use. It takes a module type and a template HTML document (dist/index.html
) as its constructor arguments.ApplicationFromModuleFactory<V>
ngc
and produced .ngfactory.js
files, then you can pass your root @NgModule
's NgFactory -- not the module definition itself, but its compilation output -- to ApplicationFromModuleFactory<V>
and you can skip the template compilation process.ApplicationFromSource<V>
@angular/cli
if you wish to use inplace compilation to generate an NgModuleFactory
from raw source code. It's fairly unlikely that you will ever use this class: its main purpose is for the implementation of the ng-render
command.Other classes of interest are DocumentStore
and DocumentVariantStore
. You can use these in conjunction with ApplicationBuilder
to maintain and query a cache of rendered pages.
Snapshot<V>
Another interesting one is Snapshot
. This is the data structure you get back from the server-side rendering process. It takes a type argument that represents the variants your application is aware of, or void
if you are not using variants.
One thing to note about Snapshot
is that it contains far more information than just the final rendered document. It has:
console: Array<ConsoleLog>
console
.exceptions: Array<Error>
exceptions
, so you should usually check this in your retrieval methods to ensure that everything worked properly. You don't want to send a mangled document to the user.renderedDocument: string
variant: V
uri: string
I am in the process of building out some more complete example applications over the next day or two. In the meantime, if you have questions you want answered, you can email me at cb@clbond.org
or post an issue in this repository and I would be more than happy to answer!
Christopher Bond
FAQs
Angular server-side rendering implementation
The npm package angular-ssr receives a total of 112 weekly downloads. As such, angular-ssr popularity was classified as not popular.
We found that angular-ssr demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now allows maintainers to archive projects, improving security and helping users make informed decisions about their dependencies.
Research
Security News
Malicious npm package postcss-optimizer delivers BeaverTail malware, targeting developer systems; similarities to past campaigns suggest a North Korean connection.
Security News
CISA's KEV data is now on GitHub, offering easier access, API integration, commit history tracking, and automated updates for security teams and researchers.