Проблема
Ни для кого не секрет, что производительность и по сей день остается одним из основных показателей качества веб-приложения. И, конечно, любой веб-разработчик провел не один час, оптимизируя свое приложение и добиваясь приемлемой производительности, как на серверной, так и на клиентской стороне. Несмотря на то, что аппаратное обеспечение день ото дня становится все мощнее и мощнее, всегда находятся узкие места, которые бывает непросто обойти. С приходом AJAX, HTTP запросы стали «мельче» по объему получаемых на клиента данных, но их количество увеличилось. Каналы связи могут быть достаточно широкими, а вот время соединения и процесс формирования ответа на сервере могут занимать значительное время. Кэширование результатов запросов на клиенте может значительно повысить общую производительность. Не смотря на то, что кэширование может быть настроено на уровне HTTP протокола, часто оно не удовлетворяет реальным требованиям.
Задача
Наша система клиентского кэширования должна удовлетворять следующим требованиям:
- Возможность реализовать логику управления кэшем любой сложности;
- Возможность повторного использования в разных приложениях;
- Возможность прозрачного встраивания в существующее приложение;
- Независимость от типа данных и способа их получения;
- Независимость от способа хранения закешированных данных;
Существующее приложение
Допустим, у нас уже есть работающее приложение, которое использует 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));
}
В заключение
Мы рассмотрели лишь подход к проксированию любых операций, которые выполняют ваши приложения. Вариантов применения – множество. От логирования, до реализации сложных аспектно-ориентированных алгоритмов, все зависит от ваших потребностей и фантазии.