Recently I have shown how to load and render a simple AngularJS application in a synthetic browser environment under NodeJS. One can use this technique to fully test the application's code using a clean environment for each unit test. At ng-conf 2015 I showed how to run a simple implementation of Angular's dirty checking and digest cycle inside a web worker.
In this blog post I will show how to load and run the full standard Angular 1.x inside a browser's Web Worker.
This instance of Angular will run separately from the main browser's window (where the original AngularJS can
be running or not), will communicate with the main window via postMessage
API, and will not block the
browser's responses while processing data.
Before proceeding, let me state the differences in the three environments where I got AngularJS running: browser, NodeJS and Web Worker. This will be helpful when trying to make Angular work inside a web worker.
browser | NodeJS | NodeJS + jsdom (benv) | Web Worker
------------------------------------------------------------------------------------------
window | global | global, window | self
document | - | document | -
- | module, require | module, require | -
window.location | __filename | __filename | -
Compared to the browser environment, or even plain NodeJS runtime, the Web Worker runtime is very bare bones.
AngularJS requires both window and document objects to run (the window
object to do everything, and
the document
object to mainly run document.getElementById
and document.attachEventListener
methods).
Thus I have decided to do the following:
Step 1 - load Angular under Node
I created the synthetic environment using jsdom via benv wrappers.
These libraries create both window
and document
objects that can fool any other library.
1 | var benv = require('benv'); |
Creating a mock browser environment is pretty simple, but how to load the Angular framework into the mock window?
Well, inside the setup
callback we do have a window and a document, so we can just load angular.js
as if it were a CommonJS module.
1 | var benv = require('benv'); |
When run, this code displays
have window? true
have document? true
created window and document
loaded angular { full: '1.2.25',
major: 1,
minor: 2,
dot: 25,
codeName: 'hypnotic-gesticulation' }
Nice!
Step 2 - pack everything from Node to run in the browser
Wait, we need to pack the synthetic objects that mimic the browser environment to run inside the browser?
Yes.
We have loaded the Angular library in a synthetic environment running inside the Node runtime. Most parts of the
code live in the CommonJS modules, like node_modules/benv/index.js
and its dependencies. We need
to pack everything together and create a bundle we can load inside the browser. Luckily there is a tool
for that: browserify. We will create a stand alone bundle from the code above
$ browserify test.js -o test-bundle.js
The test-bundle.js
will be huge - it will have all the code from out test.js
plus benv
, plus
its dependencies (like jsdom
), plus some parts of the Node system libraries.
Step 3 - remove parts that assume the browser environment
We cannot load the test-bundle.js
right away, unfortunately. While browserify command packed everything
and added shims to recreate the Node environment, some code inside jsdom
and its dependencies assumed Node.
For example, there are places where __filename
variable was used directly. Remember, browserify
only tried to bundle up our application that created a synthetic window for NodeJS environment.
A second obstacle to loading the test-bundle.js
in the browser was: we are NOT loading the bundle in the
browser environment (the first column of the "environments" table). Instead we are going to load this bundle
in the web worker
column. Browserify assumes that it has a window object when shimming
the NodeJS system libraries.
Luckily these parts were few and could be easily removed. For example, there were places where the CommonJS
require
call was patched over. There will be no need for require
in the web worker, thus I could easily
comment these out. My demo application also does not perform any Ajax calls, thus I could comment out the
xhrHttp
code that checked for the window
object's presence. Again. the window
and document
will
only be created inside the benv.setup(...)
callback. These objects cannot be used anywhere before
the benv.setup
call executes.
Step 4 - bootstrap Angular application and return digested HTML
The last step to show the AngularJS framework working inside a web worker is to bootstrap an angular
application and run a digest cycle. Here is a small application I placed inside the benv.setup()
callback. The synthetic document got its HTML inside the benv.setup
callback too.
1 | benv.setup(function () { |
For clarity I wrapped the application definition in the setupApp
function, and
its bootstrapping in the startApp
function.
The application has a single controller with a single two-way bound expression. I am using
two classical Angular services $scope
and $timeout
. After the first 1 second time out finishes,
the scope property title
changes. Then I let the digest run by setting another time out.
The digest cycle finds the changed scope property and updates the HTML inside the synthetic document.body
.
The callback afterDigest
runs AFTER the DOM update and grabs the rendered HTML
and sends it back to the main browser window using self.postMessage
. The demo page listens for a message
and sticks the received HTML directly into the output element.
1 | <div id="output">rendered HTML from web worker will replace this text.</div> |
That is it. Seems like the digest cycle is running, and at least some basic AngularJS services and directives
are working: $scope
, $timeout
and ng-controller
. In the future I plan to simplify and separate the
application from the wrapped framework bundle to allow any arbitrary application to execute in a web worker context.
Conclusion
I hope to use the separate Web Worker threads each running an AngularJS framework to speed up certain tasks, like loading the initial application code heavy page.