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


Интересные статьи

Интересные статьи

Попадание внутренней корпоративной информации в чужие руки — страшный сон для любого бизнеса. Последствия таких инцидентов могут быть непредсказуемыми, от утечек данных клиентов и сопутствующих штрафо...
Система отслеживания звонков, она же кол-трекинг – верный друг маркетологов и предпринимателей. Это она помогает понять, какие каналы работают лучше всего, как оптимизировать рекламные бюджеты, какие ...
У нас был сложный сайт с личным кабинетом клиентов, устаревшая, переписанная 1С-ка, десяток маркетинговых сервисов, и телефония на Asterisk.Единственное, что вызывало у меня опасение — это учётная сис...
⇨ Первая часть И снова здравствуйте! Как вы могли заметить, праздники несколько подкосили график выхода статей. Думаю, многие за это время успели если не полностью обучить свою модель, то...
Инженер компании Nissan сделал робота, который перемешивает слои воды на рисовых плантациях, снижая ее прозрачность, чтобы растущие там сорняки получали меньше солнечного света. ...