Angular is great on the client, but a lot of people complain that web crawlers do not see the dynamic application. This forces developers to either pre-render a view of the website using PhantomJs / CasperJs, or create isomorphic JavaScript libraries that can render client application and an equivalent server-side application. These solutions seem like huge overkill to me. What if we could render the same DOM server side using Node?
Previously I tried loading AngularJs library from Node, but it required small fixes to content security policy variables (see Unit testing Angular load using Node). Recently, the Angular library has been fixed, and v1.2.25 loads under Node using synthetic browser emulation just fine. Here is what you can do with plain vanilla angular from Node.
Basic setup
To load and run Angular we need valid window
and document
objects that simulate the actual browser.
I am using benv that wraps around jsdom
to create these objects.
1 | var benv = require('benv'); |
Loading Angular
benv allows loading scripts in the window context, but returns a valid reference. In the example below I will load zepto before loading AngularJs script. Loading Zepto is not strictly necessary, but will make DOM manipulations simpler compared to jQuery-lite included with AngularJs.
1 | var benv = require('benv'); |
Both Zepto and Angular scripts are run of the mill scripts installed using bower
bower install zepto angular
Bind model
Let us take a simplest Angular feature - model binding and see if it works. We will setup window
and document
,
then will load basic HTML into the document. Then we will load AngularJs library and will bootstrap the document.
1 | var benv = require('benv'); |
Notice that there is no distinction between the Node script and the "browser" environment - we can
manipulate angular modules, controllers, etc. directly. This is very different from running scripts
via PhantomJs (where we need to use evaluate
method to execute script in the browser context).
The script produces valid DOM with bound data values
1 | <html> |
Notice that it even adds AngularJs style sheet, and most importantly sets the correct contents to h1
node.
This page can now be parsed by any web crawler without running JavaScript.
Using $timeout
Let us do something a little more complicated: let us update the model after an interval to see if it works.
1 | benv.setup(function () { |
Notice that instead of writing angular script directly inside timeout-example.js
we moved it into
separate file timeout-app.js
1 | angular.module('bindValue', []) |
Running timeout-example.js
from node generates after 1.5 seconds
$ node timeout-example.js
...
<body style="">
<div ng-controller="greetingController" class="ng-scope">
<h1 class="ng-binding">Hi Node after 1 second!</h1>
</div>
</body>
So $timeout
service works
Application organization
In the above timeout example we loaded the AngularJs script from separate file. We can move the HTML text into
separate file index.html
and load timeout-app.js from there too!
1 |
|
The app.js
file
1 | angular.module('myApp', []) |
You can open index.html
in your browser and see AngularApp working just fine.
Here is the Node rendering script
1 | var benv = require('benv'); |
We need to load app.js
because synthetic browser environment does NOT load and run scripts.
Thus all <script>
tags in the index.html
are ignored. Still we generate valid DOM when we run
1 | <html> |
Conclusion
This was simple experiment in loading and running Angular application completely inside synthetic browser environment. It proves that Node JavaScript environment is good enough for Angular to run its update cycle, perform dependency injection, run services and update DOM.