[Записки тимлида] Битрикс: от модулей к сервисам 3
Автор: Денис Закусило
Приветствую всех неравнодушных! Это заключительная статья цикла о переходе от модульной архитектуры к сервисам.
[Записки тимлида] Битрикс: от модулей к сервисам
[Записки тимлида] Битрикс: от модулей к сервисам 2
Сегодня мы рассмотрим организацию структуры frontend стороны приложения.
Первым делом нам необходимо подключить node на сервере. В нашем случае мы добавим в docker-compose новую запись.
node:
build:
context: ./node
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
volumes_from:
- source
links:
- php
environment:
TZ: Europe/Moscow
NODE_ENV: ${NODE_ENV:-production}
HOST_FROM: ${HOST_FROM:-localhost}
stdin_open: true
tty: true
networks:
- bitrixdock
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
И устанавливаем Bitrix cli по инструкции в нашем node/Dockerfile, прокидывая права нашего пользователя из родительской ОС.
FROM node:22-alpine
ARG UID
ARG GID
RUN echo http://dl-2.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories
RUN apk add -U shadow
RUN npm install -g @bitrix/cli
RUN chown -R ${UID}:node /usr/local/lib/node_modules/
RUN usermod -u ${UID} node
WORKDIR "/var/www/bitrix"
EXPOSE 8888
Организуем структуру, такую же, как на бекенде по DDD.
Выполним практическую задачу: Для сделки необходимо получать минимальную дату доставки от бекенда и запрещать оператору выбирать более ранние даты.
Решение:
1. Для начала нам нужно определить место для endpoint list, в котором было бы организовано хранилище адресов для запросов на бекенд.
Сделаем по аналогии с Enums php:
Enum в PHP 8.1 — для чего нужен enum, и как реализован в PHP
Через несколько дней заканчивается голосование по первой итерации реализации enum в PHP 8.1 . Уже ви…
Мы создадим класс, который будет хранить константы.
1.1 Создаем модуль enums и переходим в директорию доменов. Для этого заходим в контейнер, переходим в домен и запускаем команду bitrix build enums:
docker compose exec -T -u node node sh
cd /local/js/App/Domains
bitrix create enums
? Extension name: (enums) enums
? Extension name: enums
? Enable tests: (Y/n) Y
? Enable tests: Yes
? Use Browserslist: (Y/n) Y
? Use Browserslist: Yes
? Enable minification: (y/N) y
? Enable minification: Yes
? Enable sourceMaps: (Y/n) y
? Enable sourceMaps: Yes
│ Success! │
│ Extension App.Domains.enums created │
│ Run bitrix build -p ./enums for build extension │
│ Include extension in php │
│ \Bitrix\Main\UI\Extension::load('App.Domains.enums'); │
│ or import in your js code │
│ import {enums} from 'App.Domains.enums'; │
Наш модуль создался и доступен для импортирования в другие модули через import {enums} from 'App.Domains.enums;
В директории, в конец созданного класса модуля js/App/Domains/enums/src/enums.js,
добавляем наши наборы экспортируемых констант.
import {Type} from 'main.core';
export class Enums {
constructor(options = {name: 'Enums'}) {
this.name = options.name;
}
setName(name) {
if (Type.isString(name)) {
this.name = name;
}
}
getName() {
return this.name;
}
}
/**
* Enum for common entity types.
* @readonly
* @enum {string}
*/
export const EntityTypeEnum = Object.freeze({
DEALS: Symbol("crm:deal")
});
/**
* Enum for actions.
* @readonly
* @enum {string}
*/
export const ActionsLinkEnum = Object.freeze({
DEAL_STORE: Symbol(".api.ActionController.getStoreByDeal"),
DEAL_MIN_DELIVERY_DATE_GET: Symbol(".api.ActionController.getMinDeliveryDate"),
DEAL_MIN_DELIVERY_DATE_UPDATE: Symbol(".api.ActionController.updateMinDeliveryDate"),
DEAL_ORDER_CANCEL: Symbol(".api.ActionController.cancelOrderByDeal"),
DYNAMIC_NOT_AGREE_CONTACTS_GET: Symbol(".api.ActionController.getVisitNotAgreeContacts")
});
Примечание! Если хотите переименовать класс и файл по аналогии, как это делается в PHP, то необходимо поправить пути в bundle.config.js и config.php
модуля.
import {ActionsLinkEnum} from "../../../enums/src/Enums";
BX.ajax.runAction(ActionsLinkEnum.DEAL_MIN_DELIVERY_DATE_UPDATE.description, {
data: {
iDealId: 1
},
}).then(function (response) {
}, function (response) {
});
Теперь мы можем импортировать наши константы в другие модули и делать с ними запросы к бекенду через библиотеку битрикса.
Также, как мы бы использовали подобный подход на PHP.
enum ConstantsEnum: string
{
case CSE_TRACKING_LINK = 'https://www.cse.ru/mow/track?numbers=';
}
printf('Result:%s
', print_r (ConstantsEnum: CSE_TRACKING_LINK→value, true));
Для тех, кому нужно напомнить, как организовать контроллер на бекенде: пройдите курс либо просто используйте обычный AJAX, а вместо endpoints — адреса API.
2. По аналогии создадим модуль для работы с календарем. Он будет содержать методы, изменяющие битрикс-календарь. Так как это не часть бизнес-логики, а дополнительный функционал, отнесем его к инфраструктуре: local/js/App/Infrastructure/BXCalendar/src/BXCalendar.js
import {Type} from 'main.core';
/**
* Доработки для стандартного календаря.
*/
export default class BXCalendar {
constructor(options = {name: 'BXCalendar'}) {
this.name = options.name;
}
setName(name) {
if (Type.isString(name)) {
this.name = name;
}
}
getName() {
return this.name;
}
/**
* Метод деактивирует даты в календаре, до определенной даты.
* @param $DOMInput Инпут, на который применяется ограничение
* @param $sDate Дата, до которой отключить выбор.
*/
static disableDatesBefore($DOMInput, $sDate) {
if(typeof $DOMInput !== "undefined") {
const iMinDate = Date.parse($sDate);
// В календаре добавим проверку выбора дат.
let $el = BX.calendar({
node: $DOMInput,
field: $DOMInput.name,
form: '',
bTime: true,
bHideTime: false,
callback: function (sPickedDate) {
const currentDate = BX.date.format("c", new Date());
const pickedDate = BX.date.format("c", sPickedDate);
const iCurrentDate = Date.parse(currentDate);
const iPickedDate = Date.parse(pickedDate);
if (iPickedDate < iCurrentDate) {
BX.adjust($DOMInput, {
props: {
value: ''
}
});
BX.UI.Notification.Center.notify({
"content": "Нельзя выбрать прошедшую дату.",
})
return false;
} else if (iPickedDate < iMinDate) {
BX.adjust($DOMInput, {
props: {
value: ''
}
});
BX.UI.Notification.Center.notify({
"content": "Невозможно выбрать дату до: " + BX.date.format("d.m.Y", new Date($sDate)),
})
return false;
} else {
BX.adjust($DOMInput, {
props: {
value: BX.date.format("d.m.Y", new Date(pickedDate))
}
});
}
return true;
}
});
//найдем элементы отображающие дни
let links = $el.DIV.querySelectorAll(".bx-calendar-cell");
let date = new Date($sDate);
for (let i = 0; i < links.length; i++) {
let atrDate = links[i].attributes['data-date'].value;
let d = date.valueOf();
let g = links[i].innerHTML;
//меняем класс у элемента отображающего день, который меньше по дате чем текущий день
if (date - atrDate > 0) {
$('[data-date="' + atrDate + '"]').addClass("bx-calendar-date-hidden disabled");
}
}
} else {
}
}
}
Теперь у модуля есть статичный метод, в который мы передаем элемент, к которому привязан календарь, и дату, до которой необходимо отключить выбор чисел.
3. Добавляем модуль для работы со сделками: local/js/Domains/deals
. Лично мне нравится переименовывать его в DealsService.js
по аналогии с бекендом, но это не принципиально.
Также на основе бекенда контроллеры хочется вынести в отдельный неймспейс App.Domains.Deals.controller
. Для этого мы установим подмодуль в папку controllers
. Кроме того, понадобятся события, которые будет отслеживать модуль, реагирующие на открытие попапа, в котором отображаются сделки. Для них сделаем подмодуль events
.
Родительский модуль при инициализации вызывает метод load()
, который подключает слежение за событиями.
import {Type} from 'main.core';
import {DealsEvents} from '../events/src/DealsEvents'
export class DealsService {
constructor(options = {name: 'DealsService'}) {
this.name = options.name;
}
setName(name) {
if (Type.isString(name)) {
this.name = name;
}
}
getName() {
return this.name;
}
load() {
const $DealsEvents = new DealsEvents();
$DealsEvents.sliderLoadEvents();
$DealsEvents.sliderOpenEvents();
}
}
new DealsService().load();
Модуль событий отслеживает открытие и загрузку слайдера, так как в битриксе эти события разные и срабатывают в разных ситуациях, а нам нужно поведение для обоих вариантов.
import {Type} from 'main.core';
import {DealsController} from "../../controller/src/DealsController";
import {EntityTypeEnum} from "../../../enums/src/Enums";
/**
* @module BX.App.Domains.Deals.Events
*/
export class DealsEvents {
/**
* @constructor
* @param options
*/
constructor(options = {name: 'DealsEvents'}) {
this.name = options.name;
}
setName(name) {
if (Type.isString(name)) {
this.name = name;
}
}
getName() {
return this.name;
}
/**
* События по открытию слайдера.
*/
sliderOpenEvents() {
// Установка даты минимальной доставки
BX.addCustomEvent("SidePanel.Slider:onOpen", function (event) {
if (event?.slider?.minimizeOptions?.entityType === EntityTypeEnum.DEALS.description) {
const $controller = new DealsController()
const $dealId = event.slider.minimizeOptions.entityId;
$controller.updateMinDlvDate($dealId);
}
})
}
sliderLoadEvents() {
BX.addCustomEvent("SidePanel.Slider:onLoad", function (event) {
if (event?.slider?.minimizeOptions?.entityType === EntityTypeEnum.DEALS.description) {
/**
* Установка даты доставки
*/
const $dealId = event.slider.minimizeOptions.entityId;
setTimeout(async () => {
const mutationObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation?.removedNodes[0]?.dataset?.fieldTag === "UF_DEAL_DLV_DATETIME") {
setTimeout(() => {
const $Event = new DealsController()
$Event.blockDates($dealId)
}, 1000)
}
});
});
mutationObserver.observe(document, {
childList: true,
subtree: true,
characterDataOldValue: true
});
}, 1000);
/**
* Добавляем кнопки с остатками.
*/
DealsController.addAvailableCountButtons($dealId);
}
});
}
}
Соответственно, мы обращаемся к контроллеру, который уже имеет набор методов для работы со сделками.
import {Type} from 'main.core';
import BXCalendar from '../../../../Infrastructure/BXCalendar/js/BXCalendar'
import './DealsController.css'
import {ActionsLinkEnum} from "../../../enums/src/Enums";
export class DealsController {
constructor(options = {name: 'DealsController'}) {
this.name = options.name;
}
setName(name) {
if (Type.isString(name)) {
this.name = name;
}
}
getName() {
return this.name;
}
/**
* Получить минимальную дату доставки и установить её в сделке
* @param $dealId Ид сделки.
* @returns void
* пишет в консоль SON {"success": bool, "message": string}
*/
updateMinDlvDate($dealId) {
BX.ajax.runAction(ActionsLinkEnum.DEAL_MIN_DELIVERY_DATE_UPDATE.description, {
data: {
iDealId: $dealId
},
}).then(function (response) {
}, function (response) {
});
}
/**
* Заблокировать выбор даты меньше минимальной даты доставки
* @param dealId
* @returns {void}
*/
blockDates(dealId) {
let dateInput = $("input[name='UF_DEAL_DLV_DATETIME']").get()[0];
let divCalendar = $("div[class='bx-calendar-button-block']").get()[0];
divCalendar.style.visibility = 'hidden';
BX.ajax.runAction(ActionsLinkEnum.DEAL_MIN_DELIVERY_DATE_GET.description, {
data: {
iDealId: dealId
},
}).then(function (response) {
divCalendar.style.visibility = 'visible';
let obResponse = JSON.parse(response.data)
if (obResponse.success) {
const minDate = obResponse.message;
BXCalendar.disableDatesBefore(dateInput, minDate)
} else {
}
}, function (response) {
});
}
/**
* Метод добавляет вывод информации об остатках на складах для сделок.
*
* @param dealId Ид сделки.
*/
static addAvailableCountButtons(dealId) {
console.log(‘секретик))’);
}
}
Все, структура есть, модули созданы. Осталось подключить их на бекенде в нужном нам месте. Создадим класс для работы с расширениями:
И подключим в шаблоне, либо прямо в init.php
, если требуется на всех страницах.
Осталось собрать наш фронтенд. Для этого возвращаемся в папку local и запускаем сборку:
Ну вот и все! Завершился наш цикл о переходе от модульной архитектуры к сервисам в битрикс. Делитесь в комментариях, на какие темы вы хотели бы увидеть следующие тексты.
Кстати, пока вы ждете новых статей, подпишитесь на канал DD Planet! Там вы найдете не только мои тексты, но и замечательные материалы от коллег по цеху.