Pull to refresh

WXHR: старый добрый XHR со вкусом Web Workers

Reading time 4 min
Views 2.6K
Бывают ситуации, когда веб-приложению требуется поднять кучу данных с сервера, раскодировать их и отправить дальше по назначению. Примером этому может быть онлайн 3d редактор, где каждая модель может занимать несколько мегабайт в gzip'аном json'e.

Что же делать когда браузер среднего пользователя подвисает на секунду или даже больше при загрузке и распаковке данных?
1. Придумать что-нибудь на flash (я не уверен на 100%, но некоторые браузеры запускают плагины в основном потоке)
2. Загружать данные кусками, обрабатывать кусками.
3. Попросить пользователя сделать апгрэйд компьютера.

Все 3 варианта не очень, правда?

Под катом элегантное решение (без лишних скриптов и дописывания кода приложения) этой проблемы.

Нам на помощь приходят веб воркеры, которые, благо, имеют xhr интерфейс внутри.

Нам необходимо подсолить старый добрый XHR воркерами, но все нужно сделать это так, чтобы скрипты, которые используют старую версию XHR не заметили подмену. А старые браузеры, без поддержки воркеров, работали бы как и раньше.
За основу я возьму скрипт xhr из книги Pro JavaScript Design Patterns (7.03 — XHR factory example).
Наш xhr скрипт должен работать в 3-х режимах: xhr для старых браузеров, хост wxhr, воркер wxhr.

Логика работы будет следующая:
0. Пользователь выполняет xhr.request
1. Если браузер не держит воркеров, то работаем по старому
2. Если браузер держит то, в методе request спавним воркера (wxhr.js) и не выполняем запрос,
2.1 Вешаем воркеру соответствующие события, проксируем воркеру запрос через postMessage
3. Запускается скрипт wxhr.js в качестве воркера, скрипт понимает, что он сейчас работает в режиме воркера и вешает message событие
3.1 Скрипт получает запрос,
3.2 Создает обычный xhr объект,
3.3 Выполняет обычный xhr.request,
3.4 Обрабатывает данные, передает данные вниз своему хосту,
3.5 Хост выполняет callback'и с данными, которые обработал воркер.

Патченный xhr он же 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-*/

Пример работы: azproduction.ru/wxhr

Я не советую использовать wxhr где попало, он необходим только в тех случаях, когда необходима обработка большого объема входных данных, в остальных случаях он будет всегда уступать xhr'у из основного потока (спавн воркеров, двойная пересылка данных). В некоторых браузерах, в частности в сафари, postMessage перед отправкой кодирует данные в json, а при получении декодирует. Так, что может получиться ещё хуже, чем с обычным xhr.

Провел тест: запускал 2 одинаковых запроса, минуя кэш, первым слал wxhr вторым xhr. В хроме, фф и сафари в 100% случаях вторым приходил wxhr, в опере в 75% второй был wxhr. Данные 6 байт + заголовки.
Tags:
Hubs:
+21
Comments 4
Comments Comments 4

Articles