I wanted to post something in Daniweb for a while but I didn't had anything new. Recently I decided to write a new implementation for websocket connections client side using Shared Web Workers. Shared Web Workers are supported today by most browsers for desktop but few for mobile devices , so it had to have a fallover mechanism to Dedicated Web Workers. I searched but I didn't found an example , so I am posting it here. Moreover since we will have a worker not in main thread it would be great to have the option for callback functions for other uses than WebSockets. I implemented these without any framework to keep it as simple as possible and here it is an example of this for something common and useful , lazy loading images . It needs to be modified if you are going to use it to be adjusted to your needs and to add error handling and arguments checking at least.
We are going to user five files in a folder. Two images: one imageExample.jpg that is going to be the image to lazy load and a transparent1x1.png that is a transparent image 1px x 1px that we will use as placeholder src for the image before is loaded.
Lets start with the index.php (I am using PHP here but you can use any language)
<?php
/*
getting the root URL of the project is beyond the scope of this ,
but here is a quick implementation
*/
$v = $_SERVER["SERVER_PROTOCOL"] ;
$v = mb_strtolower(mb_substr($v, 0 , mb_strrpos($v, "/")));
if(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on")
{
$v .= "s";
}
$root = $v."://".$_SERVER['HTTP_HOST'];
$v = $_SERVER["PHP_SELF"];
$v = mb_substr($v, 0 , mb_strrpos($v, "/"));
$root .= $v;
?>
<html>
<head>
<title>
JavaScript Shared Web Worker with Dedicated Web Worker as failover
- Lazy Loading Images example
</title>
<style>
/* IntersectionObserver works better with blocking elements to determine if are
intersected with the viewport */
picture,img
{
display: inline-block;
vertical-align: middle;
}
</style>
</head>
<body>
<!-- all the img tags inside picture elements will be loaded with the
src of the noscript img tag when they appear on screen -->
<picture>
<noscript>
<img src="<?=$root?>/imageExample.jpg" alt="example image" width="800" height="500" />
</noscript>
<img src="<?=$root?>/transparent1x1.png" alt="example image" width="800" height="500" />
</picture>
<script type="text/javascript">
/*
load the example.js after DOMContentLoaded or if that doesn't fired on window.onload
and then call the init of the example object that instantiated there
*/
var _fncl=false;
_fnc=function()
{
_fncl=true;
var s = document.createElement("script");
s.async=true;
s.src="<?=$root?>/example.js?"+<?=date("U")?>;
s.addEventListener("load", function(e)
{
_.init("<?=$root?>");
}, false);
document.body.appendChild(s);
};
window.addEventListener("DOMContentLoaded", _fnc);
window.onload=function(){if(!_fncl){_fnc();}};
</script>
</body>
</html>
As you can see it is very simple , now lets move to the main JavaScript file that will be loaded the example.js
/*
this is an example of how the concept works it doesn't have error handling
or checking arguments for their correct type or even existence ,
in real life projects those should implemented
*/
const _ = new (function() // _ object holds the core functionalities
{
var _this = this;
var worker = null;
var shared = false;
var privateRoot = null;
var index = 0;
var works = {};
this.init = function(root)
{
privateRoot = root;
shared = !!window.SharedWorker; // if Shared Web Workers are supported
//use Dedicated Web Worker when developing new methods
// in order to view the errors in console
//shared = false;
setWorker();
app.init(); // ready to init the app object
}
this.root = function()
{
return privateRoot;
};
this.getWorkKey = function()
{
index++;
return "w"+index;
};
/*
request a work from the worker with callback function
dataObj is the object that will be send to the worker
, it must contains at least an action that will be a function of the worker
and any other data we might use there
fnc is the the callback function example: function(responseDataObj,argObj){}
argObj contains all other data we might need along with the response data
*/
this.work = function(dataObj,fnc,argsObj)
{
let wKey = _this.getWorkKey();
dataObj.wKey = wKey;
works[wKey] = [fnc,argsObj];
worker.postMessage(dataObj);
};
function setWorker()
{
if(shared)// instantiate the worker
{
let w = new SharedWorker(_this.root()+"/worker.js");
worker = w.port;
worker.start();
}
else
{
worker = new Worker(_this.root()+"/worker.js");
}
var handler = function(event)//handle incoming events from worker
{
let data = event.data;
if("n" in data && data.n == "u") // pong (ping response) that this page is still alive
{
worker.postMessage({n:"y"});
}
else if("log" in data) // let the worker send messages to window console
{
window.console.log(data.log);
//worker.postMessage({n:"y"});
}
else if(data.wKey in works)
{
let arr = works[data.wKey]; // the array that is store in works object
// that has as first element the function and second the arguements object
arr[0](data,arr[1]); // call the callback function
works[data.wKey] = null;
delete works[data.wKey]; // try to remove it from memory when gc will pass
}
};
if(shared) // attach the handler to the worker
{
worker.onmessage = (event) => {
handler(event);
};
}
else
{
worker.addEventListener('message', event =>
{
handler(event);
});
}
}
})();
const app = new (function() //app object has specific code for this app
{
var _this = this;
this.lazyLoadObjerver = null;
this.init = function()
{
setIntersectionObserver();
};
function setIntersectionObserver()
{
if ("IntersectionObserver" in window)
{
_this.lazyLoadObjerver = new IntersectionObserver(_this.lazyLoader);
}
_this.lazyLoad();
}
this.lazyLoad = function()
{
const els = document.getElementsByTagName("picture");
for (const el of els)
{
if(_this.lazyLoadObjerver == null) // if the browser doesn't support IntersectionObserver
{
const src = el.querySelector("noscript").innerHTML
.match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
el.querySelector("img").setAttribute("src",src);
}
else
{
_this.lazyLoadObjerver.observe(el);
}
}
};
this.lazyLoader = function(entries, observer)
{
entries.forEach(function(entry)
{
const el = entry.target;
if (entry.isIntersecting)
{
if(el.getAttribute("class") != "lz")
{
const src = el.querySelector("noscript").innerHTML
.match(/.*src=["']([^"]*)['"].*/)[1];// get src of the image
// data object for the worker request
const dataObj = {action:"loadBlob",url:src};
// arguements object for the callback function
const argObj = {el:el};
// the callback function;
const fnc = function(data,arg){
const img = arg.el.querySelector("img");
arg.el.setAttribute("class","lz");
img.setAttribute("src",URL.createObjectURL(data.blob));
};
_.work(dataObj,fnc,argObj);
}
}
});
};
})();
And finally the worker.js file , it worth mentioning that if you are going to use Shared Web Workers I couldn't find any other way then a separate file.
(I am adding it as code snippet because without code snippet it doesn't allow me to post)
I hope to enjoy it , I added some comments inside the code but feel free to ask anything related. Also any other ideas would be welcomed