Yii2, порядок подключения js и css через AssetBundle

Как всегда, сначала опишу проблему, которая возникла, потом как я ее решил, и почему решил написать про это пост.

Не всегда хочется подключать все js скрипты и css стили на всех страницах. Для этого я зачастую задаю комплекты ресурсов, с помощью AssetBundle (yii\web\AssetBundle) и потом подключаю их в нужных местах.<!–more–> Основные стили, которые нужны на всех страницах падают в стандартный AppAsset, и регистрируются в основном layouts:

use app\assets\AppAsset;
AppAsset::register($this);

Если же в определенном месте мне нужно подключить дополнительные комплекты ресурсов, я подключаю их в views нужного места, абсолютно идентичным кодом, приведенным выше.

Но в скором времени у меня появилась проблема. Мне нужно было, что бы подключаемые скрипты в локальном месте, которые используются только там, были ниже, чем подключаемые скрипты главного layouts, т.к. они зависят от подключаемых выше скриптов.

Если просто зарегистрировать два раза комплекты ресурсов в своих местах, то сначала выведут локальный, который зарегистрирован в views.

Что бы решить такую проблему, в структуре есть свойство “depends”, в котором перечисляются имена комплектов ресурсов, от которых зависит данный комплект.

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

 public $depends = [
    'app\assets\AppAsset',
];

И вроде бы все гуд, и отлично работает, выводит так, как нужно, в нужном порядке, но есть одно но. Любопытство не давало мне покоя, и было очень интересно, как эта дичь работает. Полез в ядро фреймворка.

Прошел по методам, унаследованным классам, и пришел к методу, который занимается непосредственно регистрацией этих комплектов, а именно “registerAssetBundle($name, $position = null)” в “yii\web\View”, приведу кусочек нужного кода:

if (!isset($this->assetBundles[$name])) {
    $am = $this->getAssetManager();
    $bundle = $am->getBundle($name);
    $this->assetBundles[$name] = false;
    
    // register dependencies
    
    $pos = isset($bundle->jsOptions['position']) ? $bundle->jsOptions['position'] : null;
    foreach($bundle->depends as $dep) {
        $this->registerAssetBundle($dep, $pos);
    }
    
    $this->assetBundles[$name] = $bundle;
    }

Метод принимает значение комплекта ресурсов, и если его нет в массиве уже зарегистрированных комплектов “$this->assetBundles”, начинает регистрировать его через “getAssetManager”. Но мы видим, что в цикле он перебирает все “depends” в если они есть, вызывает этот же метод, что бы зарегистрировать указанный там комплект ресурсов.

В итоге, при регистрации этого комплекта ресурсов в views мы регистрируем так же и наш основной комплект ресурсов “app\assets\AppAsset”, который указанный в “depends”. Учитывая то, что по структуре рендера страницы, мы должны сначала получить контент с views, т.е. выполнить наш php скрипт с представлением страницы, где указано подключение нашего локального комплекта ресурсов с основным в “depends”, мы выполняем первым именно это подключение, после чего, в layouts, будет выполнятся подключение основного комплекта ресурсов, что запустит этот же метод, но учитывая то, что мы зарегистрировали его раньше, условие “if (!isset($this->assetBundles[$name]))” не будет выполнятся, и мы пропустим регистрацию.

Вот таким образом, мы подключили все это дело, но не на страницу, только в память, что бы дальше работать с этим делом.

Зарегистрировать зарегистрировали, теперь нужно вывести, тут все еще интереснее. Подключение css стилей происходит в месте, где прописан метод “$this->head()”, подключение js скриптов – “$this->endBody()”. Если посмотреть в эти методы, то мы увидим, что там выводятся якоря, а именно “<![CDATA[YII-BLOCK-HEAD]]>” и “<![CDATA[YII-BLOCK-BODY-END]]>”, но в “$this->endBody()” мы еще и регистрируем непосредственно подключаемые файлы:

    foreach (array_keys($this->assetBundles) as $bundle) {
    $this->registerAssetFiles($bundle);
    }

“registerAssetFiles($name)” :

if (!isset($this->assetBundles[$name])) {
    return;
}

$bundle = $this->assetBundles[$name];

if ($bundle) {
    foreach($bundle->depends as $dep) {
        $this->registerAssetFiles($dep);
    }

    $bundle->registerAssetFiles($this);
}

unset($this->assetBundles[$name]);

В этом метод мы передаем ранее зарегистрированные комплекты ресурсов. Если его нет в списке, мы его не обрабатываем. Далее, если он все таки есть, мы проверяем есть ли у него “depends” и вызывая этот же метод, мы работаем с комплектом, который расположен в “depends”. После, мы все таки регистрируем непосредственно файлы “$bundle->registerAssetFiles($this);”, и удаляем из массива обработанный комплект ресурсов. Таким образом, мы получили списки css и js файлов, а учитывая, что мы удаляем обработанный комплект ресурсов с массива “unset($this->assetBundles[$name]);” и каждый раз проверяем на наличие комплектов в “depends” и подключаем их, файлы в “depends” всегда будут выводится выше, неважно, где подключены в коде комплекты ресурсов. Если комплект ресурсов, который есть в “depends”, подключен выше, файлы в нем зарегистрируются первыми, если он подключен ниже, но прописан в “depends”, благодаря такой логике, файлы в нем подключатся так же выше, и не будут подключатся повторно.

В конце концов, в конце нашего “layouts” мы вызывает метод “$this->endPage()”, где заменяем ранее прописанные якоря на списки наших подключаемых файлов, а выглядит все это так:

    echo strtr($content, [
    self::PH_HEAD => $this->renderHeadHtml(),
    self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
    self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
]);

В итоге мы выводим всю страницу, с уже подключенными стилями, скриптами и контентом.

Большая простыня получилась, но все таки хотел докопаться до сути, и понять, как реализовано упорядоченное подключение файлов.

Возможно, я что-то пропустил, если это так, жду комментариев.