📜 ⬆️ ⬇️

WXHR: Good old XHR with Web Workers

There are situations when the web application needs to pick up a lot of data from the server, decode it and send it further to its destination. An example of this could be an online 3d editor, where each model can occupy several megabytes in gzipan json'e.

What to do when the browser of the average user hangs for a second or even more when loading and unpacking data?
1. Come up with something on flash (I'm not 100% sure, but some browsers run plugins in the main thread)
2. Download data in chunks, process chunks.
3. Ask the user to upgrade the computer.

All 3 options are not very, right?
')
Under the cut, an elegant solution (without unnecessary scripts and writing application code) of this problem.

We get help from web workers who, fortunately, have a xhr interface inside.

We need to salt the good old XHR workers, but all we need to do is so that the scripts that use the old version of XHR did not notice the substitution. And the old browsers, without the support of workers, would work as before.
I will take the xhr script from the Pro JavaScript Design Patterns book (7.03 - XHR factory example) as a basis.
Our xhr script should work in 3 modes: xhr for old browsers, wxhr host, wxhr worker.

The logic of work will be as follows:
0. User performs xhr.request
1. If the browser does not keep workers, then we work in the old way.
2. If the browser holds something, in the request method, call the worker (wxhr.js) and do not execute the request,
2.1 We hang the appropriate events to the worker, we proxify the request to the worker via postMessage
3. The wxhr.js script runs as a worker, the script understands that it is now working in worker mode and hangs a message event
3.1 The script receives the request,
3.2 Creates a normal xhr object,
3.3 Performs the usual xhr.request,
3.4 Process data, transfer data down to its host,
3.5 The host performs a callback with the data that the worker processed.

Patched xhr aka wxhr:
/** * @fileOverview WXHR Request - Web Worker XHR * * @example * <pre> * var myHandler = new global.xhr(true), // enable workers * myHandler2 = new global.xhr(), // worker mode is disabled by default * data = { * method: 'GET', * url: 'test.txt', * success: function (data, isWorker) { * alert(data + (isWorker ? ' XHR called from Worker' : ' XHR called from Window')); * }, * error: function (status) { * alert(status); * } * }; * * myHandler.request(data); * myHandler2.request(data); * </pre> * * @author azproduction */ /**#nocode+*/ (function (global) { /**#nocode-*/ // Upgrade 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - var xhr = function (canUseWorkers) { this.canUseWorkers = (typeof canUseWorkers === 'undefined') ? false : !!canUseWorkers; }, // detect workers support workersSupported = typeof global.Worker !== 'undefined', // detect mode itIsWindow = typeof global.document !== 'undefined'; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - xhr.prototype = { request: function(params) { params.method = params.method.toUpperCase() || 'GET'; params.url = params.url || ''; params.postVars = params.method !== 'POST' ? (params.postVars || null) : null; params.success = params.success || function () {}; params.error = params.error || function () {}; // Upgrade 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (this.canUseWorkers && workersSupported && itIsWindow) { // use advanced wxhr var worker; // this is bad part mb createObjectURL will save in future worker = new global.Worker('wxhr.js'); // <<< bad worker.onmessage = function(e) { var data = e.data; // proxy response // @todo delete true parameter in Production! params[data.callback](data.data, true); }; // if worker throws error query fails worker.error = function(e) { params.error(0); }; // worker proxy request worker.postMessage({ method: params.method, url: params.url, postVars: params.postVars }); return; } // browser do not support workers or script is already works as Worker // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var xhr = (this.createXhrObject())(), self = this; xhr.onreadystatechange = function() { try { if (xhr.readyState !== 4) { return; } self.processResponse(params, { status: xhr.status, responseText: xhr.responseText, contentType: xhr.getResponseHeader('Content-type') }); } catch (e) { params.error(xhr.status); } }; xhr.open(params.method, params.url, true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(params.postVars); }, createXhrObject: function() { // Factory method. var methods = [ function() { return new XMLHttpRequest(); }, function() { return new ActiveXObject('Msxml2.XMLHTTP'); }, function() { return new ActiveXObject('Microsoft.XMLHTTP'); } ]; for (var i = 0, len = methods.length; i < len; i++) { try { methods[i](); } catch(e) { continue; } // If we reach this point, method[i] worked. this.createXhrObject = methods[i]; // Memoize the method. return methods[i]; } // If we reach this point, none of the methods worked. throw new Error('SimpleHandler: Could not create an XHR object.'); }, processResponse: function (params, xhr) { if (xhr.status === 200) { if (xhr.contentType.match(/^application\/json/)) { params.success(JSON.parse(xhr.responseText)); } else { params.success(xhr.responseText); } } else { params.error(xhr.status); } } }; // Upgrade 3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if (!itIsWindow) { // worker mode: listen for requests global.addEventListener('message', function(e) { var data = e.data; // proxy success data.success = function (data) { global.postMessage({ callback: 'success', data: data }); }; // proxy error data.error = function (status) { global.postMessage({ callback: 'error', data: status }); }; var xhrRequest = new xhr(); xhrRequest.request(data); }, false); } else { // script mode: export xhr global.xhr = xhr; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // global.xhr = xhr; /**#nocode+*/ }(this)); /**#nocode-*/ 

Work example: azproduction.ru/wxhr

I do not advise using wxhr anywhere, it is necessary only in cases when processing a large amount of input data is needed, in other cases it will always be inferior to xhr from the main stream (spooked workers, double data transfer). In some browsers, in particular in safari, postMessage encodes the data in json before sending it, and decodes it when received. So, that can turn out even worse than with the usual xhr.

I ran the test: I ran 2 identical requests, bypassing the cache, first sent wxhr to the second xhr. In chrome, ff and safari in 100% of cases the second came wxhr, in the opera in 75% the second was wxhr. Data 6 bytes + headers.

Source: https://habr.com/ru/post/110712/


All Articles