[Записки тимлида] Битрикс: от модулей к сервисам 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 . Уже ви…

habr.com

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

1.1 Создаем модуль enums и переходим в директорию доменов. Для этого заходим в контейнер, переходим в домен и запускаем команду bitrix build enums:

docker compose exec -T -u node node sh    cd <project_dir>/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("<module>.api.ActionController.getStoreByDeal"),     DEAL_MIN_DELIVERY_DATE_GET: Symbol("<module>.api.ActionController.getMinDeliveryDate"),     DEAL_MIN_DELIVERY_DATE_UPDATE: Symbol("<module>.api.ActionController.updateMinDeliveryDate"),     DEAL_ORDER_CANCEL: Symbol("<module>.api.ActionController.cancelOrderByDeal"),     DYNAMIC_NOT_AGREE_CONTACTS_GET: Symbol("<module>.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:<pre>%s</pre><hr />', 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(‘секретик))’);     }  }

Все, структура есть, модули созданы. Осталось подключить их на бекенде в нужном нам месте. Создадим класс для работы с расширениями:

<?php  namespace App\Shared\Enums;  enum JSDomainsEnums: string  {     case DEALS = 'App.Domains.deals';     case VISITS = 'App.Domains.visits';     case CONTACTS = 'App.Domains.contacts';  }

И подключим в шаблоне, либо прямо в init.php, если требуется на всех страницах.

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/874078/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *