In the last months, I worked on a project that required a lot of processing time when manipulating huge amounts of data in the browser. The tool is basically a JavaScript obfuscator that you can test only whenever you want. The problem is simple, if the JavaScript file that needs to be obfuscated is quite long, for example, the jQuery source code, the UI will freeze for a couple of seconds, making the application unresponsive for a while until the code is successfully obfuscated.
The solution to this issue is to process the information within another thread. This can be solved using Web Workers in JavaScript easily, however, the project is based on the Vue.js framework, so, as you may know, in order to use a worker, its code must be placed in another JavaScript file that will be requested by the Worker class, but as I'm working with this framework, I will end up with a single JavaScript file, so I had to research a little bit about this problem and I'm going to share with you my solutions to this.
A. Using a library (or your own inline WebWorker)
The first solution comes in handy when you are writing your own heavy logic, which means that you won't include scripts in the worker. The library that will help you with this in your Vue.js project is vue-worker. This library uses under the hood, the simple-web-worker package, written by @israelss. This library uses the Inline Web Workers approach. As you may know, a standard web worker requires an extra js file that will contain the code of the worker:
// New workers is created for a link to an external js file
var myWorker = new Worker('web-worker.js');
// function to fire when a message is sent from the worker.
myWorker.onmessage = function (e) {
console.log(e.data); // HELLO WORLD
}
// send a message to the worker
myWorker.postMessage('hello world');
The code of the web worker file would be:
// When the web worker receives a worker. look at the data and uppercase it, and send it back
self.onmessage = function (e) {
self.postMessage(e.data.toUpperCase());
}
For inline web workers, this isn't necessary as using a helper method like the following one to create an inline web worker:
function createInlineWorker(fn) {
let blob = new Blob(['self.onmessage = ', fn.toString()], { type: 'text/javascript' });
let url = URL.createObjectURL(blob);
return new Worker(url);
}
Would make pretty easy to implement the same logic of the original worker without needing an extra JS file:
// Note that the function scope is limited to the worker itself
// you won't be able to access any other variable from this context
// inside the worker
let myWorker = createInlineWorker(function (e) {
self.postMessage(e.data.toUpperCase());
});
myWorker.onmessage = function (e) {
console.log(e.data); // HELLO FROM AN INLINE WORKER!
};
myWorker.postMessage('hello from an inline worker!')
The VueWorker module does this in a very fancy way. Install this module with the following command:
npm install vue-worker --save
Then register the module in your main.js:
import Vue from 'vue'
import VueWorker from 'vue-worker'
Vue.use(VueWorker)
That will inject a property into Vue (and pass it to every child component), with a default name of $worker
. In your components, you will be able to use the module like this. The logic of the workers with this module is a little bit different as the first option that you have is to run a plain function that behaves like a JavaScript promise, however its code runs within another thread:
this.$worker.run(function(){
// The logic inside this function runs in a separate thread from your application
return 'this.$worker run 1: Function in other thread';
}).then(function(result){
// Outputs: 'this.$worker run 1: Function in other thread'
console.log(result);
}).catch(function(error){
// log any possible error
console.log(error);
});
If you need to pass some arguments:
this.$worker.run(function(arg1, arg2){
// The logic inside this function runs in a separate thread from your application
return `this.$worker run 2: ${arg1} and ${arg2}`;
}, ["This is argument #1", "This is argument #2"]).then(function(result){
// Outputs: 'this.$worker run 2: This is argument #1 and This is argument #2
console.log(result);
}).catch(function(error){
// log any possible error
console.log(error);
});
The library is quite easy to use in case you don't want to create your own inline web worker.
B. Using workers in an old-fashioned way
If you need to import scripts, the inline web worker implementation won't be an option for you, just exactly as it was in my case. So, the way in which it will work as usual, is to create a regular web worker. All you need to care about is to provide the proper URL of the worker and it should work:
// App.vue
// Load the worker file from a web URL (in this case we use the public directory)
let regularWorker = new Worker("/workers/example.worker.js");
regularWorker.onmessage = function(e) {
// Should output: The sum of the numbers is 15
console.log(`The sum of the numbers is ${e.data.result}`);
};
regularWorker.postMessage({
"numbers": [1,2,3,4,5]
});
The code in this case of the example.worker.js
file that is stored under public/workers/example.worker.js
is the following one:
// public/workers/example.worker.js
// Listen to every message posted from the Worker initializator
self.addEventListener('message', function(MessageEvent) {
// Contains the sum of the numbers in the given array
let result = MessageEvent.data.numbers.reduce((a, b) => a + b, 0);
self.postMessage({
"result": result
})
console.log(MessageEvent);
}, false);
If you run the mentioned code in your Vue.js application, it should work as expected using the new Web Worker that you can use to run some heavy work.
Using a helper
With this helper, you should be able to work with the worker easier as it contains a pretty easy syntax that you can use in most of the cases where is needed. We'll use the mentioned JavaScript obfuscator example that should work out of the box in your project as it uses a CDN to load the library. Create the WorkerHelper
class in your Vue.js project that will contain the following code:
// WorkerHelper.js
export default class WorkerHelper
{
/**
* The constructor expects as first argument the URL of the Worker file.
*
* @param {String} scriptPath
*/
constructor(scriptPath)
{
let _this = this;
this.worker = new Worker(scriptPath);
this.events = {};
this.worker.addEventListener('message', function(e) {
if(Object.prototype.hasOwnProperty.call(_this.events, e.data.event)) {
_this.events[e.data.event].call(null, e.data.data);
}
}, false);
this.worker.addEventListener('error', function(e) {
if(Object.prototype.hasOwnProperty.call(_this.events, "error")) {
_this.events["error"].call(null, e);
}
}, false);
}
/**
* Returns the original WebWorker instance.
*
* @returns {Worker}
*/
getWorker(){
return this.worker;
}
/**
* Triggers an event in the WebWorker.
*
* @param {*} eventName
* @param {*} data
*/
trigger(eventName, data)
{
this.worker.postMessage({
"eventName": eventName,
"data": data
});
}
/**
* Responds to an event from the web worker.
*
* @param {*} eventName
* @param {*} callback
*/
on(eventName, callback)
{
this.events[eventName] = callback;
}
}
Now create the worker that will use the obfuscation library and will communicate with the main thread using an eventName that we will assign later:
// public/workers/example.worker.js
// 1. Import a script, in this case the JavaScript Obfuscator library
self.importScripts("https://cdn.jsdelivr.net/npm/javascript-obfuscator@latest/dist/index.browser.min.js");
// 2. Listen to every message posted from the Worker initializator
self.addEventListener('message', function(MessageEvent) {
switch(MessageEvent.data.eventName){
// 3. React to the `obfuscate` event
case "obfuscate":
// 4. arguments will contain the object that you pass as second argument to the WorkerHelper.trigger method
let arguments = MessageEvent.data.data;
// 5. Run the logic that takes a lot of time to execute, in this case, the JavaScript obfuscation
let obfuscationResult = JavaScriptObfuscator.obfuscate(arguments.code);
// 6. Send the obfuscated code to the main thread with the identifier of this event (obfuscate)
self.postMessage({
"event": MessageEvent.data.eventName,
"data": {
"code": obfuscationResult.getObfuscatedCode()
}
});
break;
}
}, false);
Now, inside your Vue.js application, use the following logic to use the worker:
// 1. Import the created file
import WorkerHelper from './WorkerHelper';
// 2. Create the worker helper instance
let worker = new WorkerHelper("/workers/example.worker.js");
// 3. Run the code that responds to the `obfuscate` event name in the worker
// passing an object as argument
worker.trigger("obfuscate", {
"code": `
let name = 'Carlos';
`
});
// 4. Attach the listener that reacts to the obfuscate event triggered by the worker
worker.on("obfuscate", function(data){
// Will display an object with the response of the worker:
// e.g. {code: "const _0x3773=['3617462yGjhHE','537744gbuLjY','964…;}}}(_0x3773,0xb9d46));let name=_0x3ee9be(0x1ab);"}
console.log(data);
});
// 5. Listen in case of errors
worker.on("error", function(err){
console.log(err);
});
Happy coding ❤️!