HMVC — различия между версиями

Материал из YourcmcWiki
Перейти к: навигация, поиск
м
м
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
 
Об одном правильном подходе к HMVC (Hierarchical MVC) и кэшированию HTML на стороне сервера.
 
Об одном правильном подходе к HMVC (Hierarchical MVC) и кэшированию HTML на стороне сервера.
  
Делим страницу на блоки, каждый блок обрабатывается одним контроллером. При этом контроллеры образуются в "иерархию" - каждый блок может вызывать другие и подставлять внутрь себя их вывод. Каждый вызов контроллера описывается именем контроллера и параметрами, в которых, аналогично URL-параметрам, допускаются только сериализованные данные, а не объекты.
+
Делим страницу на блоки, каждый блок обрабатывается одним контроллером. При этом контроллеры образуются в «иерархию» — каждый блок может вызывать другие и подставлять внутрь себя их вывод. Каждый вызов контроллера описывается именем контроллера и параметрами, в которых, аналогично URL-параметрам, допускаются только сериализованные данные, а не объекты.
  
Для корректной инвалидации кэша используем теги.
+
Для корректной инвалидации кэша используем теги. Для реализации тегов в кэше, который сам по себе их не поддерживает, вместе с каждым ключом кэша будем сохранять «номера версий» всех связанных с ним тегов, а номер версии самого тега будем хранить в отдельной легковесной БД типа redis (либо в самом кэше, если он позволяет пометить ключ как невытесняемый). При чтении ключа из кэша будем сверять номера версий всех его тегов с новыми номерами версий тегов, и если в прочтённом ключе фигурирует более старая версия тега — будем вести себя так, как будто прочтённый ключ в кэше отсутствует. Таким образом, сброс ключей по тегу превращается в простой инкремент его номера версии. Также в ключе «информации о теге», кроме номера версии, мы будем сохранять время его модификации, чтобы потом легко вычислять время последней модификации страницы на основании одних только тегов кэша.
  
 
Далее запуск каждого контроллера делится на два метода:
 
Далее запуск каждого контроллера делится на два метода:
* check() - запускается всегда, перед попыткой загрузить результат из кэша. Здесь должны выполняться следующие задачи:
+
* check() — запускается всегда, перед попыткой загрузить результат из кэша. Здесь должны выполняться следующие задачи:
** обработка запросов на модификацию данных (и редирект после сохранения изменений)
+
** обработка запросов на модификацию данных (и редирект после сохранения изменений), либо отключение кэширования для таких запросов, чтобы они дошли до run()
 
** валидация параметров запроса, редиректы на очищенные URL
 
** валидация параметров запроса, редиректы на очищенные URL
 
** вычисление ключа кэша
 
** вычисление ключа кэша
 
** если хочется, вызов будущих подзапросов (метод subrequest()). это бывает полезно, если не хочется множить ключи кэша в зависимости от набора подзапросов. в то же время, это бывает вредно, если для вызова подзапросов нужно совершить дополнительную работу, которая при этом будет так или иначе совершена в run().
 
** если хочется, вызов будущих подзапросов (метод subrequest()). это бывает полезно, если не хочется множить ключи кэша в зависимости от набора подзапросов. в то же время, это бывает вредно, если для вызова подзапросов нужно совершить дополнительную работу, которая при этом будет так или иначе совершена в run().
 
** также можно вычислить и теги для будущего вывода, хотя практического смысла это не имеет
 
** также можно вычислить и теги для будущего вывода, хотя практического смысла это не имеет
* run() - запускается, только если результат не удалось загрузить из кэша. run() делает следующее:
+
* run() — запускается, только если результат не удалось загрузить из кэша. run() делает следующее:
 
** вычисляет вывод (HTML)
 
** вычисляет вывод (HTML)
 
** вычисляет теги кэша для вывода (если только check() это уже не сделал)
 
** вычисляет теги кэша для вывода (если только check() это уже не сделал)
 
** запускает подзапросы (если только их, опять-таки, не запускал check())
 
** запускает подзапросы (если только их, опять-таки, не запускал check())
  
Частый кейс: вкрапления ссылок "править" или других мелких зависящих от пользователя элементов (например, цен товаров в валюте пользователя). Варианты решения:
+
Частый кейс: вкрапления ссылок «править» или других мелких зависящих от пользователя элементов (например, цен товаров в валюте пользователя). Варианты решения:
* добавить ID пользователя в ключ кэша - нам не подходит, пользователей ведь будет много, только кэш забьётся лишним мусором
+
* добавить ID пользователя в ключ кэша — нам не подходит, пользователей ведь будет много, только кэш забьётся лишним мусором
* вынести подстановку этих элементов на javascript, в браузер - вменяемое решение, но генерирует много дополнительных запросов и замедляет полную загрузку страницы
+
* вынести подстановку этих элементов на javascript, в браузер — вменяемое решение, но генерирует много дополнительных запросов и замедляет полную загрузку страницы
* наше решение: отдельный небольшой дочерний контроллер, в который через параметры передаются готовые данные для определения, выводить или не выводить ссылку (например, набор пользователей, которым разрешено править выведенный элемент). Эти параметры будут кэшированы вместе с основным выводом в наборе подзапросов, таким образом, после загрузки HTML из кэша можно будет проверить эти разрешения "малой кровью" и вывести (или не выводить) ссылки.
+
* наше решение: отдельный небольшой дочерний контроллер, в который через параметры передаются готовые данные для определения, выводить или не выводить ссылку (например, набор пользователей, которым разрешено править выведенный элемент). Эти параметры будут кэшированы вместе с основным выводом в наборе подзапросов, таким образом, после загрузки HTML из кэша можно будет проверить эти разрешения «малой кровью» и вывести (или не выводить) ссылки.
  
 
Порядок обработки запроса:
 
Порядок обработки запроса:
 
# Создать главный контроллер
 
# Создать главный контроллер
# Создать контроллер layout'а (почти ничем не отличается от обычного, кроме того, что является как бы "подзапросом" главного, но при этом не подставляется в его вывод, а оборачивает его вывод)
+
# Создать контроллер layout’а (почти ничем не отличается от обычного, кроме того, что является как бы «подзапросом» главного, но при этом не подставляется в его вывод, а оборачивает его вывод)
 
# Начиная с главного контроллера:
 
# Начиная с главного контроллера:
 
## Вызываем check()
 
## Вызываем check()
 
## Пробуем загрузить вывод из кэша
 
## Пробуем загрузить вывод из кэша
## Если вывод загрузился и актуален, и если check() не вызывал subrequest() загружаем записи о подзапросах из вывода, загруженного из кэша
+
## Если вывод загрузился и актуален, и если check() не вызывал subrequest() — загружаем записи о подзапросах из вывода, загруженного из кэша
 
## Если не загрузился:
 
## Если не загрузился:
### Если check() вычислил теги для будущего ключа кэша - НЕ сбрасываем их!
+
### Если check() вычислил теги для будущего ключа кэша — НЕ сбрасываем их!
 
### Вызываем run()
 
### Вызываем run()
 
### Сохраняем вывод, теги и (если подзапросы вызывались в run()) записи о подзапросах в кэш
 
### Сохраняем вывод, теги и (если подзапросы вызывались в run()) записи о подзапросах в кэш
 
## Делаем (3) со всеми контроллерами подзапросов
 
## Делаем (3) со всеми контроллерами подзапросов
 
## Подставляем выводы подзапросов в вывод их родительских контроллеров
 
## Подставляем выводы подзапросов в вывод их родительских контроллеров
# Делаем (3) с контроллером layout'а
+
# Делаем (3) с контроллером layout’а
# Вычисляем Last-Modified из тегов кэша всех контроллеров (время модификации тега обновляется при каждом его сбросе)
+
# Вычисляем Last-Modified из тегов кэша всех контроллеров (помним, что время модификации тега обновляется при каждом его сбросе!)
 
# Сравниваем Last-Modified с запрошенным If-Modified-Since и отправляем HTTP 304, если кэш браузера актуален
 
# Сравниваем Last-Modified с запрошенным If-Modified-Since и отправляем HTTP 304, если кэш браузера актуален
 
# Отправляем вывод обычным образом
 
# Отправляем вывод обычным образом

Текущая версия на 16:13, 21 мая 2016

Об одном правильном подходе к HMVC (Hierarchical MVC) и кэшированию HTML на стороне сервера.

Делим страницу на блоки, каждый блок обрабатывается одним контроллером. При этом контроллеры образуются в «иерархию» — каждый блок может вызывать другие и подставлять внутрь себя их вывод. Каждый вызов контроллера описывается именем контроллера и параметрами, в которых, аналогично URL-параметрам, допускаются только сериализованные данные, а не объекты.

Для корректной инвалидации кэша используем теги. Для реализации тегов в кэше, который сам по себе их не поддерживает, вместе с каждым ключом кэша будем сохранять «номера версий» всех связанных с ним тегов, а номер версии самого тега будем хранить в отдельной легковесной БД типа redis (либо в самом кэше, если он позволяет пометить ключ как невытесняемый). При чтении ключа из кэша будем сверять номера версий всех его тегов с новыми номерами версий тегов, и если в прочтённом ключе фигурирует более старая версия тега — будем вести себя так, как будто прочтённый ключ в кэше отсутствует. Таким образом, сброс ключей по тегу превращается в простой инкремент его номера версии. Также в ключе «информации о теге», кроме номера версии, мы будем сохранять время его модификации, чтобы потом легко вычислять время последней модификации страницы на основании одних только тегов кэша.

Далее запуск каждого контроллера делится на два метода:

  • check() — запускается всегда, перед попыткой загрузить результат из кэша. Здесь должны выполняться следующие задачи:
    • обработка запросов на модификацию данных (и редирект после сохранения изменений), либо отключение кэширования для таких запросов, чтобы они дошли до run()
    • валидация параметров запроса, редиректы на очищенные URL
    • вычисление ключа кэша
    • если хочется, вызов будущих подзапросов (метод subrequest()). это бывает полезно, если не хочется множить ключи кэша в зависимости от набора подзапросов. в то же время, это бывает вредно, если для вызова подзапросов нужно совершить дополнительную работу, которая при этом будет так или иначе совершена в run().
    • также можно вычислить и теги для будущего вывода, хотя практического смысла это не имеет
  • run() — запускается, только если результат не удалось загрузить из кэша. run() делает следующее:
    • вычисляет вывод (HTML)
    • вычисляет теги кэша для вывода (если только check() это уже не сделал)
    • запускает подзапросы (если только их, опять-таки, не запускал check())

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

  • добавить ID пользователя в ключ кэша — нам не подходит, пользователей ведь будет много, только кэш забьётся лишним мусором
  • вынести подстановку этих элементов на javascript, в браузер — вменяемое решение, но генерирует много дополнительных запросов и замедляет полную загрузку страницы
  • наше решение: отдельный небольшой дочерний контроллер, в который через параметры передаются готовые данные для определения, выводить или не выводить ссылку (например, набор пользователей, которым разрешено править выведенный элемент). Эти параметры будут кэшированы вместе с основным выводом в наборе подзапросов, таким образом, после загрузки HTML из кэша можно будет проверить эти разрешения «малой кровью» и вывести (или не выводить) ссылки.

Порядок обработки запроса:

  1. Создать главный контроллер
  2. Создать контроллер layout’а (почти ничем не отличается от обычного, кроме того, что является как бы «подзапросом» главного, но при этом не подставляется в его вывод, а оборачивает его вывод)
  3. Начиная с главного контроллера:
    1. Вызываем check()
    2. Пробуем загрузить вывод из кэша
    3. Если вывод загрузился и актуален, и если check() не вызывал subrequest() — загружаем записи о подзапросах из вывода, загруженного из кэша
    4. Если не загрузился:
      1. Если check() вычислил теги для будущего ключа кэша — НЕ сбрасываем их!
      2. Вызываем run()
      3. Сохраняем вывод, теги и (если подзапросы вызывались в run()) записи о подзапросах в кэш
    5. Делаем (3) со всеми контроллерами подзапросов
    6. Подставляем выводы подзапросов в вывод их родительских контроллеров
  4. Делаем (3) с контроллером layout’а
  5. Вычисляем Last-Modified из тегов кэша всех контроллеров (помним, что время модификации тега обновляется при каждом его сбросе!)
  6. Сравниваем Last-Modified с запрошенным If-Modified-Since и отправляем HTTP 304, если кэш браузера актуален
  7. Отправляем вывод обычным образом

English

Our own HMVC. Each controller has 2 methods:

  • check() — always runs before trying to load the output from cache:
    • handles modification requests (and redirects after applying changes),
    • validates parameters, redirects to handsome URLs
    • calculates cache key
    • also it MAY calculate cache tags and do subrequests if it wants to
  • run() — runs only on cache miss:
    • calculates output HTML
    • MAY calculate cache tags and do subrequests if check() does not want to :-)

Request workflow:

  1. Create main controller
  2. Create layout controller
  3. Starting with the main controller:
    1. Call check()
    2. Try to load output from cache
    3. If loaded and valid, and if check() did not call subrequest() — load subrequest records from cached output
    4. If not loaded:
      1. If check() calculated some cache tags — do NOT reset them
      2. Call run()
      3. Save output, tags and subrequest records to cache
    5. Do (3) with all subrequest controllers
    6. Replace placeholders in output with subrequest outputs
  4. Do (3) with the layout controller
  5. Calculate Last-Modified from cache tags of all controllers (each tag flush updates its modification time)
  6. Compare Last-Modified with the requested If-Modified-Since and send HTTP 304 if not modified
  7. Send generated output normally