[Записки тимлида] Битрикс: от модулей к сервисам 3

Автор: Денис Закусило

Приветствую всех неравнодушных! Это заключительная статья цикла о переходе от модульной архитектуры к сервисам.

[Записки тимлида] Битрикс: от модулей к сервисам

[Записки тимлида] Битрикс: от модулей к сервисам 2

Сегодня мы рассмотрим организацию структуры frontend стороны приложения.

f7c40e28abee97abd19f80ab9c0f637c.png

Первым делом нам необходимо подключить 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.

3f3a4cd0aacd8f58feebc5909db9e692.png

Выполним практическую задачу: Для сделки необходимо получать минимальную дату доставки от бекенда и запрещать оператору выбирать более ранние даты.

Решение:

1. Для начала нам нужно определить место для endpoint list, в котором было бы организовано хранилище адресов для запросов на бекенд.

Сделаем по аналогии с Enums php:

Enum в PHP 8.1 — для чего нужен enum, и как реализован в PHP

Через несколько дней заканчивается голосование по первой итерации реализации enum в PHP 8.1 . Уже ви…

habr.com

Мы создадим класс, который будет хранить константы.

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.

441fca61f0eeac004047b3c6ba0a2afc.png

Родительский модуль при инициализации вызывает метод 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, если требуется на всех страницах.

98d42a8d8ea2c3d552751292bf457dae.png

Осталось собрать наш фронтенд. Для этого возвращаемся в папку local и запускаем сборку:

d92fb71482fa428a2e43366002e8aa40.png

Ну вот и все! Завершился наш цикл о переходе от модульной архитектуры к сервисам в битрикс. Делитесь в комментариях, на какие темы вы хотели бы увидеть следующие тексты.

Кстати, пока вы ждете новых статей, подпишитесь на канал DD Planet! Там вы найдете не только мои тексты, но и замечательные материалы от коллег по цеху.

© Habrahabr.ru