1 марта 2012 в 04:56

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

Yii*
При разработке веб-приложений существует одна общеизвестная проблема. Мы, программисты, пишем новый 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, но русская версия выполнена более качественно и понятно (в силу разного уровня знания языков).
+35
4778
119
dhampik 5,0 G+

Комментарии (15)

+5
JiLiZART, #
Я у себя использую чуть чуть расширенную версию.
    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
diwms, #
Спасибо за ваш вариант
0
vitalets, #
возможно я не очень понял, не могли бы вы пояснить пару моментов:

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

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

2. Раширения можно оставить — у них должны быть свои ассеты.
0
Fesor, #
А не проще ли выставить в конфиге ассет менеджера параметр linkAssets в true? Мне кажется это решает все проблемы, начиная от обновления файлов статики, заканчивая правами на файлы.
0
vitalets, #
мне под Виндой не удалось заставить его работать(
0
dhampik, #
Да, команда линуксовая. В Windows нет стандартного способа изменения даты папки из командной строки.
Есть некоторые обходные пути, о которых можно почитать в интернете, например, здесь
Но поскольку php сайты как правило разворачивают на линуксовых хостингах — я рассмотрел именно это вариант. Вечером добавлю уточнение в статью.
0
Fesor, #
Функция symlink в PHP работает под виндой начиная с 5.3.
0
Athari, #
В 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
dhampik, #
linkAssets не всегда возможно использовать, об этом написано и в документации по yii

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

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

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

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

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

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