Работает по принципу прогрессивного улучшения (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>
|