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

    При разработке веб-приложений существует одна общеизвестная проблема. Мы, программисты, пишем новый 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, но русская версия выполнена более качественно и понятно (в силу разного уровня знания языков).
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 15
    • +5
      Я у себя использую чуть чуть расширенную версию.
          private $_assetsUrl;
          /**
           * @return string the base URL that contains all published asset files of app.
           */
          public function getAssetsUrl()
          {
              if($this->_assetsUrl !== null)
              {
                  return $this->_assetsUrl;
              }
              else
              {
                  $assetsPath=Yii::getPathOfAlias(($this->module !== null) ? $this->module->name.'.assets':'application.assets');
                  $this->setAssetsUrl($assetsPath);
                  return $this->_assetsUrl;
              }
          }
      
          /**
           * @param string $value the base URL that contains all published asset files of app.
           */
          public function setAssetsUrl($path)
          {
              if(($assetsPath=realpath($path))===false || !is_dir($assetsPath) || !is_writable($assetsPath))
                  throw new CException(Yii::t('app','Assets path "{path}" is not valid. Please make sure it is a directory writable by the Web server process.',
                      array('{path}'=>$path)));
      
              $assetsUrl=Yii::app()->assetManager->publish($path,false,-1,YII_DEBUG);
      
              $this->_assetsUrl=$assetsUrl;
          }
      
      • 0
        Спасибо за ваш вариант
      • 0
        возможно я не очень понял, не могли бы вы пояснить пару моментов:

        1. для чего делать touch? Ведь если я поменял скрипты, то дата модификации изменится и assets обновится. Если я ничего не поменял, то и не нужно обновлять assets

        2. что включает в себя перенос всей статики в protected/assets? Extension's тоже? это ведь неудобно: приятней когда расширения хранят assets в своих папках.
        • 0
          Отвечаю:
          1. touch нужен для изменения даты модификации папочки /protected/assets. Только в этом случае изменится её хешированное название в папочке assets в корне сайта. При изменении самих файлов — этого не произойдет, если только вы не публикуете каждый файл по отдельности.

          2. Раширения можно оставить — у них должны быть свои ассеты.
        • 0
          А не проще ли выставить в конфиге ассет менеджера параметр linkAssets в true? Мне кажется это решает все проблемы, начиная от обновления файлов статики, заканчивая правами на файлы.
          • 0
            мне под Виндой не удалось заставить его работать(
            • 0
              Да, команда линуксовая. В Windows нет стандартного способа изменения даты папки из командной строки.
              Есть некоторые обходные пути, о которых можно почитать в интернете, например, здесь
              Но поскольку php сайты как правило разворачивают на линуксовых хостингах — я рассмотрел именно это вариант. Вечером добавлю уточнение в статью.
              • 0
                Функция symlink в PHP работает под виндой начиная с 5.3.
                • 0
                  В PHP 5.3+ на Windows Vista+ работает само. Чтобы работало в Windows XP или при PHP < 5.3:
                  if (!function_exists('symlink')) {
                      function symlink($target, $link)
                      {
                          `junction -accepteula "$link" "$target"`;
                      }
                  }
                  
                  if (!function_exists('unlink')) {
                      function unlink($link)
                      {
                          `junction -accepteula -d "$link"`;
                      }
                  }<source>
                  
                  Программа junction берётся из WinInternals и кладётся в PATH.
                • 0
                  linkAssets не всегда возможно использовать, об этом написано и в документации по yii

                  Ну и даже если использовать linkAssets — я так понимаю путь к файлу при обновлении его содержимого меняться не будет, а это значит браузер будет брать его из своего кеша.
                  • 0
                    путь к файлу меняться не будет, да, но дата изменения файла будет другой. Так что тут уже настройки сервера виноваты. У меня никогда небыло никаких проблем с симлинками.

                    Единственный минус — у меня dev сервер для некоторых случаев развернут на виртуальной машине с шаред фолдерами в Windows. В таком случае я не могу создать симлинку. Если использовать Samba и т.д. то вроде бы таких проблем нету, но мне так лень настроить сеть…
                • 0
                  Использую такой велосипед:

                  class Y
                  {
                  public static function asset($path)
                  {
                  return Yii::app()->assetManager->publish($path,false,-1,defined('YII_DEBUG') && YII_DEBUG);
                  }
                  }

                  Тот принцип что вы описали в статье использую в модулях и виджетах. В контроллерах такое делать как то не возникало нужды. Обычно ресурсы либо виджеты, либо глобально модули контролируют.
                  • 0
                    А ресурсы для аппликейшена вы храните в папках css/js глобально? Я даже ресурсы тем в ассеты складываю- так удобнее…
                    • +1
                      Нет, все в ресурсах данного компонента, можете глянуть мои расширения на github е в качестве примера. P.S.: Почему меня минусуют? Ребят, вы чего? По-моему, это слишком.
                      • 0
                        Думаю ответа на ваш вопрос вы не получите. Еще никто не получал.

                        вы имеете в виду что контроллеры не должны вообще ничего публиковать в ассеты? Ну тогда я согласен.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.