Run AngularJS apps on the server, using NodeJS.
AngularJS is an awesome framework for building rich single page applications. However, one significant downside of single page applications is that they are often inscrutable to non-browser clients like search engines, RSS readers and social network indexers.
AngularJS-Server is a reimplementation of several core AngularJS modules with the goal of making it possible to run AngularJS applications on the server, using NodeJS.
In the simple case this allows a well-behaved application (one which interacts only with AngularJS's API, not with the browser directly) to render its pages on the server, generating a full HTML page that is readable by search engines. It also enables more advanced use-cases of sharing code between client and server, such as running only parts of an AngularJS application on the server to produce an RSS feed whose content is rendered from the same templates as the site itself.
A simple application can be run in AngularJS-Server using only a small amount of NodeJS application bootstrap code, using express:
var angularserver = ;var express = ;var fs = ;// The main index.html file for your application, that you'd normally serve to browsers to start the app.// This should have any script tags for your application removed from it, as they will be added separately.var templateFile = 'index.html';var template = fs;// Directory to serve static resources from.var staticDir = 'static';var app = ;var angularMiddlewares = angularserver;// Make the scripts and other assets available to the browser.app;// Serve all other URLs by rendering the Angular page on the server.app;
The above arranges for the HTML delivered by the server to include both the raw template for AngularJS and the pre-rendered version for robots. This is the minimal basic implementation of AngularJS-Server. For a more complete example making use of some additional features, see the weather example.
The AngularJS-Server core includes a simple middleware for rendering snapshots from a NodeJS application. However, it also provides an API for creating custom integrations between normal NodeJS server code and AngularJS application code.
The HTML generated by the
htmlGenerator are compatible with the alternative
angularjs-sdr, which moves the route resolution steps onto the server.
For applications using the
resolve property on route definitions, this can improve performance by retrieving
necessary data on the server before returning the page, allowing the page to be rendered with no additional
To use this mechanism it is necessary to expose an "SDR API" endpoint on the server:
With this in place, the app should then be configured to load the
angularjs-sdr code on the client (i.e. add it
clientScripts list in the AngularJS-Server instantiation) and then use the
sdr module in place of
If all data loading is confined to route resolution functions then this means that every route transition will lead to exactly one fetch from the server, to resolve the route. The server will then fetch any necessary data before returning a flattened route payload to the client. As a further trick the initial route data is inlined within the HTML on first page load, so the first page view may be displayed with no additional data fetches whatsoever.
In return for this optimization your app must comply with some additional constraints. Most notably, any data returned
by route resolution must be JSON-compatible, since this is how the data is transmitted to the client from the
server. Notably also this mechanism works by overriding
$route, so it will work only for applications using
the standard AngularJS router.
One way to implement a custom integration is to "wrap" a middleware with an Angular context. This means that AngularJS-server will instantiate the AngularJS application for each request and pass an injector for it as an additional parameter to the middleware. For (contrived) example:
Here we use the injector to get hold of the root scope and the
$interpolate service, and use the latter
to interpolate a name into a greeting string. The injector can be used to obtain a reference to any service
available from the configured AngularJS modules.
When accessed in this way, by default the global template is not compiled, since many uses of this
mechanism are not expected to use the template at all. However, if a full-page compile is required then
this can be arranged by calling
injector.bootstrap() early on in the request handling. After this,
$rootElement will be available in the injector as normal.
The lowest-level interaction with AngularJS-Server is to simply obtain an injector and do to it what you will. This is accomplished as follows:
One way to use this mechanism is to make use of configuration information from the Angular application during
the application setup process. For example, one can request the
$routeProvider service and use the routes configured
within it to conditionally create specific URL routes in an Express application.
Injectors created via this mechanism are not initially attached to a request, so the
$location service will
be inoperable. The
wrapMiddlewareWithAngular mechanism introduced above is essentially a wrapper around
runInContext that pre-configures
$location based on the URL from the incoming request.
Rather than emulating an entire browser, as some sites do using PhantomJS, AngularJS-Server simply shims the AngularJS API to work better in the NodeJS server environment.
This means that performance is likely to be better, and allows for some more interesting interactions between the Angular app
and the server, but this solution is not so well suited to applications that routinely bypass the AngularJS API and access
browser DOM functionality directly.
jsdom provides an implementation of much of the DOM API, but certain things like
measuring window size or taking element positions obviously make no sense in an environment with no renderer.
Pre-existing applications may therefore need to be lightly modified in order to work within the assumptions of this container. Anecdotally Say Media managed to get its reasonably-complex site rendering application working within these constraints with a few weeks of investment, although more hours were subsequently spent optimizing the result for performance due to different constraints in the server environment.
The limits on acceptable performance characteristics can differ quite significantly between client and server. Although AngularJS-Server can run many AngularJS applications as-is, there are some gotchas to keep in mind:
Client-side code consumes memory and CPU only on the client's machine, whereas of course server resources are shared between all clients.
Applications that do lots of object instantiation may find that their memory usage grows faster than the V8 garbage collector is able to reclaim it -- at least, without significant GC pauses. For such applications it may be necessary to set a stricter memory limit (at the expense of increased GC CPU usage when memory is tight) or again implement a solution with AngularJS-Server access happening in worker processes that can be periodically rebooted to reclaim memory, asynchronously from main request handling.
JSDOM's priority is correct implementation of the DOM API rather than performance, and for many operations JSDOM is significantly slower than state-of-the-art browser DOM implementations. Applications that freqently update or otherwise traverse the DOM should expect visibly slower performance when running in the server, and high CPU usage with the same implications described above. This can be mitigated by strategies such as delaying template compilation until data loading has stablized, to avoid incremental DOM updates as data loads.
The built-in features for server-side rendering of AngularJS-powered pages require an application using the standard
$route service, since they work by overriding this service to intercept route registration.
The lower-level features --
runInContext -- have no specific service dependencies.
In fact, it may be possible to use these mechanisms to implement comparable features for other route implementations,
but for the moment that is left as an exercise for the reader.
Parts of this codebase are currently in use in production on Say Media-run content sites like Bio and xoVain. However, the interface is not finalized and will evolve as we learn more about this problem space.
Therefore this codebase is currently shared primarily as an illustration of a possible strategy for making AngularJS-based sites robot-friendly, rather than as a ready-to-go solution. However, at the time of writing the following features are in use in production:
All of the AngularJS service overrides in
ngoverrides.js. These replace several key AngularJS services with more
appropriate implementations for the server environment.
makeJsonFriendlyRoute functions, which allow server-side code to match paths to routes and
run the route "resolve" code on the server.
middlewareWithAngular function, which decorates a connect middleware with code to spin up an AngularJS context
and pass its injector as an extra parameter.
Variants of the remaining functions are in use in the Say Media content delivery platform, but their implementation has been modified beyond what is shown in this codebase. Once their implementations are more stable we intend to update this codebase, at which point the interface will almost certainly change.
In the mean time we welcome others to experiment with using AngularJS-Server on their own applications and share their experiences in the AngularJS-Server Google Group.
Development environment setup is pretty standard: just clone the git repo and run
Contributions are welcome but are assumed to be licensed under the MIT license, as this package itself is. Please be sure to run the unit tests (such that they are) and the linter on your modified version, and then open a pull request.
npm testnpm run-script lint
The project's code style is represented by the
.jshintrc file but please also attempt to remain
consistent with the prevailing style even if particular details aren't tested for by the linter.