HMVC
Об одном правильном подходе к HMVC (Hierarchical MVC) и кэшированию HTML на стороне сервера.
Делим страницу на блоки, каждый блок обрабатывается одним контроллером. При этом контроллеры образуются в "иерархию" - каждый блок может вызывать другие и подставлять внутрь себя их вывод. Каждый вызов контроллера описывается именем контроллера и параметрами, в которых, аналогично URL-параметрам, допускаются только сериализованные данные, а не объекты.
Для корректной инвалидации кэша используем теги.
Далее запуск каждого контроллера делится на два метода:
- check() - запускается всегда, перед попыткой загрузить результат из кэша. Здесь должны выполняться следующие задачи:
- обработка запросов на модификацию данных (и редирект после сохранения изменений)
- валидация параметров запроса, редиректы на очищенные URL
- вычисление ключа кэша
- если хочется, вызов будущих подзапросов (метод subrequest()). это бывает полезно, если не хочется множить ключи кэша в зависимости от набора подзапросов. в то же время, это бывает вредно, если для вызова подзапросов нужно совершить дополнительную работу, которая при этом будет так или иначе совершена в run().
- также можно вычислить и теги для будущего вывода, хотя практического смысла это не имеет
- run() - запускается, только если результат не удалось загрузить из кэша. run() делает следующее:
- вычисляет вывод (HTML)
- вычисляет теги кэша для вывода (если только check() это уже не сделал)
- запускает подзапросы (если только их, опять-таки, не запускал check())
Частый кейс: вкрапления ссылок "править" или других мелких зависящих от пользователя элементов (например, цен товаров в валюте пользователя). Варианты решения:
- добавить ID пользователя в ключ кэша - нам не подходит, пользователей ведь будет много, только кэш забьётся лишним мусором
- вынести подстановку этих элементов на javascript, в браузер - вменяемое решение, но генерирует много дополнительных запросов и замедляет полную загрузку страницы
- наше решение: отдельный небольшой дочерний контроллер, в который через параметры передаются готовые данные для определения, выводить или не выводить ссылку (например, набор пользователей, которым разрешено править выведенный элемент). Эти параметры будут кэшированы вместе с основным выводом в наборе подзапросов, таким образом, после загрузки HTML из кэша можно будет проверить эти разрешения "малой кровью" и вывести (или не выводить) ссылки.
Порядок обработки запроса:
- Создать главный контроллер
- Создать контроллер layout'а (почти ничем не отличается от обычного, кроме того, что является как бы "подзапросом" главного, но при этом не подставляется в его вывод, а оборачивает его вывод)
- Начиная с главного контроллера:
- Вызываем check()
- Пробуем загрузить вывод из кэша
- Если вывод загрузился и актуален, и если check() не вызывал subrequest() — загружаем записи о подзапросах из вывода, загруженного из кэша
- Если не загрузился:
- Если check() вычислил теги для будущего ключа кэша - НЕ сбрасываем их!
- Вызываем run()
- Сохраняем вывод, теги и (если подзапросы вызывались в run()) записи о подзапросах в кэш
- Делаем (3) со всеми контроллерами подзапросов
- Подставляем выводы подзапросов в вывод их родительских контроллеров
- Делаем (3) с контроллером layout'а
- Вычисляем Last-Modified из тегов кэша всех контроллеров (время модификации тега обновляется при каждом его сбросе)
- Сравниваем Last-Modified с запрошенным If-Modified-Since и отправляем HTTP 304, если кэш браузера актуален
- Отправляем вывод обычным образом
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:
- Create main controller
- Create layout controller
- Starting with the main controller:
- Call check()
- Try to load output from cache
- If loaded and valid, and if check() did not call subrequest() — load subrequest records from cached output
- If not loaded:
- If check() calculated some cache tags — do NOT reset them
- Call run()
- Save output, tags and subrequest records to cache
- Do (3) with all subrequest controllers
- Replace placeholders in output with subrequest outputs
- Do (3) with the layout controller
- Calculate Last-Modified from cache tags of all controllers (each tag flush updates its modification time)
- Compare Last-Modified with the requested If-Modified-Since and send HTTP 304 if not modified
- Send generated output normally