Yii — php-фреймворк / AssetManager: как форсировать получение пользователем обновленной статики

в 0:56, , рубрики: assets, cache, yii, yii framework, кеширование, кэш, статика, метки: , , , , , ,

При разработке веб-приложений существует одна общеизвестная проблема. Мы, программисты, пишем новый javascript-код, стили в css, меняем статику… И статика эта как правило кешируется браузером пользователя и может оставаться в кеше на довольно долгое время (и это на самом деле правильно, ибо может ускорить загрузку страниц в разы).

Но что же делать, если мы поменяли статику? Как заставить пользователя сбросить кеш и обновить эти файлы? Существуют некоторые общепринятые способы, например, добавлять версионную метку к имени файла, или добавлять временную метку в GET-параметре при подключении файла.

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

Шаг 1. Подготовка файлов.

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

 /protected     /assets         /css             /main.css         /img             /bg.png             /sprite.png             /extra_icons.png         /js             /main.js             /form.js             /etc.js 

Шаг 2. Подготовка инструментов.

Скорее всего у вас уже есть переопределенный класс Controller, который вы используете в качестве базового для всех остальных контроллеров.
Мы его немного изменим, чтобы обеспечить работу с ассетами:

class Controller extends CController {         private $_assetsBase;          public function getAssetsBase()         {                 if ($this->_assetsBase === null) {                         $this->_assetsBase = Yii::app()->assetManager->publish(                                 Yii::getPathOfAlias('application.assets'),                                 false,                                 -1,                                 YII_DEBUG                         );                 }                 return $this->_assetsBase;         }         public $menu=array();         public $items=array();         public $breadcrumbs=array(); } 

Все просто.
Здесь мы добавили геттер, который в первый раз публикует ассеты из папки /protected/assets и сохраняет путь в приватное свойство _assetsBase, а в последующие разы просто возвращает значение этого приватного свойства.
У метода publish() класса CAssetManager есть несколько интересных параметров.

Вот сигнатура метода:

publish(string $path, boolean $hashByName=false, integer $level=-1, boolean $forceCopy=false) 

И краткое описание параметров:

$path — собственно путь до папки ассетов, которую мы будем публиковать;
$hashByName — определяет, публиковать ли имя папки как есть, или хешировать. Благодаря этому параметру и происходит вся магия, о которой будет рассказано дальше;
$level — определяет степень вложенности папок для публикации (-1 — все подпапки рекурсивно);
$forceCopy — определяет, копировать ли файлы принудительно, даже если они уже существуют. Устанавливаем этот параметр в YII_DEBUG, за счет чего на локальной машинке наши скрипты будут постоянно обновляться, а на продакшене — только в случае необходимости (на продакшене YII_DEBUG должен быть выставлен в false).

Шаг 3. Орудуем подготовленными инструментами над подготовленными файлами.

То самое свойство assetsBase, для которого мы написали геттер чуть выше — можем теперь использовать везде.
Сейчас во вьюшках и лейаутах можем писать что-то типо:

<link rel="stylesheet" type="text/css" href="<?=$this->assetsBase?>/css/main.css" /> 

или

<?Yii::app()->clientScript->registerScriptFile($this->assetsBase.'/js/utils.js')?> 

И мы можем писать именно так, потому что $this в данном случае — это экземпляр класса Controller от которого наследуются все наши контроллеры и который передается во вьюшки и лейауты.

С виджетами дело обстоит немного по-другому, нужен свой подход, поскольку вьюшки виджета выполняются не в контексте контроллера, а в контексте виждета, поэтому для подключения Javascript файла во вьюшке виджета будем писать:

<?Yii::app()->clientScript->registerScriptFile(Yii::app()->controller->assetsBase.'/js/widget.js')?> 

Шаг 4. Обновляем версию на продакшене и принуждаем браузер пользователя загружать обновленные файлы.

Это самый важный момент и именно его упускают многие разработчики из виду, в том числе и я когда-то.
Читая многие статьи по Yii может показаться, что для того, чтобы пользователь получил обновленные файлы статики — достаточно залить их в /protected/assets почистить папочку /assets в корне сайта (если конечно используется AssetManager) и вуаля — у пользователя все обновится само собой.
Но это совсем не так!

Автогенерируемые имена подпапок в папочке /assets в корне веб-приложения основываются на хеше имени папки /protected/assets и не меняются от раза к разу, даже если вы чистите папочку /assets в корне веб-приложения. Они восстанавливаются в прежнем виде. А значит браузер пользователя не «просекает», что что-то поменялось до тех пор, пока не истечет время кеширования.

По крайней мере так было до недавнего времени. И эта проблема обсуждалась в этом топике, после чего Александр Макаров нашел обходной путь и своим коммитом от 8 ноября 2011 года исправил ситуацию.
Теперь, второй параметр метода publish() класса CAssetManager — параметр $hashByName, установленный в false — вынуждает Yii строить имена подпапок в папке /assets в корне веб-приложения уже на основании хеша имени папки со статикой, а также даты модификации этой папки.

Благодаря этому мы можем сделать очень простой финт при деплое файлов статики приложения:

touch /path/to/your/website/protected/assets 

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

Если говорить кратко

Нужно просто:

  1. Всю статику вынести в папку /protected/assets
  2. Расширить базовый класс контроллера так, как описано выше
  3. Везде использовать assetsBase в качестве основы путей до статики
  4. При обновлении статики на сайте выполнить команду
    touch /path/to/your/website/protected/assets

    которая приведет к изменению даты модификации папки /protected/assets, а значит генерации нового хеша в папке /assets в корне сайта

  5. Выгода: вы можете быть уверены, что пользователи получают самую последнюю версию ваших скриптов, стилей и т.п.

Немного о другом

Кстати говоря, насколько мне известно, например, в Ruby on Rails данная проблема уже решена.
В Yii версии 2, которая находится в стадии разработки данная проблема также скорее всего будет решена.
В обсуждении решения данной проблемы для Yii 2 можно поучастовать на форуме (доступ открыт только для активных участников).

Также, еще одной интересной фишкой является автоматическая минификация скриптов и css. На данный момент она не реализована на уровне фреймворка, но реализована в некоторых расширениях, но это уже тема другой статьи…

Полезные ссылки по теме

  1. Документация к классу CAssetManager.
  2. Обсуждение данной темы на форуме Yii
  3. Обсуждение данной проблемы для новой версии Yii 2 (доступно для только активных пользователей)
  4. Расширения для минификации:
  5. Английская версия данной статьи
  6. Еще одна статья, описывающая приемы работы с CAssetManager

PS: Отмечу, что данная статья не является ни переводом, ни кросспостом, ибо английскую версию статьи писал также я для базы знаний Yii, но русская версия выполнена более качественно и понятно (в силу разного уровня знания языков).

Автор: dhampik


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js