Здравствуй %хабраюзер%.

Хочу поделиться с тобой идеей/реализацией аналога технологии ajax которая в отличии от первого лишена основных его недостатков. Из вики об ajax мы знаем что:

Преимущества
  • Экономия трафика;
  • Уменьшение нагрузки на сервер;
  • Ускорение реакции интерфейса;

Недостатки
  • Отсутствие интеграции со стандартными инструментами браузера;
  • Динамически загружаемое содержимое недоступно поисковикам;
  • Старые методы учёта статистики сайтов становятся неактуальными;
  • Усложнение проекта;


Придуманная идея Template client cache (далее TCC) обладает те ми же достоинствами, что и ajax, но полностью лишена её недостатков.

Идея


В случае с ajax основная «фишка» заключается в загрузке отдельных блоков, что значительно экономит трафик. Но большой проблемой ajax является сильная несовместимость как со старыми браузерами и поисковиками, так и сложная интеграция в существующие проекты. Решая головоломку переписыванием очередного сайта на блоки с ajax я подумал, не уж то при помощи современных технологий нельзя добиться аналогичного результата но с меньшими затратами по внедрению и совместимости? Итак… закончим лить воду… идея до безумия проста: грузить только то, что нам необходимо обновить (ну правда, очень смахивает на идею ajax). Весь вопрос в реализации. За основу были взяты современные стандарты по поддержке некого хранилища для нужд JS (такие как localStorage) и идея кеширования шаблонов на стороне сервера. Скрестив их мы и получили TCC. Т.е., как и в случае шаблонов на сервере обычно вся страница делиться на куски, которые обрабатываются и формируются отдельными файлами (кусками кода), совмещаются и выбрасываются пользователем. Зачастую некоторые куски кешируются и не формируются повторно в течении некоторого времени. Идея заключается в переносе этого кеша некоторых кусков на сторону клиента и создание инфраструктуры для передачи информации от клиента к серверу и обратно для поддержания кеша в ликвидном состоянии. Т.е. получиться, что при загрузке страницы мы будем так же, как и обычно её формировать на стороне сервера, пропуская некоторые блоки, но не беря их из кеша сервера, а просто оставляя пустыми, и в так��м виде будем передавать клиенту, где уже он будет объединять их с существующим кешем и отображать итоговую страницу пользователю. Из описания понятно, что:
  • + Мы не будем формировать лишние блоки и не будем тратить на них время на стороне сервера (плюс в производительности, включая затраты на кеширование этих блоков);
  • + Мы не будем передавать лишнюю (повторяющуюся) информацию от сервера к киленту (плюс в скорости загрузки и уменьшении трафика);
  • + Мы по сути не меняем структуру и метод формирования документа (плюс в простоте интеграции в существующие движки/сайты, а так же отсутствие проблем с поисковиками и старыми браузерами, так как в случае отсутствия у них поддержки TCC они будут получать всегда полную версию страницы со всеми блоками);


image

Итого, последовательность работы TCC можно представить так:
  • Клиент запрашивает страницу;
  • Сервер формирует html разделяя его на логические блоки специальными маркерами блоков;
  • Клиент получив эту страницу разделяет её по маркерам и помещает в кеш;
  • Клиент переходит по ссылке на другую страницу этого же сайта;
  • Сервер. зная какие блоки помещены в кеш, формирует страницу состоящую из пустых маркеров блоков и заполнеными маркерами блоков только для тех блоков, которые не актуальны в кеше (т.е. по сути формирует diff между старой и новой страницей);
  • Клиент, получив такой diff, извлекает из кеша необходимые блоки и наполняет страницу;
  • Profit;


Реализация


Понятно, что собирать я буду демо-пример и за короткий строк, а потому на многие вещи буду закрывать глаза (как то, например, использование jQuery для парсинга html, а так же некоторые облегченности для получения быстрого результата).

Список того, что можно будет улучшить и как я приведу ниже, так что не стоит относиться к демо-примеру критически (с точки зрения применения на продакшене).

Для начала нам нужно определиться с тем, как мы будем маркировать блоки для кеширования. Для простоты я взял стандартный html-элемент span, так как он мало влияет на содержащиеся в нем блоки (т.е. нормально работает и с плавающими вложенными блоками и с инлайновым контекстом). Для маркирования, что это именно маркер, был выбран спец класс именуемый по следующему принципу:
tcc_<ID>_<MODE>[_<TTL&gt];
Где:
<ID> — Уникальный номер блока;
<MODE> — Режим кеширования (о нем ниже);
<TTL> — Время жизни кеша;

Режим кеширования предназначен для указания клиенту, с каким именно блоком он имеет дело. Было выделено три режима кеширования:
r (realtime) — блок, не подлежащий кешированию (обновляющийся при каждой перезагрузки страницы);
s (server) — блок, сформированный на сервере и подлежащий кешированию (создаётся в случае, если необходимо сформировать или заменить блок в кеше);
c (client) — блок, сформированный на сервере пустым, в который клиент должен поместить закешированные данные;

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

function blockStart($id, $ttl = false)
{
  global $curTime;
  global $tcc;

  if (!$ttl)
  {
    echo '<span class="tcc_' . $id . '_r">';
    return true;
  }
  else if (isset($tcc[$id]) && ($tcc[$id] > $curTime))
  {
    echo '<span class="tcc_' . $id . '_c_' . $tcc[$id] . '">';
    return false;
  }
  else
  {
    echo '<span class="tcc_' . $id . '_s_' . $ttl . '">';
    return true;
  }
}


Как видно, в функции используется две внешних переменных, $curTime (текущее время в нужном формате), а так же $tcc (список кешей клиента, а его получении на сервере с клиента мы поговорим ниже). В остальном логика функции очень проста и понятна.

Далее, нам необходимо на стороне клиента (т.е. на стороне JS) написать некоторый набор сервисных функций для работы со стораджем (в данном случае с localStorage).

function storSet(id, ttl, value)
{
  localStorage.setItem('tcc_' + id, ttl + ':' + value);
}

function storGet(id, ttl)
{
  var data = localStorage.getItem('tcc_' + id);
  if (data == null) return null;

  var data_ttl = data.substr(0, 12).split(':', 1)[0];
  if ((data_ttl != ttl) || (data_ttl <= curTime)) return null;

  return data.substr(data_ttl.length + 1);
}


Здесь тоже всё просто… в storSet мы сохраняем данные не забывая указать id и сохранить ttl для проверки валидности блоков. В storGet мы извлекаем блок по его id, проверяем валидность ttl и, в случае успеха, возвращаем закешированный блок.

Затем нам необходим мегапарсер, который и будет разбирать всё полученное от сервера.

function tcc()
{
  var item;

  while ((item = $('[class^=tcc_]:first')) && (item.length != 0))
  {
    var cn = item.attr('class').split('_');

    if (cn[2] == 's')
    {
      var nItem = item.clone();
      var cItem;

      while ((cItem = nItem.find('[class^=tcc_]:first')) && (cItem.length != 0))
      {
        cItem.replaceWith('<span class="cache_' + cItem.attr('class') + '"></span>');
      }

      storSet(cn[1], cn[3], nItem.html());
    }
    else if (cn[2] == 'c')
    {
      var data = storGet(cn[1], cn[3])

      if (data == null)
      {
        alert('critical error');
      }
      else
      {
        var nItem = $('<span>' + data + '</span>');
        var cItem;

        while ((cItem = nItem.find('[class^=cache_tcc_]:first')) && (cItem.length != 0))
        {
          var ncn = cItem.attr('class').split('_');

          var rItem = item.find('[class^=tcc_' + ncn[2] + '_]');

          if (rItem.length != 0)
          {
            cItem.replaceWith('<span class="' + rItem.attr('class') + '">' + rItem.html() + '</span>');
          }
          else
          {
            cItem.remove();
          }
        }

        item.replaceWith(nItem.html());
      }
    }

    if (cn[2] != 'c')
    {
      item.replaceWith(item.html());
    }
  }

  ...

}


Говорить тут тоже особо не о чем, так как код достаточно прозрачен. Нашли блок -> поняли что это -> произвели необходимое действие.

В итоге, единственное, что нам осталось сделать на стороне клиента — дать знать серверу, что за кешем мы обладаем, что бы сервер мог правильно сформировать последующие страницы. В виде транспорта был выбран простой и элементарный — печеньки.

function tcc()
{

  ...

  var storage = '';

  for (var i = localStorage.length - 1; i >= 0; i--)
  {
    var key = localStorage.key(i);
    var prefix = key.split('_', 1)[0];
    var key = key.substr(prefix.length + 1);

    if (prefix != 'tcc') continue;

    var ttl = storExist(key);

    if (ttl != null)
    {
      if (storage.length != 0) storage += ',';

      storage += key + '_' + ttl;
    }
    else
    {
      localStorage.removeItem('tcc_' + key);
    }
  }

  var cookie = 'tcc=' + escape(storage) + ';path=/';

  if (storage == '') cookie += ';expires=Thu, 01-Jan-1970 00:00:01 GMT';

  document.cookie = cookie;
}


Кукисы формируются вполне прозрачно, проходимся по хранилищу выискивая наши блоки и заполняем список в кукисах указывая ID и TTL каждого блока. В приведенном куске код используется еще одна сервисная функция storExist, которая почти аналогична storGet за тем лишь исключением, что она возвращает TTL блока в случае, если этот блок есть и валиден.

function storExist(key)
{
  var data = localStorage.getItem('tcc_' + key);

  if (data == null) return false;

  var data_ttl = data.substr(0, 12).split(':', 1)[0];

  if (data_ttl <= curTime) return null;

  return data_ttl;
}


Ну вот, на стороне клиента мы по сути всё сделали, теперь вернемся к серверу. В самом начале я упомянул некий массив $tcc (для функции формирования маркерных блоков), который нам и необходимо сформировать.

if (isset($_COOKIE['tcc']))
{
  $list = explode(',', $_COOKIE['tcc']);

  for ($i = 0; $i < count($list); $i++)
  {
    $item = explode('_', $list[$i]);

    if (count($item) == 2)
    {
      $tcc[$item[0]] = $item[1];
    }
  }
}


Всё элементарно.

Подведение итогов


Надо сказать, что результатом эксперимента я вполне доволен. В итоговом скрипте я получил простую систему, которая на автоматике обрабатывает кеш, мержит и передает так мало трафика, как только возможно. Итоговый JS скрипт (который в идее должен всегда передаваться от клиента к серверу) занимает 2к трафика сжатого YUV но несжатого GZip, что по мне, так вполне приемлемо. Так же стоит отметить, что в случае старых браузеров или поисковиков, на стороне клиента не будет поддержки JS, localStorage или Cookie, а значит сервер будет всегда думать, что кеша на стороне клиента нет и будет всегда формиро��ать полную страницу (т.е. мы получили автоматическую систему совместимости со старичками, что по мне, так очень здорово). Так же стоит отметить достаточно простую серверную реализацию, которая может легко быть совмещена с каким либо шаблонизатором и производить маркирование блоков и кеширование их как на стороне клиента, так и сервера без лишнего геморроя для программиста. Так же, опционально, можно кешировать таким образом JS и CSS помещенные прямо в тело документа (в тело документа, кстати, в этом случае можно инклюдить все необходимые скрипты и стили). В этом случае, разделив и выделив каждый JS или CSS в отдельный блок, мы получим удобную и элементарную систему погрузки только необходимого JS и CSS кода сократив до минимума запросов к серверу (даже если этот запрос просто проверка валидности кеша).

Минусы (или что можно сделать иначе)


Явно необходимо избавляться от jQuery (ну это и так понятно).
Изменить маркировку блоков на что-то типа <!-- cc_<ID>_<MODE>[_<TTL>] -->, в этом случае мы сможем маркировать блоки не заботясь о совместимости со стандартами и целостностью разделяемого html (как ни как, с блоками span нам бы пришлось вырезать только законченные блоки html иначе, в случае не поддержки со стороны браузера TCC у нас бы всё развалилось).

Основными же минусами самой идеи являются:
  • Необходимость на стороне сервера следить за актуальностью кеша для каждого клиента (например, в случае корзины товаров в интернет магазине, также должен меняться при изменении корзины и быть уникальным для клиента, т.е. мы должны как то понимать, что кеш клиента устарел);
  • Потенциальное разрастание cookie, что ведет за собой увеличение трафика от клиента к серверу (но это именно потенциальная проблема, так как в итоге всё зависит от выбранных TTL блоков и их количества;


Ну собственно вот и всё…
Хотелось бы услышать разумную критику и идеи оптимизации. С удовольствием отвечу на все вопросы.

P.S. К сожалению, не могу выложить пример рабочего скрипта, если кто подскажет местечко для 1 php файла, буду благодарен.
UPD. P.S. Ссылка на рабочий пример: catsmile.heliohost.org/Mear/tcc.php (спасибо пользователю catsmile за местечко)

UPD.2 P.P.S. Для ликвидации неясности понимания целей данного топика уточню. Я не предлагаю замену ajax в прямом смысле это слова. Я предлагаю лишь рассмотреть вариант клиентского кеширования в какой то конкретной реализации. Так же продемонстрированный код не претендует на завершенность и создан лишь для целей демонстрации подхода. Основными целями же данного топика являются сбор идей и замечаний по поводу идеи кеширования некоторых блоков на стороне клиента и методы работы с этими блоками.