Повторно используемый кэширующий прокси на JavaScript

Проблема


Ни для кого не секрет, что производительность и по сей день остается одним из основных показателей качества веб-приложения. И, конечно, любой веб-разработчик провел не один час, оптимизируя свое приложение и добиваясь приемлемой производительности, как на серверной, так и на клиентской стороне. Несмотря на то, что аппаратное обеспечение день ото дня становится все мощнее и мощнее, всегда находятся узкие места, которые бывает непросто обойти. С приходом AJAX, HTTP запросы стали «мельче» по объему получаемых на клиента данных, но их количество увеличилось. Каналы связи могут быть достаточно широкими, а вот время соединения и процесс формирования ответа на сервере могут занимать значительное время. Кэширование результатов запросов на клиенте может значительно повысить общую производительность. Не смотря на то, что кэширование может быть настроено на уровне HTTP протокола, часто оно не удовлетворяет реальным требованиям.

Задача


Наша система клиентского кэширования должна удовлетворять следующим требованиям:
  1. Возможность реализовать логику управления кэшем любой сложности;
  2. Возможность повторного использования в разных приложениях;
  3. Возможность прозрачного встраивания в существующее приложение;
  4. Независимость от типа данных и способа их получения;
  5. Независимость от способа хранения закешированных данных;

Существующее приложение


Допустим, у нас уже есть работающее приложение, которое использует jQuery для получения разметки или данных с сервера через AJAX:

function myApp() {
    this.doMyAjax = function (settings) {
        settings.method = 'get';
        settings.error = function (jqXHR, textStatus, errorThrown) {
            //handle error here
        }
        $.ajax(settings);
    }
    this.myServerDataAccess = function() {
        doMyAjax({
            url: 'myUrl',
            success: function (data, textStatus, jqXHR) {
                //handle response here
            }
        });
    }
}


Где-то мы вызываем метод, который обращается за данными:

var app = new myApp();
app.myServerDataAccess();


Кэширующий слой


Реализуем простейший кэширующий слой, который будет состоять из прокси, управляющего доступом к данным, и кэша.

Интерфейс, который будем проксировать, состоит из единственного метода getData. Полностью прозрачный прокси просто делегирует вызов своему источнику данных, используя тот же самый интерфейс:

function cacheProxy(source) {
    var source = source;
    this.getData = function (request, success, fail) {
        source.getData(request, success, fail);
    }
}


Добавим немного логики для доступа к кэшу, который реализуем чуть позже:

function cacheProxy(source, useLocalStorage) {
    var source = source;
    var cache = new localCache(useLocalStorage);
    this.getData = function (request, success, fail) {
        var fromCache = cache.get(request.id);
        if (fromCache !== null) {
            success(fromCache);
        }
        else {
            source.getData(request, function (result) {
                cache.set(request.id, result);
                success(result);
            }, fail);
        }
    }
}


При попытке получить данные, прокси проверяет их наличие в кэше и отдает, если они там есть. Если их нет, то он получает их, используя источник, помещает в кэш и отдает инициатору запроса.

Реализуем кэш, с возможностью помещения данных в Local Storage:

function localCache(useLocalStorage) {
    var data = useLocalStorage ? window.localStorage || {} : {};
    this.get = function (key) {
        if (key in data) {
            return JSON.parse(data[key]);
        }
        return null;
    }
    this.set = function (key, value) {
        if (typeof (key) != 'string') {
            throw 'Key must be of string type.';
        }
        if (value === null || typeof (value) == 'undefined') {
            throw 'Unexpected value type';
        }
        data[key] = JSON.stringify(value);
    }
}


Данные хранятся в виде пар ключ/сериализованное значение кэшируемых данных.

Интеграция в существующее приложение


Как видим, интерфейсы доступа к данным в существующем приложении и в полученном прокси отличаются (не подумайте, что это попытка усложнить себе жизнь, мы это сделали умышленно в демонстрационных целях). Для интеграции достаточно написать адаптер, реализующий проксируемый интерфейс, и применить его:

function applyCacheProxyToMyApp(app) {
    var app = app;
    app.old_doMyAjax = app.doMyAjax;
    var proxy = new cacheProxy(this, true);
    app.doMyAjax = function (settings) {
        proxy.getData({
            id: settings.url
        },             
        settings.success,
        settings.error);
    }
    this.getData = function (request, success, fail) {
        app.old_doMyAjax({
            url: request.id,
            success: success,
            error: fail
        });
    }
}
var patch = new applyCacheProxyToMyApp(app);


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

Бонус


Полученный кэширующий слой легко применить, например, к ресурсоемким повторяющимся операциям:

function complicatedStuff(a, b) {
    return a * b;
}

function complicatedStuffAdapter(complicatedStuff) {
    var proxy = new cacheProxy(this, true);
    var source = complicatedStuff;
    this.complicatedStuff = function (a, b) {
        var result;
        proxy.getData({id: a.toString() + '_' + b,  a: a,  b: b},
        function(res) { result = res; });
        return result;
    }
    this.getData = function (request, success, fail) {
        success(source(request.a, request.b));
    }
}
var p = new complicatedStuffAdapter(complicatedStuff);

function test() {
    alert(p.complicatedStuff(4, 5));
}


В заключение


Мы рассмотрели лишь подход к проксированию любых операций, которые выполняют ваши приложения. Вариантов применения – множество. От логирования, до реализации сложных аспектно-ориентированных алгоритмов, все зависит от ваших потребностей и фантазии.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 26

    0
    Интересный подход. А разве свойство $.ajax cache не этим же занимается? Или браузер как-то не так кеширует аякс запросы? И что по поводу ограничения на размер local storage?
      0
      На работу $.ajax cache влияют заголовки ответа сервера. Сервер может запрещать кэширование ответа.

      Например PHP при включении сессий отправляет заголовки:
      Expires Thu, 19 Nov 1981 08:52:00 GMT
      Cache-Control no-store, no-cache, must-revalidate, post-check=0, pre-check=0
      Pragma no-cache
        0
        Наверника это можно как-нибудь настраивать. Кэш может быть полезен для специального рода случаев, но для стандартных ресурсов я бы по возможности перекладывал все на браузер.
          0
          Пардон, но о чём вы? Какие специальные случаи? Вы читали топик?

          > Не смотря на то, что кэширование может быть настроено на уровне HTTP протокола,
          > часто оно не удовлетворяет реальным требованиям.

          Автор знает про http кэш, но ставит себе задачу кэшировать _клиентом_. Именно в это суть топика.

          Кто-то в javascript видео декодер делает, кто-то эмулятор для запуска linux, а автор изобретает «повторно используемый кэширующий прокси».

          Извращение это или нет — тема для отдельного разговора. Я лично не вижу ничего плохого в обдуманном использовании предложенного решения.
            0
            Я к тому что если нельзя использовать кэш браузера только из-за заголовков PHP как вы писали в своем комментарии выше, то лучше настроить заголовки которые выдает PHP, а не реализовывать свой кэш на JavaScript.

            Вы зачем-то доказываете мне что могут быть случаи когда это решение будет лучше, но я же с этим и не спорю. Но не тогда когда на сервере неправильно выставлены настройки кэширования (при условии что есть доступ к настройкам сервера).
              0
              Думаю мне стоит оговорится, что предложенное решение не панацея от всех бед, а лишь несколько паттернов, для которых можно найти как правильное, так и неправильное применение. И здесь видимо тот случай, когда «незнание законов не освобождает от ответственности».
        +1
        Во-первых, если в Вашем приложении простой алгоритм кэширования, например нужно обновлять данные раз в день, то естественно это лучше реализовать на уровне HTTP. Во-вторых, как было сказано в статье, решение, позволяет кэшировать не только результаты HTTP запросов. По-поводу размера local storage, логика чистки может быть реализована в классе localCache. В простейшем варианте это может быть полный сброс кэша при достижении лимита, в более сложных — отслеживание частоты запросов или любой другой алгоритм на Ваше усмотрение.
          0
          Я просто не пойму сути кеширования на клиенте… Я понимаю сценарий когда на сервер идут 1000 запросов с разных клиентов, тогда мы на стороне сервера, при первом запросе, кешируем, а всем остальным 999 клиентам отдаем закешированные данные. А сценарий с кешированием на стороне клиента мне немного понятен. Т.е. получается, что клиент сам инициирует запрос и сам же его кеширует. Где логика?
            +2
            Кэширование на сервере означает необходимость пойти на сервер и спросить либо результат запроса (если он закэширован на сервере), либо информацию о том что данные не менялись и можно показать то что есть в кэше браузера. Но вот сама необходимость на каждый чих спрашивать у сервера «как там дела» может значительно снижать производительность клиентского приложения, да и лишняя нагрузка серверу не нужна. Как я уже упомянул в статье время соединения с сервером может в несколько раз превышать обработку запроса и время пересылки данных.
        +1
        Искал глазами Deffered. Удивился, что не нашел.
        Если вдруг незнакомы stackoverflow.com/questions/5890512/jquery-ajax-cache-with-deferred-when-duplicate-requests-are-fired-simultaneousl
          +1
          Если бы в условии задачи было обязательное использование jQuery для реализации слоя, то Deffered там скорее всего был бы. Здесь демонстрируется решение не завязанное ни на какую стороннюю JS библиотеку.
            0
            Ну, скажем, Deferred — это не jQuery, это паттерн для работы с асинхронными вызовами, очень хорошо бы вам его использовать не зависимо от используемых библиотек. Но передавать в кеширующий слой (localCache) правильнее именно данные (как у вас сейчас и сделано), т.к. в реализации, описанной в stackoverflow, нельзя выбрать место хранения этих данных (jqXHR/deferred нельзя, например, «сохранить» в localStorage или на другом сервере).
              0
              Мне показалось что lublushokolad имел ввиду реализацию паттерна Deferred именно в jQuery. В любом случае Deferred имеет смысл использовать если у запроса может быть несколько подписчиков. Это опять же зависит от конкретных задач приложения и от стиля программирования. Для демонстрации я выбрал вариант с одним подписчиком.
          0
          Отличная статья, надо будет попробовать, как оно в работе.
          Скептикам хочу сказать, что далеко не все запросы нормально кэшируются http-механизмом.
            +1
            [irony] Закат Солнца вручную. А вы не хотите написать браузер на JavaScript? :) [/irony]

            По идее все эти механизмы кеширования в браузере уже есть, осталось только их освоить и начать использовать. Браузер сам будет кешировать — ваша задача отдавать ему правильные заголовки.
            Все равно будет возникать ситуация, когда надо пойти на сервер и узнать «как там дела — появилось что-то новое или нет?». Что тогда делать?
            Все равно надо отправлять запрос.
            Если бы вы использовали правильные заголовки, то все было бы очень просто:
            1-й запрос — браузер ответил данными и сказал, что их можно хранить 5 минут
            2-й запрос — данные еще не устарели, браузер вернул их из кеша
            3-й запрос — время однозначного кеширования истекло, браузер пошел на сервер и добавил заголовок Last-Modified — сервер видит, что данные еще актуальные и отвечает пустым ответом 304 — браузер опять не передавая ничего «тяжелого» по сети использует те же данные
            4-й запрос — данные протухли, браузер пошел на сервер, сервер отдал свежее.

            Так вот — ничего из этого вам не надо делать руками! Все уже сделано до вас.
              0
              Ключевое отличие предложенного автором варианта (да и вообще всех клиентских реализаций кеша) от описанного вами HTTP-кеширования — возможность управлять политикой кеширования на клиенте. То есть когда не сервер решает, у каких данных истек срок годности, а клиент. В таком случае предложенный вами механизм не работает.

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

              Как справедливо указал автор, время установления соединения в определенных случаях может быть сильно больше времени генерации ответа сервера, тогда отказ от установления лишних соединений (для проверки «как там дела») может привести к существенному снижению времени реакции клиента на действия пользователя.

              Кроме того, данный механизм, как опять же указано автором, можно применять не только в случае с HTTTP-запросами.

              Вывод — текст не читай@комментируй вы не правы в части наличия всех необходимых механизмов кеширования в браузере.
                0
                Если по уму, то и так клиент решает, только под клиентом здесь подразумевается браузер.

                Если push — то можно сразу и данные доставлять, зачем еще запрашивать?

                Не очень понял, про соотношение времен. Понимаете, если возник вопрос «как там дела», то ключевое слово — «там», то есть вам по-любому придется идти за ответом «туда». Локальный кеш никак не поможет. Кроме одной единственной ситуации — сервер ответит «все так же, ничего не изменилось».
                  0
                  Нет. Если еще раз внимательно прочитаете описанную вами схему, то поймете, что в вашем примере клиент никогда не знает до момента отправки запроса, изменились данные или нет, то есть он всегда спрашивает сервер, соответственно, ваша схема допускает только серверное управление политикой кеширования.

                  Про соотношение времен:

                  В вашем случае проверка «как там дела» производится в момент запроса данных, и если данные не изменились, то проверка в любом случае отнимет время на установление соединения, то есть получим минимальное время реакции, равное времени установления соединения, а это может быть много и не поддаваться оптимизации («последнюю милю», например, никто не отменял).

                  В случае с реализацией собственного механизма кеширования можно, например, пушем отправлять сигналы типа «такие-то данные изменены». То есть на момент попытки доступа будет заранее известно, надо отправлять запрос или нет. Таким образом минимальное время реакции будет очень близко к нулю (и существенно меньше времени в первом случае).

                  Про push: во-первых, не все данные бывают нужны в момент их инвалидации, во-вторых, никто не мешает реализовать кеш таким образом, чтоб данные сразу обновлялись при пуше, и к моменту запроса там они будут уже готовые лежать — таким образом, время реакции еще сократится. При этом, кстати, не потребуется модифицировать код, получающий доступ к этим данным.

                  Не спорю, в большинстве случаев, стандартные механизмы HTTP-кеширования работают вполне удовлетворительно. Но есть случаи, когда их возможностей не хватает. Это, кстати, написано в топике.
              0
              Прошу прощения, но я вижу в этом неумение строить вещи нормальным путем. А ограничения localStorage и security issues сводят метод на нет. Грубо говоря, вы пытаетесь использовать расширеные куки под кэш. Было бы нормально, если это кэш какой-то runtime data или сделать данные доступными между вкладок, но не запросов…

              > Полученный кэширующий слой легко применить, например, к ресурсоемким повторяющимся операциям
              а бутылочное горлышко не в доступе к данным, а в самих операциях. Ресурсоемкие операции не выполняются на стороне юзверя. Докиньте виджеты, плагины jQuery, Flash, canvas, 50 вкладок,- и больше нет места на ресурсоемкие операции. Надо рассматривать вопрос как получилось так, что операциям нужны одни и те же данные N раз.
                0
                В последнее время набираются популярность мобильные html5 приложения, которые могут работать оффлайн. Используя localStorage кэш, Вы можете какое-то время работать (просматривать, а если постараться, то и редактировать данные), даже не подозревая об отсутствии связи. И здесь важно кэширование именно на уровне запросов к данным.

                > как получилось так, что операциям нужны одни и те же данные N раз

                А что, если я сегодня посчитал числа фибоначи до миллиона, то завтра мне это уже точно не потребуется? :)
                  0
                  > оффлайн
                  Вот я и говорю, что можно использовать как кэш какой-то runtime data (если RAM не хватает). Не вижу связи с кэшированием запросов к серверу. Сервер всегда выдает актуальные данные и конфигурируется запросом (это ожидается). Какой смысл кешировать данные с ключом запроса? Что это за мини-memcache? Фишка в том что кеширует по строке запроса, а это впринципе неправильно, если ваша аппликуха зовет один и тот же урл много раз в своем процессе. Это какие-то костыли из-за недостатка навыка в разработке high avalability.

                  > А что, если я сегодня посчитал числа фибоначи до миллиона, то завтра мне это уже точно не потребуется? :)
                  А что если юзер оборвет сессию не дав сохранить? Зависнет броузер, blue death screen?
                  Просчитайте один раз на backend до 64-битного значения, загрузите на сервер в таблицу, сделайте индексацию, пагинатор с продолжением просчетов. Если же процесс не детерминистичен, то есть другие способы.

                  Короче, хватит мракобесить :)
                    +1
                    > Какой смысл кешировать данные с ключом запроса?
                    Сами же отвечаете: "> Сервер всегда выдает актуальные данные и конфигурируется запросом (это ожидается)"

                    > Что это за мини-memcache?
                    Если мини-memcache для Вашего приложения не подходит, напишите свою реализацию localCache, всего-то реализовать 2 метода get(key) и set(key, data), хоть на соседний сервер сохраняйте, который пингуется лучше.

                    > Фишка в том что кеширует по строке запроса, а это впринципе неправильно
                    Это всего лишь один (самый простой для примера) вариант реализации.

                    >если ваша аппликуха зовет один и тот же урл много раз в своем процессе. Это какие-то костыли из-за недостатка навыка в разработке high avalability

                    Навыков в разработке high availability приложений у меня действительно не много, но все что я хочу, так это писать в своем приложении сколь угодно много раз так:

                    var myData = dataProvider.getData({ ...settings... });
                    


                    при этом не задумываясь какой урл и сколько раз позовет моя аппликуха, и позовет ли вообще, пусть об этом позаботятся те люди, которые хорошенько изучив статистику запросов и распределение времени «протухания» данных разных типов правильно реализуют кэширующий слой или докажут его несостоятельность и ограничатся настройкой HTTP кэширования (задача тоже не тривиальная). Ну а если такие специально обученные люди отсутствуют, то к этому вопросу я могу вернуться сам после реализации бизнес-логики моего приложения и основательно занявшись оптимизацией, не модифицируя при этом логику.
                      0
                      сдаюсь
                0
                > 1-й запрос — браузер ответил данными и сказал, что их можно хранить 5 минут
                Вероятно Вы имели ввиду сервер, а не браузер.

                Спасибо, за очень доходчивый туториал по кэшированию http запросов. Но, во-первых, как браузер справится с кэшированием результатов работы функции complicatedStuff. Во-вторых, сценарии работы с данными на клиенте иногда выходят за привычные запрос/готовый ответ. Например, два ответа от сервера инициированные двумя разными запросами могут содержать пересекающиеся данные. Если сделать кэш более интеллектуальным, то ответ последнего запроса может обновить данные в кэше для первого запроса. Браузер с такой задачей точно не справится.

                В целом я согласен, что в предложении «Не смотря на то, что кэширование может быть настроено на уровне HTTP протокола, часто оно не удовлетворяет реальным требованиям» слово «часто» лучше заменить на «иногда».

                  0
                  > Вероятно Вы имели ввиду сервер, а не браузер.
                  Да, конечно, это сервер, спасибо.

                  Нужна ли такая сложная схема? Не будет ли в ней слишком непрозрачная зависимость, когда ответов станет 20? Потом дебагать станет сложно.
                    +1
                    Ну 20 ответов там или 20000 сложность отладки от этого не должна зависеть. Есть две части системы, которые друг о друге ничего не знают. Каждая часть просто хорошо делает свое дело. А делает она его хорошо, потому что когда Вы писали эти части вы описывали сценарии которые они реализуют юнит-тестами. Интерфейс взаимодействия между ними настолько прост, что вероятность ошибки там очень мала. Но если ошибки все-таки обнаружатся, то процесс отладки прост. Воспроизводим проблему, отключаем кэш, если проблема осталась ищем ошибку в приложении с отключенным кэшем, если ошибка пропала, локализуем ошибку в кэше, покрываем тестом и продолжаем радоваться жизни не боясь что кто-то снова что-то сломает во вновь написанном коде.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое