Pull to refresh

Настоящие online, offline события

Reading time 5 min
Views 3.6K
С появлением online, offline событий многие разработчики, особенно мобильных веб-севисов возложили на них большие надежды. Казалось бы online, offline говорят нам когда у пользователя есть доступ к интернету, но на самом деле это далеко не так. Подробности их поведения когда-то давно описал Резиг в своем блоге.



Кратко — online, offline сигнализирует нам, что пользователь вручную переключился в оффлайн либо у него нет ни одного соединения с сетью. Фактически эти 2 события бесполезны в том виде в котором они представлены — я не знаю кто будет вручную переключать таб в режим онлайн/оффлайн, и с сетевыми подключениями тоже все плохо. Ну и, конечно, доисторические бразуеры не знают эти события.

Под катом элегантное и 100% кросбраузерное решение, позволяющее получить настоящие online, offline события.

Нам нужно получить события, проверяющие без лишних затрат наличие интерента у пользователя, точнее соединение до нашего веб-сервиса.

Алгоритм (я назвал его LIP — Long Ping Image):
1. Создаем картинку через new Image
2. Вешаем на неё onload onerror
3. Прописываем путь до long polling ресурса нашего ping сервера
4. Браузер устанавливает висящее соединение с сервером, если соединение было по каким-либо причинам сброшено — упал инет, http 50x, то сработает событие onerror. Тут мы создаем ещё одну картинку, на сей раз «быструю», чтобы удостовериться на 100% в том, что сервис оффлайн. Если у этой картинки сработал onerror значит сервис точно оффлайн. Через определенный интервал пытаемся поднять картиночное ping соединение.

Решение абсолютно кроссбраузерно и кроссдоменно. Реагирование на offline 2-4 секунды, реагирование на online 0-15 секунд.

Проблемы которые были замечены:
1. Опера как новогодняя елка всем чем можно предательски сигнализирует пользователю, что «Загрузка...» во время длинного картиночного соединения — это починить нереально (пробовал iframe, css url, sse) и вечная «Загрузка...» напрягает. Для Оперы длинное соединение не устанавливается, а просто через определенный интервал опрашивается «быстрая» картинка — не так оперативно, немного затратно, но ничего не поделать.
2. Пользователи ФФ могут убить длинное соединение, нажав Esc при загрузке страницы — было пофиксено preventDefault'ом.
3. При физическом отключении интернета (выдернул шнур) все браузеры не сбрасывают висящее соединение, поэтому оффлайн не приходит.

Достоинства:
1. Покрытие всех браузеров, даже самых древних
2. Быстрое реагирование на offline/online
3. Можно подкрутить nginx для выполнения функции ping-сервера и получить мизерные издержки на стороне сервера.

Недостатки:
1. Небольшая утечка траффика около 1Кб в минуту (с возможностью сократить издержки до 1кб за сеанс)
2. Необходимость поднятия Пинг сервера
3. Большое количество висящих соединений
4. Если пинг сервер находится на том же домене, что и основной сервер, то мы занимаем 1 из 4 возможных http соединений.

Код


Это все можно пролистать, архив с исходниками и пример ниже.

ping.php — наш ping-сервер
<?php
isset($_GET['long']) && sleep(55);
  
header("Content-type: image/gif");
header("Expires: Wed, 11 Nov 1998 11:11:11 GMT");
header("Cache-Control: no-cache");
header("Cache-Control: must-revalidate"); 
    
// 1x1 gif
printf("%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c" . 
"%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c",
71,73,70,56,57,97,1,0,1,0,128,255,0,192,192,192,0,0,0,33,
249,4,1,0,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59);

?>


Есть ещё вариант через nginx HttpEmptyGifModule
location = /_.gif {
  empty_gif;
}

Как реализовать длинный запрос на nginx я без понятия, поэтому приложил вариант на ПХП.

Ping.js
/**
 * @fileOveriew Long Polling Image Ping
 */
  
(function (window, Image) {
    
    /**
     * Long Polling Image Ping
     * 
     * Way to detect user's inernet connection       
     */         
    var Lpip = {
        BASE_URL: 'ping.php',
        /**
         * Long polling request URL. Can be crossdomain         
         */                          
        LONG_POLLING_IMAGE_URL: '?long',
        /**
         * Common image request. Can be crossdomain
         */                 
        IMAGE_URL: '',
        RECONNECT_TIMEOUT: 15000,

        _image: null,
        _stage: 2,
        _makeImage: function (url) {
            var image = new Image();
            image.onload = Lpip.onImageLoad;
            image.onerror = Lpip.onImageError;
            image.src = url + (url.match(/\?/) ? '&' : '?') + Math.random();
            
            return Lpip._image = image;
        },

        /**
         * @type Boolean
         */
        online: false,
        
        /**
         * @type Boolean
         */
        offline: false,

        /**
         * Long polling image request
         */                 
        watch: function () {
            // Opera fix
            if (window.opera) {
                window.setTimeout(function () {
                    Lpip._makeImage(Lpip.BASE_URL + Lpip.IMAGE_URL);
                }, Lpip.RECONNECT_TIMEOUT);
                return;
            }
            
            // For other browsers 
            Lpip._makeImage(Lpip.BASE_URL + Lpip.LONG_POLLING_IMAGE_URL);
        },
        
        /**
         * Quick image request
         */                 
        quick: function () {
            Lpip._makeImage(Lpip.BASE_URL + Lpip.IMAGE_URL);
        },

        onImageLoad: function () {
            if (!this.width) {
                // Error
                Lpip.onImageError.call(this);
            } else {
                // Image ok
                if (Lpip._stage > 1) {
                    // Internet connection up!
                    Lpip.onConnectionUp();
                    Lpip.offline = !(Lpip.online = true);
                }

                // Reset errors
                Lpip._stage = 0;

                // Continue requesting
                Lpip.watch();
            }
        },

        onImageError: function () {
            Lpip._stage += 1;
            if (Lpip._stage > 1) {
                if (Lpip._stage === 2) {
                    // User's internet connection down...
                    Lpip.onConnectionDown();
                    Lpip.offline = !(Lpip.online = false);
                }
                
                // Try reconnect
                window.setTimeout(Lpip.quick, Lpip.RECONNECT_TIMEOUT);
            } else {
                
                // Maybe long polling request aborts for some resons
                // Try to get "quick image"
                Lpip.quick();
            }
        },

        onConnectionUp: function () {
            window.console && window.console.log('onConnectionUp');
        },

        onConnectionDown: function () {
            window.console && window.console.log('onConnectionDown');
        }
    };
    
    // Exporting Lpip         
    window.Ping = {
        /**
         * Inits Ping
         *
         * @param {Object} [options]
         * @param {Number} [options.reconnectTimeout=15000]
         * @param {String} [options.baseUrl='ping.php']
         */
        init: function (options) {
            Lpip.RECONNECT_TIMEOUT = options.reconnectTimeout || Lpip.RECONNECT_TIMEOUT;
            Lpip.BASE_URL = options.baseUrl || Lpip.BASE_URL;

            // User can cancel long polling request by pressing Esc button in Firefox or Opera
            if (window.addEventListener) {
                window.document.addEventListener('keypress', function (event) {
                    if (event.keyCode === 27) {
                        event.preventDefault();    
                    }
                }, false);
            }
            Lpip.quick();
        },
        /**
         * Connection up event helper
         *
         * Supports only one listener
         *
         * @param {Function} callback
         */
        connectionUp: function (callback) {
            Lpip.onConnectionUp = callback;
        },
        /**
         * Connection up event helper
         *
         * Supports only one listener
         *
         * @param {Function} callback
         */
        connectionDown: function (callback) {
            Lpip.onConnectionDown = callback;
        },
        /**
         * @returns {Boolean}
         */
        isOnline: function () {
            return Lpip.online;
        },
        /**
         * @returns {Boolean}
         */
        isOffline: function () {
            return Lpip.offline;
        }
    };
    
}(this, this.Image));


Пример использования

<body>
<button onclick="checkConnectionStatus()">Connection Status</button>
<pre id="log"></pre>
<script type="text/javascript" src="Ping.js"></script>
<script type="text/javascript">
(function (window, Ping, log) {
    log.innerHTML += 'Lpip is watching...<br/>';

    Ping.connectionUp(function () {
        log.innerHTML += 'Connection Up<br/>';
    });

    Ping.connectionDown(function () {
        log.innerHTML += 'Connection Down<br/>';
    });

    // call on window.onload to prevent "loading... status"
    window.onload = function () {
        Ping.init({
            'baseUrl': '/lpip/ping.php',
            'reconnectTimeout': 15000
        });
    };

    window.checkConnectionStatus = function () {
        window.alert(Ping.isOnline() ? 'Online' : 'Offline');
    }
}(this, this.Ping, this.document.getElementById('log')));
</script>
</body>


Живой пример: azproduction.ru/lpip (Прошу долго не проверять)
Исходник: narod.ru/disk/6061703001/Ping.rar.html

Критика, предложения приветствуются. Буду рад если кто-нибудь напишет вариант ping-сервера под nginx.
Tags:
Hubs:
+28
Comments 40
Comments Comments 40

Articles