Run AngularJS apps on the server, using NodeJS.
This package is no longer maintained and not recommended for use. While it did run in production at Say Media for several years, other approaches are now being used and this package is no longer being maintained for newer versions of AngularJS or Node.
The code is preserved here for posterity, in case anyone still wishes to use it, but we'd recommend instead considering Angular Universal (for Angular 2, rather than AngularJS) or one of the various hosted services that can provide server-side rendering for single-page applications within their own infrastructure.
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.
AngularJS-Server is built on jsdom, an implementation of the DOM API for Node, and angularcontext, which provides a lightweight container in which to instantiate your AngularJS application.
A simple application can be run in AngularJS-Server using only a small amount of NodeJS application bootstrap code, using express:
var angularserver = require('angularjs-server');
var express = require('express');
var fs = require('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.readFileSync(templateFile);
// Directory to serve static resources from.
var staticDir = 'static';
var app = express();
var angularMiddlewares = angularserver.Server({
template: template,
// Scripts that should be loaded into the angularjs context on the server.
// This should include AngularJS itself and all of the source files required
// to register your Angular modules, but *not* code to bootstrap the
// application.
serverScripts: [
'angular.js',
'angular-route.js',
'yourapp.js'
],
// Scripts that should be loaded by the client browser to render the page.
// This should include the same set of files to load Angular itself and
// your Angular modules, but should also include additional code that
// calls into angular.bootstrap to kick off the application.
// Unlike serverScripts, these are URLs.
clientScripts: [
'/static/angular.js',
'/static/angular-route.js',
'/static/yourapp.js',
'/static/clientonly.js'
],
// Angular modules that should be used when running AngularJS code on
// the server. 'ng' is included here by default, along with the
// special AngularJS-Server overrides of 'ng'.
angularModules: [
'yourapp'
]
});
// Make the scripts and other assets available to the browser.
app.use('/static', express.static(staticDir));
// Serve all other URLs by rendering the Angular page on the server.
app.use(angularMiddlewares.htmlGenerator);
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 $route
implementation
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
XMLHttpRequest
lookups.
To use this mechanism it is necessary to expose an "SDR API" endpoint on the server:
app.use('/:', angularMiddlewares.sdrApi);
With this in place, the app should then be configured to load the angularjs-sdr
code on the client (i.e. add it
to te clientScripts
list in the AngularJS-Server instantiation) and then use the sdr
module in place of
ngRoute
.
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:
app.use(
angularMiddlewares.wrapMiddlewareWithAngular(
function (req, res, next, injector) {
var $rootScope = injector.get('$rootScope');
var $interpolate = injector.get('$interpolate');
$rootScope.name = 'world';
var greeting = $interpolate('Hello, {{name}}')($rootScope);
res.end(greeting);
}
)
);
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:
angularMiddlewares.runInContext(
function (injector) {
// ... whatever you want
}
);
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.
In particular, the AngularJS digest mechanism is synchronous, so an application with many scope watchers may experience significant pauses in execution during digest, during which the NodeJS process is blocked from all other JavaScript code execution. For such applications it is pertinent to interact with AngularJS-Server only in separate worker processes, to avoid stalling the event loop in the main NodeJS process.
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
AngularJS $route
service, since they work by overriding this service to intercept route registration.
The lower-level features -- wrapMiddlewareWithAngular
and 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 were used in production on Say Media-run content sites. However, Say Media is no longer using AngularJS for this purpose and so this codebase is no longer used or maintained.
This codebase was 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, the following features were used 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.
The resolveRoute
and makeJsonFriendlyRoute
functions, which allow server-side code to match paths to routes and
run the route "resolve" code on the server.
The 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 were used in the Say Media content delivery platform, but their implementation was modified beyond what is shown in this codebase.
Development environment setup is pretty standard: just clone the git repo and run npm install
within it.
Since this package is no longer maintained, we are no longer able to accept contributions.