Using web workers on an AngularJs app.
So you want to implement a Web Worker on your Angular app? It turns out this can be a simple thing to do. However, depending on your requirements things can get a little bit complex. Nevertheless, Web Workers are great and we should use them more often.
Before we go into the Angular part, let's take an overview about the Web Worker implementation.
Typical worker implementation
I will assume you have reasonable knowledge about Web Workers. If so, this code will be familiar to you (if not, I suggest reading this documentation on the developer.mozilla.org site):
var myWorker = new Worker('worker.js');
The first thing that’s suddenly noticed is that the worker is declared on a dedicated file: worker.js. This wouldn't be such a big deal until you see the worker implementation:
function Worker() {
this.onmessage = function(e) {
console.log('Worker received message', e);
// do the worker stuff here
postMessage(someMessage);
}
}
Considering we want to do this on an Angular app, this file content will fall out of the common angular file which would typically declare Angular entities. Not a big deal but I would avoid this if I could.
Blobs and Blob URLs to the rescue
Blobs (Binary Large Object) are a relatively unused objects provided by the browser. You can learn more about them here, but for the sake of simplicity, let's look at them as objects that the browser can interpret as files (although they can be more than that).
var fileContent = ["function Worker() { " +
"this.onmessage = function(e) { console.log('Worker received message', e); " +
"postMessage(someMessage); } }"];
var myBlob = new Blob(fileContent, {type : "application/jasvascript"});
The Blob constructor accepts an Array with a string that represents your file's content and a second argument that specifies the MIME type of that file. What I've done here was to insert the worker's code on that argument, as a string. Wait. I'm not suggesting you to write your worker as a string. In fact, we will need to call the URL.createObjectURL(myblob) in order to dynamically create a "file like" resource. This is part of the file API. The idea is not new, and has been proposed before. However, we could go a little further:
// this is the function we would write on the worker.js file
var workerFunction = function () {
this.onmessage = function(e) {
console.log('Worker received message', e);
}
}
// this will convert the function into a string
var dataObj = '(' + workerFunction + ')();';
// this is a BlobURL that the browser interprets as a file resource
var blob = new Blob([dataObj]);
var blobURL = window.URL.createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
// and this is our worker
var myWorker = new Worker(blobURL);
Notice the part:
var dataObj = '(' + workerFunction + ')();';
This will convert the worker function into a string (including it's body). It is the same as calling:
var dataObj = '(' + workerFunction.toString() + ')();';
And the result will be:
var dataObj = "(function () {
this.onmessage = function(e) {
console.log('Worker received message', e);
}
})();"
So at this point, the function we would write inside the worker.js file can be created on the fly, converted to string and then used to create a Blob object to "simulate" a file. Once we have that, nothing stops us from creating a worker:
var myWorker = new Worker(blobURL);
So, how do I do this in Angular?
So far, this has been just javascript code written on the global scope. Good enough to understand the idea, but when we think about integrating this into Angular, we need to think about how to structure the code using the Angular "entities" (controllers, services, factories, etc...). To approach this, let's take another look at our code:
var workerFunction = function () {
this.onmessage = function(e) {
console.log('Worker received message', e);
}
}
var dataObj = '(' + workerFunction + ')();';
var blob = new Blob([dataObj]);
var blobURL = $window.URL.createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
var myWorker = new Worker(blobURL);
We could divide this code into the following:
1. Create the worker function
var workerFunction = function () {
this.onmessage = function(e) {
console.log('Worker received message', e);
}
}
2. Instantiate a worker with that worker function
var dataObj = '(' + workerFunction + ')();';
var blob = new Blob([dataObj]);
var blobURL = $window.URL.createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
var myWorker = new Worker(blobURL);
To do this we could create the following angular code:
myApp.factory('workerFunctionFactory', function () {
'use strict';
function workerFunction() {
var self = this;
self.onmessage = function(event) {
var result;
// do something with event data and assign it to result
self.postMessage(result); // return the result
}
};
return {
getWebworkerFunction: function () {
return workerFunction;
}
}
});
This code is responsible for the creation of the worker function. Its main method returns a reference to that function. This function implementation must follow the web worker API requirements (notice the onmessage method) and can then be used as an input to another factory, responsible for the web worker creation:
myApp.factory('workerFactory', function ($window) {
'use strict';
function createWorker(workerFunction) {
var dataObj = '(' + workerFunction + ')();';
var blob = new $window.Blob([dataObj.replace('"use strict";', '')]);
var blobURL = ($window.URL ? URL : webkitURL).createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
return new Worker(blobURL);
};
return {
createWorker: createWorker
}
});
The createWorker() function accepts a function as an argument. This function is then used with a Blob object to create and return the worker. This is actually a generic factory that creates a web worker from any given worker function implementation, which means it can be used to create as many web workers as you like.
A web worker example
As an example, let's create a web worker that accepts a comma separated values and returns an array with those values (you can see full code here).
Putting all this together, our angular app would look like this:
var myApp = angular.module('myApp',[]);
myApp.factory('workerFunctionFactory', function () {
'use strict';
function workerFunction() {
var self = this;
self.onmessage = function(event) {
var splitedString = event.data.split(',');
self.postMessage(splitedString);
}
};
return {
getWebworkerFunction: function () {
return workerFunction;
}
}
});
myApp.factory('workerFactory', function ($window) {
'use strict';
function createWorker(workerFunction) {
var dataObj = '(' + workerFunction + ')();';
var blob = new $window.Blob([dataObj.replace('"use strict";', '')]);
var blobURL = ($window.URL ? URL : webkitURL).createObjectURL(blob, {
type: 'application/javascript; charset=utf-8'
});
return new Worker(blobURL);
};
return {
createWorker: createWorker
}
});
myApp.controller('MyCtrl', function MyCtrl( $timeout, workerFactory, workerFunctionFactory) {
'use strict';;
var worker;
var self = this;
this.inputText = '';
this.data = '';
this.sendMessage = function () {
worker.postMessage(self.inputText)
};
function setData(data) {
self.data = data;
}
(function init() {
worker = workerFactory.createWorker(
workerFunctionFactory.getWebworkerFunction()
);
worker.onmessage = function (message) {
$timeout(function () {
setData(message.data)
}, 0);
}
}());
});
The corresponding view, would be something like this:
<body ng-app="myApp">
<div ng-controller="MyCtrl as ctrl">
<p>Type in a comma separated text, and the web worker will split and present you with the result</p>
<input ng-model="ctrl.inputText" type="text">
<button ng-click="ctrl.sendMessage()">Send to worker</button>
<ul>
<li ng-repeat="elem in ctrl.data track by $index">{{elem}}</li>
</ul>
</div>
</body>
When you click the Send to worker button, the worker will split the values and present you withalues:
What is that $timeout stuff?
If you noticed, there is a call to $timeout service:
worker.onmessage = function (message) {
$timeout(function () {
setData(message.data)
}, 0);
}
What happens is that the web worker is asynchronous. And whenever it sends data, through the onmessage() method, angular needs to do a $scope.apply so that changes are reflected on the view. Usually, you don't notice this on other common assync calls like $http for example. This is because, angular is smart enough to do a $scope.apply on those calls, but not on web workers. So in our example, we are using $timeout with a 0 value because we know angular will update the scope at the end.
So, what about unit tests?
Notice how the web worker is supposed to be declared:
function () {
this.onmessage = function(e) {
console.log('Worker received message', e);
}
}
See the this.onmessage part? This denotes that this function will be used as a constructor, i.e., with the new keyword. Yes. Plain old javascript, just like the good old days. Once this is understood, testing the worker function becomes easy:
describe('workerFunctionFactory', function () {
'use strict';
var workerFunctionFactory;
beforeEach(module('myApp'));
beforeEach(function () {
inject([
'workerFunctionFactory',
function (_workerFunctionFactory_) {
workerFunctionFactory = _workerFunctionFactory_;
}
])
});
describe('when worker function is used as a constructor', function () {
var workerFunction;
var workerFunctionInstance;
beforeEach(function() {
workerFunction = new workerFunctionFactory.getWebworkerFunction();
workerFunctionInstance = new workerFunction();
workerFunctionInstance.postMessage = jasmine.createSpy('postMessage');
});
it('should expose an onmessage method', function () {
expect(workerFunctionInstance.onmessage).toEqual(jasmine.any(Function));
});
describe('when onmessage is called with an event object', function () {
it('should call postMessage with the expected value', function () {
workerFunctionInstance.onmessage({data: '1,2,3'});
expect(workerFunctionInstance.postMessage).toHaveBeenCalledWith(['1','2','3']);
});
});
});
});
One small detail though. Of all of the web workers the public methods, it is required for the worker function to just implement one: the onmessage. All other methods, like the postMessage will be implemented by the browser. However, since there are calls to this method inside the onmessage method, we need to provide a postMessage method on our unit test:
workerFunctionInstance.postMessage = jasmine.createSpy('postMessage');
And since we're doing this, we might just provide a spied method, so that we can expect it was called:
expect(workerFunctionInstance.postMessage).toHaveBeenCalledWith(['1','2','3']);
This way, we can test the output based on the input.
Conclusion
Using the Blob approach we can easily encapsulate web worker on an angular entity and testing the worker is basically testing the worker function as a constructor.
--
If you liked this, please tell the world. If you didn't, tell me. :)
Any suggestions or comments are welcome.
Very useful article, thanks for putting it together!
It is not Angular. It is AngularJS x2
It is not Angular. It is AngularJS