В мире фронтенда многие ресурсы (ассеты) связаны между собой. В Joomla никогда не было простого способа указать эту связь, но Joomla 4 изменила эту ситуацию, введя концепцию Web Assets. Управление JavaScript и CSS в Joomla значительно упростилось, благодаря классу WebAssetManager
. Есть замечательная статья Как правильно подключать JavaScript и CSS в Joomla 4, в которой подробно и с примерами кода рассказывается об этой концепции и её применении. Рекомендую ознакомиться с ней для более полного понимания сути этой статьи. Статья эта первоначальна была опубликована на Хабре. Копирую к себе.
Однако, в процессе разработки собственных решений я столкнулся с проблемой. Решение её в данной заметке будет небольшим дополнением к вышеупомянутой статье.
Задача
Задача заключается в том, чтобы подключить в общий реестр скриптов, стилей и пресетов js-библиотеку (в моём случае - Swiper.js) таким образом, чтобы она была доступна из самых разных мест Joomla 4 и её можно было использовать для разных расширений (возможно не только моих), автономно её обновлять.
Например: js-библиотеку подключает плагин, а использовать её может и модуль, и компонент, и контент-плагин, и плагин поля.
Образцом поведения является встроенный в Joomla 4 Bootstrap 5. Он поддерживает модульное подключение с автоматическим подключением всех зависимостей, которые описаны в файле media/vendor/joomla.asset.json.
Joomla 4 будет искать определение ассетов автоматически во время выполнения в следующем порядке:
-
media/vendor/joomla.asset.json (при первом обращении к WebAssetRegistry)
-
media/system/joomla.asset.json
-
media/legacy/joomla.asset.json
-
media/{com_active_component}/joomla.asset.json (Общая информация о принципе действия Joomla приложения)
-
templates/{active_template}/joomla.asset.json
А затем загрузит их в реестр известных JavaScript и CSS файлов.
Редактировать файлы ядра - нельзя (хотя, к сожалению, это распространено среди разработчиков, желающих быстро решить какую-нибудь задачу). Значит нам нужен плагин, которым можно "подлезть" на этапе формирования реестра ассетов и добавить в него нужные нам веб ассеты. Это решение было очевидно сразу.
Однако, все примеры создания и подключения скриптов, стилей и пресетов предполагали, что регистрируется и начинает использоваться ассет в одном и том же месте. Попытки вынести подключение ассета в плагин, а использовать ассет в модуле через $wa->usePreset,
$wa->useScript
приводили к ошибке "There is no "swiper-bundle" asset of a "script" type in the registry."
Для того, чтобы разобраться в работе системы я начал анализировать код "коробочных" расширений.
Пример из системного плагина jooally - плагина версии для слабовидящих
<?php
/**
* @package Joomla.Plugin
* @subpackage System.jooa11y
*
* @copyright (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
/**
* Jooa11y plugin to add an accessibility checker
*
* @since 4.1.0
*/
class PlgSystemJooa11y extends CMSPlugin implements SubscriberInterface
{
/**
* Application object.
*
* @var CMSApplicationInterface
* @since 4.1.0
*/
protected $app;
/**
* Affects constructor behavior. If true, language files will be loaded automatically.
*
* @var boolean
* @since 4.1.0
*/
protected $autoloadLanguage = true;
/**
* Subscribe to certain events
*
* @return string[] An array of event mappings
*
* @since 4.1.0
*
* @throws Exception
*/
public static function getSubscribedEvents(): array
{
$mapping = [];
// Срабатываем только на фронте
if (Factory::getApplication()->isClient('site'))
{
/**
* Срабатываем на событие onBeforeCompileHead и вызываем функцию initJooa11y.
* Можно по старинке упростить и использовать public function onBeforeCompileHead()
*/
$mapping['onBeforeCompileHead'] = 'initJooa11y';
}
return $mapping;
}
}
Плагин срабатывает на событие onBeforeCompileHead
. На этом событии возможно обработать всё, что составляет содержимое <head>
страницы в Joomla 4: title, мета-теги и т.д.
В самой функции initJooa11y
идут проверки на CLI, REST API - чтобы плагин срабатывал только при выводе HTML. В самом конце функции происходит регистрация и добавление скриптов и стилей для функционирования версии для слабовидящих:
// Get the document object.
$document = $this->app->getDocument();
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa*/
$wa = $document->getWebAssetManager();
$wa->getRegistry()->addRegistryFile('media/plg_system_jooa11y/joomla.asset.json');
$wa->useScript('plg_system_jooa11y.jooa11y')
->useStyle('plg_system_jooa11y.jooa11y');
В файле media/plg_system_jooa11y/joomla.asset.json
описываются файлы и их зависимости для работы плагина. Обратите внимание на то, что сразу после регистрации ассета начинается его использование - useScript и useStyle.
Но такое поведение меня не устраивало, так как хотелось достичь большей универсальности и автономности элементов. Помогли собственные поиски и отклик Joomla-сообщества. Итак....
Как добавить собственные js и css в Joomla 4 и сделать их доступными глобально?
Создаём плагин группы system. Официальная документация для разработчиков Joomla 4 по созданию плагинов. Можно по старинке использовать методы вида public function onBeforeCompileHead()
. В таком случае плагин можно будет использовать как в Joomla 3, так и в Joomla 4. А можно использовать новый способ, предложенный в Joomla 4.1.
<?php
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
use Joomla\CMS\WebAsset\WebAssetRegistry;
class Wtjswiper extends CMSPlugin implements SubscriberInterface
{
/**
* Subscribe to certain events
*
* @return string[] An array of event mappings
*
* @since 4.1.0
*
* @throws Exception
*/
public static function getSubscribedEvents(): array
{
return $mapping = [
'onAfterRoute' => 'addSwiperPreset'
];
}
}
Такой способ при правильном применении и вызове события позволяет не запоминать порядок передаваемых аргументов.
Самым важным оказалось найти правильное системное событие, на этапе которого есть возможность добавить свои веб-ассеты глобально в Joomla 4 Web Assets Manager.
Upd. 25.09.2023. Ранее в этом месте статьи я рекомендовал подключать веб-ассеты на событие onAfterInitialise
. Однако, это одно из самых ранних системных событий. На этом этапе Joomla ещё не знает какой именно тип документа требуется отобразить (HTML, Json и т.д.). Использование привычных методов Factory::getDocument()
(устаревший вызов) или Factory::getApplication()->getDocument()
приводили к слишком ранней инициализации типа документа и в случае использования документов типа Json могли вызывать ошибку. Об этом написано в переводе статьи Распространенные ошибки при написании плагинов Joomla 4. В случае с типом документа HTML всё работало, но под капотом оно было неправильно.
Вторым нюансом в выборе правильного системного события оказалось то, что есть WebAssetRegistry - реестр ассетов. И WebAssetManager - менеджер по работе с ассетами. WebAssetRegistry инициализируется и доступен раньше, чем WebAssetManager. Его можно получить из контейнера например на событие onAfterRoute
и здесь же добавить свой ассет. Как оказалось, событие onBeforeCompileHead
- это одно из самых последних вызываемых событий и добавлять именно файл joomla.asset.json в этот момент жизненного цикла приложения уже слишком поздно. Поэтому, как я писал ранее, попытки зарегистрировать веб-ассет на событии onBeforeCompileHead
приводили к тому, что joomla.asset.json добавлялся в реестр ассетов, но не парсился. Что равносильно тому, что его не существует. Примеры кода исправлены.
Upd.23.10.2023: В Joomla 5 появился новый системный триггер для плагинов, который идеально подходит для добавления пресетов в WebAssetRegistry: onAfterInitialiseDocument
.
Также еще один важный нюанс: Вы можете добавлять пресет с помощью добавления файла joomla.asset.json
в реестр ассетов методом $wa->addRegistryFile()
. В таком случае нужно брать одно из ранних системных событий ДО onAfterDispatch
. Так как перед триггером onAfterDispatch
происходит рендер вывода компонента (не всей страницы с модулями, а именно области компонента), а в этот момент уже начинают работать плагины контента, которые в свою очередь могут использовать те или иные веб-ассеты. Если используемых веб-ассетов не окажется в реестре - будет ошибка There is no "your-preset-name" asset of a "script" type in the registry.
Мы могли бы проверить тип документа перед добавлением ассета и если он - "html" - добавить ассет. Но, проблемой в Joomla 4 является то, что на событиях до onAfterDispatch
ещё не известен тип документа, который мы обычно получаем с помощью Factory::getApplication()->getDocument()
. Веб ассеты нужны для отображения данных в документе типа HtmlDocument. Однако, в Joomla 9 типов документа (Document) и минимум 5 видов приложений (App), а также могут быть кастомные типы приложения со своими типами документов.
Определение типа документа происходит позже. Слишком ранняя инициализация типа документа путём $app->getDocument()
не вызовет никаких последствий для Html-документа - обычных сайтов, но может вызвать ошибки в других типах документа - XML, Json, Feed etc., а так же в других типах приложений - CLI, Api и т.д.
В Joomla 5 конкретно эта проблема решена добавлением события для плагинов - onAfterInitialiseDocument
, которым и рекомендуется пользоваться.
Также есть вариант регистрировать ассеты js/css файлами - методами registerAndUseScript()
или registerAndUseStyle()
. Тогда добавлять их можно и на более поздних событиях, но это уже не так удобно, так как описывать ассет нужно в PHP, а не в joomla.asset.json
.
Следующий код позволяет зарегистрировать javascript-библиотеку
<?php
public function addSwiperPreset() : void
{
// Only trigger in frontend
if (Factory::getApplication()->isClient('site'))
{
/** @var Joomla\CMS\WebAsset\WebAssetRegistry $wa*/
$wa = Factory::getContainer()->get(WebAssetRegistry::class);
$wa->addRegistryFile('media/plg_system_wtjswiper/joomla.asset.json');
}
}
Обратите внимание на метод addRegistryFile()
, где указывается путь к файлу joomla.assets.json
от корня сайта. Так же существует прокси-метод addExtensionRegistryFile(string $name)
, который принимает в качестве параметра системное имя расширения, по которому доступны его веб-ассеты в папке media
: com_content, plg_system_jooally и т.д. Тогда подключаться будет файл 'media/com_content/joomla.asset.json' и 'media/plg_system_jooally/joomla.asset.json' соответственно.
Содержимое файла joomla.assets.json
{
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
"name": "swiper",
"version": "8.2.4",
"description": "Swiper js library",
"license": "GPL-2.0-or-later",
"assets": [
{
"name": "swiper-bundle",
"type": "script",
"uri": "plg_system_wtjswiper/swiper-bundle.min.js",
"attributes": {
"defer": true
},
"package": "swiper",
"version": "8.2.4"
},
{
"name": "swiper-bundle",
"type": "style",
"uri": "plg_system_wtjswiper/swiper-bundle.min.css",
"package": "swiper",
"version": "8.2.4"
},
{
"name": "swiper-bundle",
"type": "preset",
"uri": "",
"dependencies": [
"swiper-bundle#style",
"swiper-bundle#script"
]
}
]
}
Uri в json в зависимости от типа ассета автоматически дополняется 'js' или 'css'. Если вы подключаете файл media/plg_system_wtjswiper/css/swiper-bundle.min.css
, то uri файла в joomla.assets.json
будет plg_system_wtjswiper/swiper-bundle.min.css
Такой же принцип использовался раньше в Joomla 3 при подключении ресурсов с помощью HTMLHelper (ex. JHTML).
Выложил результат изысканий в виде готового плагина, интегрирующего Swiper.js в Joomla 4.