Работает по принципу прогрессивного улучшения (Progressive Enhancement)
Базово имеем обычный компонент, улучшение идет за счет js-расширения.
При открытии страницы компонент "устанавливает" js-расширение из шаблона.
Бэкэнд для расширения находится в ajax.php компонента.
Преимущества:
1) Вся верстка и фразы к ней в шаблоне.
2) Индексируемая верстка.
3) Наличие фоллбэка.
4) Основной js и css не загружаются сразу, возможны триггеры гидратации верстки.
5) Основная часть js и css пишется с использованием готовых библиотек из npm, плагинов для сборки и т.д.
6) Сохраняется самодостаточность компонента.
Путь до трейта с функционалом копирования расширения для классов компонентов: /local/modules/my.helper/lib/component/extensions.php
Использование трейта и модуля не обязательны. Преимущества появляются только при большом количестве компонентов и привычке оформлять код в модули.
При открытии страницы компонент копирует js-расширение из шаблона, если метка времени у расширения в шаблоне свежее, чем у расширения в папке /local/js/.
Путь до класса компонента: /local/components/my/component.class/class.php
Расширение должно находится в директории /js/ шаблона компонента.
Конфиг для утилиты-сборщика bitrix: /local/components/my/component/templates/.default/js/v1/bundle.config.js
Сборка командой: bitrix build
Путь до конфига расширения: /local/components/my/component/templates/.default/js/v1/config.php
Путь до шаблона компонента: /local/components/my/component/templates/.default/template.php
Содержит индексируемую верстку, заглушку, точку для монтирования и т.д.
Путь до js с логикой монтирования и фоллбэка: /local/components/my/component/templates/.default/script.js
Путь до класса расширения: /local/components/my/component/templates/.default/js/v1/src/index.js
Часть пути src/index.js задается в bundle.config.js, ключ input.
Путь до файла с миксином фраз: /local/components/my/component/templates/.default/js/v1/src/localize.js
Главное правило относительно фраз: не возвращать фразы с бэкэнда. Бэкэнд должен присылать только коды фраз.
Путь до файла с фразами: /local/components/my/component/templates/.default/js/v1/lang/ru/config.php
Путь до класса контроллера смонтированного расширения: /local/components/my/component.class/ajax.php
Пример использования в vue-компоненте.
Базово имеем обычный компонент, улучшение идет за счет js-расширения.
При открытии страницы компонент "устанавливает" js-расширение из шаблона.
Бэкэнд для расширения находится в ajax.php компонента.
Преимущества:
1) Вся верстка и фразы к ней в шаблоне.
2) Индексируемая верстка.
3) Наличие фоллбэка.
4) Основной js и css не загружаются сразу, возможны триггеры гидратации верстки.
5) Основная часть js и css пишется с использованием готовых библиотек из npm, плагинов для сборки и т.д.
6) Сохраняется самодостаточность компонента.
Путь до трейта с функционалом копирования расширения для классов компонентов: /local/modules/my.helper/lib/component/extensions.php
Использование трейта и модуля не обязательны. Преимущества появляются только при большом количестве компонентов и привычке оформлять код в модули.
При открытии страницы компонент копирует js-расширение из шаблона, если метка времени у расширения в шаблоне свежее, чем у расширения в папке /local/js/.
namespace My\Helper\Component; use Bitrix\Main\Application; use Bitrix\Main\IO; use Bitrix\Main\IO\Directory; use CBitrixComponent; /** * Добавляет компоненту функционал * копирования расширения из шаблона в папку /local/js. */ trait Extensions { /** * Преобразовывает класс компонента в часть пути для расширения. * Пример: MyComponentClass в my/component/class */ protected function componentClassName2Path(): string { return strtolower( preg_replace( '/([A-Z])/', '/$1', __CLASS__ ) ); } /** * Копирование расширений из директории /js/ шаблона компонента * в директорию /local/js/ расширений. * @return void */ public function installComponentExtensions(): void { /** * @var $this CBitrixComponent */ $this->initComponentTemplate(); $template = $this->getTemplate(); if(!$template) return; $path = $template->getFolder(); $serverRoot = Application::getDocumentRoot(); $templateJsPath = sprintf('%s%s/js', $serverRoot, $path); $templateJsDir = new IO\Directory($templateJsPath); try { if(!$templateJsDir->isExists()) { return; } $files = $templateJsDir->getChildren(); } catch (IO\FileNotFoundException) { return; } /** * @var IO\Directory|IO\File $extDir */ foreach ($files as $directory) { if (!$directory->isDirectory()) continue; /** @var IO\Directory $file */ $this->installComponentExtension($directory); } } /** * Копирует директорию в /local/js/my/component/class * для компонента MyComponentClass. * Получаем расширение my.component.class.dirname * @param Directory $dir * @return void */ public function installComponentExtension(IO\Directory $dir): void { $pathByTemplateJs = $dir->getName(); $pathByComponent = $this->componentClassName2Path(); $serverRoot = Application::getDocumentRoot(); $localDir = sprintf('%s/local/js/%s/%s', $serverRoot, $pathByComponent, $pathByTemplateJs ); $bundleTimestampFile = $dir->getPath() . '/.timestamp'; $localTimestampFile = $localDir . '/.timestamp'; $bundleTimestamp = (int)file_get_contents($bundleTimestampFile); if($bundleTimestamp == 0) return; $localTimestamp = (int)file_get_contents($localTimestampFile); $isLocalNotExist = $localTimestamp == 0; $isBundleNewer = !$isLocalNotExist && $localTimestamp < $bundleTimestamp; if($isBundleNewer || $isLocalNotExist) { copyDirFiles( $dir->getPath(), $localDir, true, true ); } } } |
Путь до класса компонента: /local/components/my/component.class/class.php
use My\Helper\Component\Extensions; class MyComponentClass extends CBitrixComponent { // Трейт копирования расширения из шаблона компонента в /local/js/ use Extensions; public function executeComponent() { // Функция из трейта. $this->installComponentExtensions(); $this->includeComponentTemplate(); } } |
Расширение должно находится в директории /js/ шаблона компонента.
Конфиг для утилиты-сборщика bitrix: /local/components/my/component/templates/.default/js/v1/bundle.config.js
Сборка командой: bitrix build
const pluginVue = require('rollup-plugin-vue'); const path = require('path'); const fs = require('fs'); const dayjs = require('dayjs'); module.exports = { input: 'src/index.js', output: 'dist/callback.bundle.js', namespace: 'BX.Catalog', adjustConfigPhp: false, browserslist: false, sourceMaps: false, plugins: { resolve: true, custom: [ pluginVue(), { name: 'bundle-timestamp', buildEnd: async () => { fs.writeFileSync( path.resolve('.timestamp'), dayjs().format('YYYYMMDDHHmm') ) } } ], }, }; |
Путь до конфига расширения: /local/components/my/component/templates/.default/js/v1/config.php
<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); // my/component/class - название компонента. // v1 - директория с расширением в директории /js/ шаблона компонента, в данном случае название равно версии расширения. // dist/callback.bundle.js путь из конфига bundle.config.js для сборщика bitrix. return [ 'css' => '/local/js/my/component/class/v1/dist/callback.bundle.css', 'js' => '/local/js/my/component/class/v1/dist/callback.bundle.js', 'rel' => [], 'skip_core' => true, ]; |
Путь до шаблона компонента: /local/components/my/component/templates/.default/template.php
Содержит индексируемую верстку, заглушку, точку для монтирования и т.д.
<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); use Bitrix\Main\Application; use Bitrix\Main\Localization\Loc; // Чтобы использовать один файл фраз для js-расширения и шаблона. $extLangFile = Application::getDocumentRoot() . $templateFolder . '/js/v1/config.php'; Loc::loadLanguageFile($extLangFile); ?> <!-- Точка монтирования расширения --> <div data-catalog-callback data-callback-hash="#call_me" data-callback-fallback-url="mailto:mail@example.com" hidden ></div> <!-- Ссылка не активна до монтирования или отработки фоллбэка --> <a href="#call_me" class="disabled"> <?= Loc::getMessage('MY_COMPONENT_CLASS_CALL_ME') ?> </a> |
Путь до js с логикой монтирования и фоллбэка: /local/components/my/component/templates/.default/script.js
... // Выше поиск точки монтирования, сбор параметров и триггеры монтирования. // Обязательная часть. // Итоговое название расширения для загрузки: // 1) название класса компонента MyComponentClass записываем как my.component.class. // 2) добавляем название директории v1 с расширением из /js/ шаблона. BX.loadExt('my.component.class.v1').then(function(exports) { try { // BX.Catalog - неймспейс из bundle.config.js // Callback - название класса расширения. new BX.Catalog.Callback({ el: node, // Нода в DOM c атрибутом data-catalog-callback hash: hash, // Значение атрибута data-callback-hash }); // Расширение готово к использованию. $('[href="' + hash + '"]').removeClass('disabled'); } catch (e) { showFallBack(); } }); // Ниже функция фоллбэка. ... |
Путь до класса расширения: /local/components/my/component/templates/.default/js/v1/src/index.js
Часть пути src/index.js задается в bundle.config.js, ключ input.
import {Type} from 'main.core'; import {Vue} from 'ui.vue'; import app from './App.vue'; export class Callback { constructor(options) { const { el, hash, } = options; Vue.create({ el, data: () => ({ hash, }), components: { 'catalog-callback-wrapper': app, }, template: `<catalog-callback-wrapper :hash="hash"/>` }) } } |
Путь до файла с миксином фраз: /local/components/my/component/templates/.default/js/v1/src/localize.js
Главное правило относительно фраз: не возвращать фразы с бэкэнда. Бэкэнд должен присылать только коды фраз.
export default { computed: { localize(state) { const prefix = 'MY_COMPONENT_CLASS_'; const phrases = BX.Vue.getFilteredPhrases(prefix); return Object.keys(phrases) .reduce( (carry, key) => { carry[key.replace(prefix,'')] = phrases[key]; return carry; }, {} ); } }, } |
Путь до файла с фразами: /local/components/my/component/templates/.default/js/v1/lang/ru/config.php
<?php $MESS['MY_COMPONENT_CLASS_CALL_ME'] = 'Перезвони мне'; $MESS['MY_COMPONENT_CLASS_INTRO'] = 'Оставьте свой номер телефона и мы свяжемся с вами в рабочее время'; $MESS['MY_COMPONENT_CLASS_INTRO_OFFICE_OPEN'] = 'Оставьте свой номер телефона и мы свяжемся с вами в течение 15 минут'; |
Путь до класса контроллера смонтированного расширения: /local/components/my/component.class/ajax.php
namespace My\Components\Controllers; use Bitrix\Main\Engine; class CatalogCallback extends Engine\JsonController { public function isOfficeOpenAction() { $isOpen = date('H') > 10 && date('H') < 20; return [ // Возвращаем коды фраз. 'resultCode' => $isOpen ? 'INTRO_OFFICE_OPEN' : 'INTRO' ]; } } |
Пример использования в vue-компоненте.
<sc ript> import localize from './localize'; export default { mixins: [localize], data() { return { introText: '', }; }, methods: { getIntroText() { const request = BX.ajax.runComponentAction( 'my:component.class', 'isOfficeOpen', { mode:'ajax', json: {} } ); request.then(({data}) => { this.introText = this.localize[data.resultCode] || this.localize.INTRO; }).catch(() => { this.introText = this.localize.INTRO; }); } }, mounted() { this.introText = this.localize.INTRO; this.getIntroText(); this.isOpenChecker = setInterval(this.getIntroText, 60*5*1000); }, beforeDestroy() { clearInterval(this.isOpenChecker); } }; </sc ript> |