Kotlin? Swift? Flutter? Нет, кириллица: пишем production-приложение с картой и push-ами на 1С: Элем…
- вторник, 11 августа 2026 г. в 00:00:10
Привет, Хабр! (И тебе, 1С-ник, который открыл статью только чтобы убедиться, что пока точно не надо изучать новый продукт. И тебе, питонист, который сейчас будет морщиться. И тебе, мобильный разработчик, который уверен, что без Kotlin/Swift нормальное приложение не родится - держи кружку крепче.)
Эта статья будет посвящена технологии, на которой можно написать своё мобильное приложение за пару дней. И я вам на примере реализованного мною полноценного проекта логистического приложения покажу, что сейчас из себя представляет эта технология на нашей родной КИРИЛЛИЦЕ, и что на ней можно сделать прямо сейчас. И самое главное, как…
Статья получилась большой и охватывающей множество областей разработки и знание 1С здесь вообще не нужно. А для большего понимания я буду всё сравнивать с популярными ЯП. Что бы вы - мои любители латиницы познакомились, сравнили и дали вердикт - это кринж или норм.
Маленькая ремарка для тех, кто читает меня впервые. Я не 1С-ник в классическом смысле (вообще нет). Я Python и JS разработчик (получается full-stack), который по иронии судьбы пишет целый проект на этом чуде-юде. Так что смотреть на всё я буду из своего опыта, а не из методички (тем болеем инфы там ой как не хватает). Так что думаю будет интересно)))
Есть одно очень крупное предприятие в России. У предприятия - своя логистика (логично): грузовики, водители, диспетчеры, мастера на точках погрузки-разгрузки. Всё это крутится вокруг внешней системы УАТ (управление автотранспортом), которая живёт своей жизнью и общается с внешним миром через REST API - местами логичный, местами такой, что хочется позвонить его автору и молча подышать в трубку.
Задача звучала так: нужно мобильное приложение, через которое:
Мастер создаёт заказ на перевозку - тыкает точки маршрута на карте, подбирает автомобиль, отправляет в работу, редактирует заказы;
Водитель видит свои заказы, жмёт на "Принять заказ", "Прибыл на точку", "Погрузка начата", "Убыл" - и всё это в реальном времени улетает в УАТ;
Мастер на точке подтверждает погрузку/разгрузку и получает пуши, когда к нему едет машина;
Диспетчера мониторят все заказы через более детальный мониторинг заказов и так же может редактировать;
А администратор настраивает всю эту кухню: пользователей, интеграцию, сертификаты для push-уведомлений и даже тексты этих самых пушей.
То есть как вы уже понимаете это не "Hello, World с кнопочкой", а полноценное production-приложение: карта с маркерами, машина состояний заказов, push-уведомления через FCM, живая двусторонняя интеграция с внешним API и мультиролевой интерфейс, который на телефоне, планшете и десктопе выглядит по-разному.
А теперь то, ради чего половина из вас и открыла статью.
Всё это написано не на Kotlin, не на Swift, не на React Native и даже не на Flutter.

Это написано на 1С:Элемент - новом языке и платформе от 1С (называю новым так как даже два года для языка программирования - это ещё очень мало).
А Элемент это те ещё компромиссы…

Зачем я это пишу?
Во-первых, про 1С:Элемент на Хабре почти ничего нет (не считая моих статей конечно же). В основном здесь пресс-релизы в духе "революционная платформа", а живого production-кода с граблями - ноль! Можно посмотреть и в интернете, но даже когда там вводишь "1С:Элемент", то мои статьи вылазят на первой странице поиска. А полноценных и честных разборов проектов нет, стесняются что ли? Исправляю это.

Во-вторых, мне реально есть что показать: null-safety, RPC клиент-сервер из коробки, генерацию UI прямо в коде, машину состояний и интеграцию, которая переживает падение внешнего сервера. Кое-что меня восхитило. Кое-что заставило вставить Пауза(120мс) и сделать вид, что так и было (каюсь, и про грабли тоже будет и про смирение, а куда без этого).
Короче покрутим язык со всех сторон в призме проекта.
Проект мы назвали titransflow. Работал над ним я два месяца, и работаю до сих пор. На старте была только интеграция с УАТ по HTTP - и всё. Тут, в отличие от моего прошлого опыта, уже была версия платформы 9.1, а не 5.4. Изменился ли язык за четыре версии? Не сказать. Пара доработок по отдельным направлениям, документация местами всё так же не стыкуется с реальностью. Всё как обычно. Печально.
Короче, думаю, вводной части хватит. По чуть-чуть начну погружать вас в данную технологию и по мере понимания происходящего переходить к реализации проекта. Поехали.
Kotlin-разработчики, вам сюда, будете смеяться от узнавания. В Python я долго любил нестрогую типизацию. Потом, пересев на Элемент, начал возмущаться на строгую: мне виднее, что летит через функцию, не технологии решать. А поработав над полноценным проектом, я понял, зачем строгость нужна. Вот переменная, которая обречена всю жизнь быть массивом структур СтрокаМаршрутаЗаказа:
пер ОбновленныеТочкиМаршрута: Массив<СтрокаМаршрутаЗаказа>

Опциональная цепочка и дефолт в одну строку - прямо из учебника по Swift/Kotlin:
метод ТекущийСтатусТочки(Заказ: Заказы.Ссылка?, ИдентификаторСтрокиЗаказа: Ууид?): ВидыСтатусовТочекЗаказов … возврат РезультатЗапроса.ЕдинственныйИлиНеопределено()?.Статус ?? ВидыСтатусовТочекЗаказов.Новая ;
ЕдинственныйИлиНеопределено() вернёт либо строку результата, либо Неопределено. ?.Статус безопасно достаёт поле, а ?? подставляет значение по умолчанию, если записи не нашлось. Одна строка вместо блока с вложенными проверками. Заодно обратите внимание, что метод имеет тип возвращаемого значения - ВидыСтатусовТочекЗаказов.
Пока всё просто. Смысл я думаю улавливаете.
А вот force-unwrap. Когда я уверен, что значение точно есть, ставлю ! и беру ответственность на себя:
Данные.СозданныйЗаказ = Перевозки::Сервисы::DTO.ОтправитьЗаказВУАТ(Данные) знч ОбъектЗаказа = ДанныеЗаказа.СозданныйЗаказ!.ЗагрузитьОбъект()
Заказ тут гарантированно создан, поэтому ставлю ! и беру ответственность на себя. Это как сказать компилятору “я те зуб даю”. Совру - прилетит в рантайме. Это же 1С - здесь всё должно быть по пацански))
Но иногда типизация вырождается в монстра. Вот свойство из формы выбора даты, которое умеет принимать почти всё, что похоже на дату:
Тип: Дата|ДатаВремя|Момент|Время|ЗакрытыйДиапазон<Дата>|ЗакрытыйДиапазон<ДатаВремя>|ЗакрытыйДиапазон<Момент>|ЗакрытыйДиапазон<Время>|?
Восемь типов через палочку и ? на закуску. Гибко. Но когда встречаешь такое в чужом коде в пятницу вечером, начинаешь тихо философствовать о смысле бытия. Формально можно было поставить тип неизвестно, но тогда по всему коду пришлось бы городить обработки вида Дата как Дата или Дата как Момент. А ради чего всё это? Ради общего компонента, который принимает любую дату. Грустно. Так что строгая типизация это конечно хорошо, но не всегда уместна...
Функциональщина тут живёт полноценно, а не для галочки.
Эй народ, у нас тоже есть map!
Фильтрация и маппинг в модуле прав - достаём из массива пользователей с конкретной ролью:
знч ПользователиСРолью = ПользователиИРоли .Фильтровать(Стр -> Стр.ТипСотрудника == Ключ.Роли) .Преобразовать(Стр -> Стр.Пользователь)
Python-разработчик прочитает это как filter().map(), Kotlin-разработчик - как .filter{}.map{}, а 1С-ник со стажем прочитает это и заплачет, потому что у него только Для Каждого ... Цикл на семь строк ради одной трансформации, а у его младшего брата Элемента всё продумано. Нравится, берём))
Здесь Элемент показал, что корни 1С не забыл, но переосмыслил (ну а что, в 1С удобно всё с запросами, да, есть но, ну а где их нет). Язык запросов живёт прямо в коде, и это не строка, а типизированный литерал с проверкой на этапе компиляции. Запрос из машины состояний - достаём последний статус заказа:
знч Запрос = Запрос{ ВЫБРАТЬ ПЕРВЫЕ 1 СтатусыЗаказов.Статус КАК Статус ИЗ СтатусыЗаказов.СрезПоследних КАК СтатусыЗаказов ГДЕ СтатусыЗаказов.Заказ == %{Заказ} } исп РезультатЗапроса = Запрос.Выполнить()
Тут важны две вещи. Первая - %{Заказ} подставляет параметр безопасно, а не склеивает строку (питонисты и джависты поняли, о чём я). Вторая - исп. Это using-блок, ресурс сам закроется после использования. Можно и без него, просто выполнив Запрос{}.Выполнить().
А вот совсем красиво - подстановка результата лямбды над коллекцией прямо в ГДЕ:
ГДЕ Пользователь В (%{Пользователи.Преобразовать(Стр -> Стр)}) И ТипСотрудника В (%{Ключи.Преобразовать(Стр -> Стр.Роли)})
Прямо в параметре запроса массив ключей превращается в массив ролей, и всё это типизировано. Мне это сэкономило кучу промежуточных переменных, да и для вас выглядит понятно.
Справедливости ради, язык запросов умеет быть и страшным. Есть метод, который ищет дату последнего изменения статусов по сотруднику: три подзапроса, ОБЪЕДИНИТЬ ВСЕ, НЕ СУЩЕСТВУЕТ, соединения по разнарядке, полэкрана текста. Но виноват тут не язык. Просто таков путь...)
Здесь у кого-то должна отвиснуть челюсть. В проекте работает честная инверсия зависимостей через механизм контрактов.
Задача была такая. Подсистема Перевозки умеет вести заказы и гонять их по статусам, но не должна знать, через какую внешнюю систему заказы уходят. Сегодня это УАТ, завтра что угодно. Поэтому объявляется контракт, по сути, интерфейс:
абстрактный метод НачатьДвижениеКТочке( Заказ: Заказы.Ссылка, Точка: ТочкиДоставки.Ссылка, Порядок: Число, Широта: Число?, Долгота: Число?, Одометр: Число?): РезультатИсполненияЗаказа?
Реализация лежит в отдельной подсистеме - в модуле интеграции с УАТ, помеченная @Реализация:
@Реализация @ВПроекте метод НачатьДвижениеКТочке(Заказ: Заказы.Ссылка, Точка: ТочкиДоставки.Ссылка, Порядок: Число, ...): РезультатИсполненияЗаказа? возврат ВыполнитьПереходИсполнения(Заказ, Точка, Порядок, "ON_THE_WAY", "START", ...) ;
Перевозки получают реализацию, ничего о ней не зная:
метод ИсточникиИсполненияЗаказов(): ИсполнительЗаказов знч СервисИсполнительЗаказов = ИсполнительЗаказов.ПолучитьСервис() если СервисИсполнительЗаказов == Неопределено выбросить новый ИсключениеВыполнения("Не реализован исполнитель заказов") ; возврат СервисИсполнительЗаказов! ;
ИсполнительЗаказов.ПолучитьСервис() - платформа сама находит реализацию контракта. Есть и множественный вариант ПолучитьСервисы(). Через него, например, работают источники НСИ: каждый источник данных реализует контракт ИсточникНСИ и обновляет справочники по-своему, а вызывающий код просто перебирает все реализации.
Тот же механизм собирает главный интерфейс приложения. Каждая подсистема сама решает, какие разделы показать, реализуя контракт ОсновнойИнтерфейс.
@ВПроекте @Реализация метод РазделыПриложения(): ЧитаемаяКоллекция<ОсновнойИнтерфейсКлиент.ОписаниеРаздела> знч Разделы: Массив<ОсновнойИнтерфейсКлиент.ОписаниеРаздела> если ИспользованиеСпискаЗаказов() Разделы.Добавить(ОписаниеРазделаПолучениеЗаказа()) ; если ИспользованиеРазделаСозданиеЗаказа() Разделы.Добавить(ОписаниеРазделаСозданиеЗаказа()) ; возврат Разделы ;
А ядро приложения просто собирает всё со всех подсистем:
знч Сервисы = ОсновнойИнтерфейс.ПолучитьСервисы() для Сервис из Сервисы РазделыПриложенияСведения.ДобавитьВсе(Сервис.РазделыПриложения()) ;
Подсистемы не лезут друг в друга напрямую. Перевозки не зависят от Интеграции, хотя именно Интеграция реализует их контракт. В энтерпрайзе на Java или C# за такую развязку зависимостей платят архитекторам, а тут это встроено в платформу.
Так что я надеюсь, что на данном этапе у вас возникала хотя бы мысль, что Элемент не плохой язык. А если нет продолжаем…
Об этом я рассказывал в прошлой статье, но повторю, потому что для фуллстекера это выглядит как читерство: никакого отдельного бэкенда, ручного fetch и мыслей про эндпоинты.
Реальный обработчик кнопки "Принять":
метод ПринятьПриНажатии(Источник: Кнопка, Событие: СобытиеПриНажатии) если Заказ.Ссылка == Неопределено возврат ; знч Результат = ПринятьЗаказНаСервере(Заказ.Ссылка!) если КомандыФорм.УведомитьОИсключениеВыполнения(Результат) возврат ; ОбновитьСтатусИКоманды() ; @НаСервере @ДоступноСКлиента статический метод ПринятьЗаказНаСервере(Заказ: Заказы.Ссылка): ИсключениеВыполнения? возврат СостоянияЗаказов.ПринятьЗаказ(Заказ) ;
Клиентский метод (в браузере или на телефоне водителя) вызывает ПринятьЗаказНаСервере как обычную функцию. Аннотации @НаСервере @ДоступноСКлиента говорят платформе: метод выполняется на сервере, а вызывать его в принципе можешь и с клиента. RPC генерируется сам.
Отдельно отмечу подход к ошибкам. Логично, что если ошибка на сервере, то на интерфейсе мы её не отобразим, потому что не видим. По этому серверный метод возвращает ИсключениеВыполнения?, то есть ошибка едет обратно как значение, а не летит исключением через всю сеть. УведомитьОИсключениеВыполнения проверяет результат и показывает сообщение пользователю. Без этого нам бы пришлось вызывать типовую ошибку.
Конечно всё это расплата за удобство - надо постоянно держать в голове, где что выполняется. Каждый модуль помечен окружением: Клиент, Сервер или КлиентИСервер. Дёрнуть серверный справочник из клиентского кода не выйдет - синтаксическая ошибка ещё до запуска. В прошлой статье я на это ворчал, а в бою понял, что лучше поймать ошибку компиляции, чем ловить её потом.
Модуль СостоянияЗаказов - сердце проекта. Взгляните на неё.

Доступная команда вычисляется от текущего состояния и роли пользователя одновременно:
Четвёртый параметр Истина у команды "Прибыл на точку" - это ТребуетГеопозицию.
Каждый переход защищён проверками:
метод ПрибылНаТочку(Заказ: Заказы.Ссылка): void ПроверитьСтатусЗаказа(Заказ, ВидыСтатусовЗаказов.ВРаботе) ПроверитьРоль(ТипыСотрудников.Водитель) знч СтрокаМаршрута = АктивнаяСтрокаМаршрута(Заказ) если СтрокаМаршрута == Неопределено выбросить новый ИсключениеВыполнения("Не найдена активная точка маршрута") ; ПроверитьСтатусТочки(Заказ, СтрокаМаршрута.ИдентификаторСтроки, ВидыСтатусовТочекЗаказов.ОжиданиеПрибытияАвтомобиля) СценарииЗаказов.ПрибылВТочку(Заказ, СтрокаМаршрута.ТочкаМаршрута, СтрокаМаршрута.Порядок) УстановитьСтатусТочки(Заказ, СтрокаМаршрута, ВидыСтатусовТочекЗаказов.ОжиданиеПогрузкиРазгрузки) ;
Метод проверяет статус заказа, роль пользователя, находит активную точку, проверяет её статус. Только после этого дёргает сценарий (который через контракт из главы 4 сходит в УАТ) и переводит точку дальше. Перевести заказ в некорректное состояние невозможно.
Набираем темп и теперь уже переходим к более интересному. На мой взгляд))
Заказ прошёл по статусам, водитель принял, поехал - и на каждом шаге участники должны получить уведомление. Тут уже, наверное, не совсем про язык будет, а скорее про реализацию так как вариантов развития событий целый грузовик.
Сначала устройство надо зарегистрировать. При старте приложение на Android получает идентификатор подписчика уведомлений и сохраняет его:
@ВПроекте @НаКлиенте метод ЗарегистрироватьУстройство() если КлиентскоеУстройство.ВидПлатформы == ВидПлатформыКлиента.Android знч Идентификатор = ДоставляемыеУведомления.ПолучитьИдПодписчикаУведомлений() ИдентификаторыУстройств.ИдентифицироватьУстройство(Идентификатор.ИдУстройства, КлиентскоеУстройство.ВидПлатформы) ; ;
Кстати, почему только Android? Ведь можно и приложение для IOS выпустить. Просто на производстве у всех мастеров и водителей рабочие устройства на Android, и даже нет смысла заморачиваться с IOS. Тыдынь-тыщь...
Идентификаторы хранятся в регистре сведений и привязаны к пользователю. Один пользователь может залогиниться с нескольких устройств (телефон водителя плюс планшет, к примеру), поэтому это именно коллекция идентификаторов, а не одно поле.
Когда заказ меняет статус, машина состояний дёргает сервис уведомлений. Метод собирает участников заказа и рассылает каждому push по шаблону.
@ВПодсистеме метод ОтправитьУведомленияПоСобытиюЗаказа(Заказ: Заказы.Ссылка, КатегорияСобытия: КатегорииУведомленийПеревозки, ИсключитьПользователя: Пользователи.Ссылка? = Неопределено) знч ОбъектЗаказа = Заказ.ЗагрузитьОбъект() знч Участники = ЗаказыСервер.ПолучитьУчастниковЗаказа(Заказ) для Участник из Участники пер ПользовательИзСотрудника = Участник.Сотрудник.ЗагрузитьОбъект().Пользователь если ПользовательИзСотрудника != Неопределено и ИсключитьПользователя != ПользовательИзСотрудника пер СтрокаФильтраСтроковогоРесурса = новый Основное::СтрокаФильтраСтроковогоРесурса( Наименование = "PUSH_УВЕДОМЛЕНИЕ_ПЕРЕВОЗКИ", КатегорияПервостепенная = КатегорияСобытия.Представление(), КатегорияВторостепенная = Участник.КатегорияУчастника.Представление() ) Основное::УведомленияПриложения.ОтправитьУведомлениеПользователюПоШаблону( ПользовательИзСотрудника, СтрокаФильтраСтроковогоРесурса, ПараметрыШаблона = { "НОМЕР": "№%{ОбъектЗаказа.Номер}", ... } ) ; ; ;
Здесь важно то, что уведомление адресное. Водитель получает "Вам назначен заказ №…", диспетчер - "Водитель прибыл на точку", мастер - "Ваша заявка выполнена". Кто и что получит, решает ПолучателиУведомления в связке с ролью и событием, ну и спасибо СтрковымРесурсам о которых будет дальше.
Затем за дело принимается сама отправка уведомления...
знч Уведомления = <ДоставляемоеУведомление>[] Уведомления.Добавить( новый ДоставляемоеУведомление( Ид = новый Ууид().ВСтроку(), Заголовок = Заголовок, Текст = Текст, Триггер = новый ТриггерДоставляемыхУведомленийПоСпискуПолучателей(Получатели = Получатели) ) ) знч ДанныеАунтификацииFcm = новый ДанныеАутентификацииFcm( КонстантыПриложения.ИдентификаторПриложенияПоУмолчанию(), СертификатыPUSH.ПолучитьСертификат().Загрузить().ОткрытьПотокЧтения().ПрочитатьКакБайты() ) знч Результат = ОтправкаДоставляемыхУведомлений.Отправить(Уведомления, [ДанныеАунтификацииFcm])
Здесь смысла нет останавливаться и демонстрировать код целиком, я, по сути, сделал всё как в методичке только храню и получаю сертификат от Гугл фаирбейс не в ресурсах системы (так как не является данное решение безопасным), а в шифрованном хранилище. А так здесь всё просто под капотом Элемента скрыта отправка PUSH через гугл. Точно так же, как я реализовал это для примера на Python, только из коробки:
import requests from google.oauth2 import service_account import google.auth.transport.requests def get_access_token(): credentials = service_account.Credentials.from_service_account_file("ФАЙЛИК_СЕРТИФИКАТА_ГУГЛ.json", scopes=["https://www.googleapis.com/auth/firebase.messaging"]) credentials.refresh(google.auth.transport.requests.Request()) return credentials.token def send_push(device_token): access_token = get_access_token() headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json; UTF-8",} payload = {"message": {"token": device_token, "notification": {"title": "Тебе пришла СМС-ка", "body": "Пора бы съездить на заказ"}}} response = requests.post(f"https://fcm.googleapis.com/v1/projects/{"ID_ПРОЕКТА"}/messages:send", headers=headers, json=payload)
Отдельная радость была отлаживать это всё… Push на эмуляторе Android (Да тут вам только стоит задуматься, что я могу тестировать технологию 1С на эмуляторе Андроид).
Но тут тоже не без приколов... Push на эмуляторе Android живёт по законам квантовой механики как я понял: пока не посмотришь - непонятно, пришёл он или нет. Он приходит в суперпозиции "сразу или через 10 минут или никогда". Сидишь в наушниках пишешь код и приходит уведомление которые ты уже не помнишь, когда и отправил в эмуляторе, и музыка в наушниках прерывается на громкий звон прихода PUSHа. Чистый рандом… И дёргающийся глаз. Но в 90% случаев всё нормально.

То, что выше я называется СтроковыеРесурсы, - это не встроенный механизм локализации Элемента (хотя тот присутствует в Элементе). Это моя собственная подсистема, и придумал я её из вполне конкретной боли.
Ради одной запятой в тексте уведомления или системного сообщения не хочется перекомпилировать код и выкатывать релизы. Мало-ли грамматическая ошибка… Всякое бывает…
Поэтому я сделал сразу так, чтобы все тексты приложения жили не в коде, а в справочнике, который администратор системы редактирует прямо из интерфейса, в специальном разделе настроек. Без разработчика, без сборки, без выкатки.
Устроено это следующим образом. Есть справочник строковых ресурсов, где каждая запись - это ключ, привязки, и сам текст с именованными плейсхолдерами (фильтр - тегами). Один первостепенный. Например, ресурс НовыйЗаказНазначен, а другой второстепенный который может быть адресован к конкретной роли. Вообще всё равно к чему, но предположим.
И из этих 2-х фильтров плюс ключ привязки у нас получается то или иное сообщение.

Обращение из кода идёт через сервис, который по подсистеме и ключу достаёт актуальный текст и подставляет параметры:
@ВПроекте метод ПолучитьСтроковыйРесурс(Фильтрация: СтрокаФильтраСтроковогоРесурса, ПараметрыШаблона: Соответствие<Строка, Строка>? = Неопределено): СтрокаЗначенийСтроковогоРесурса? знч РезультатЗапроса = Запрос{ ВЫБРАТЬ ПЕРВЫЕ 1 СтроковыеРесурсы.ОсновноеЗначение КАК ОсновноеЗначение, СтроковыеРесурсы.ДополнительноеЗначение КАК ДополнительноеЗначение, СтроковыеРесурсы.Использовать КАК Использовать ИЗ СтроковыеРесурсы КАК СтроковыеРесурсы ГДЕ СтроковыеРесурсы.Наименование == %{Фильтрация.Наименование} И ( %{Фильтрация.КатегорияПервостепенная} == Неопределено ИЛИ СтроковыеРесурсы.КатегорияПервостепенная == %{Фильтрация.КатегорияПервостепенная} ) И ( %{Фильтрация.КатегорияВторостепенная} == Неопределено ИЛИ СтроковыеРесурсы.КатегорияВторостепенная == %{Фильтрация.КатегорияВторостепенная} ) }.Выполнить().ЕдинственныйИлиНеопределено() если РезультатЗапроса != Неопределено пер ОсновноеЗначение = РезультатЗапроса.ОсновноеЗначение пер ДополнительноеЗначение = РезультатЗапроса.ДополнительноеЗначение пер Использовать = РезультатЗапроса.Использовать если ПараметрыШаблона != Неопределено для КлючПараметра из ПараметрыШаблона.Ключи() пер ШаблонЗамены = "\$%{КлючПараметра}\$" ОсновноеЗначение = ОсновноеЗначение.Заменить(ШаблонЗамены, ПараметрыШаблона[КлючПараметра]) ДополнительноеЗначение = ДополнительноеЗначение.Заменить(ШаблонЗамены, ПараметрыШаблона[КлючПараметра]) ; ; знч Образец = '\$[^$]*\$' для Совпадение из Образец.НайтиСовпадения(РезультатЗапроса.ДополнительноеЗначение) ОсновноеЗначение = ОсновноеЗначение.Заменить(Совпадение.Значение(), "") ; для Совпадение из Образец.НайтиСовпадения(РезультатЗапроса.ДополнительноеЗначение) ДополнительноеЗначение = ДополнительноеЗначение.Заменить(Совпадение.Значение(), "") ; возврат новый СтрокаЗначенийСтроковогоРесурса(ОсновноеЗначение, ДополнительноеЗначение, Использовать) ; возврат Неопределено ;
Код возможно не идеальный но работает...
Здесь мы в примере видим, что мы, выполнив запрос получаем результат с подходящим строковым ресурсом. В нём могут лежать теги на те или иные объекты. Мы их находим с помощью регулярных выражений. Заменяем если данное значение имеется или просто затираем из текста если нет. И в результате получаем готовое сообщение с Заголовком и Текстом.
При первом развёртывании справочник Строковых ресурсов наполняется значениями по умолчанию из ресурсов подсистем. Дальше это уже данные, которые живут своей жизнью и правятся из интерфейса.
Тексты плашек на формах я гоняю через тот же механизм. Вот вызов сообщения на форме создания заказа, когда маршрут не заполнен:
Компоненты.ОсновноеСообщение.Сообщение( ОбщийМодуль.ИД_ПОДСИСТЕМЫ, ИД_ФОРМЫ, "СозданиеЗаказаНеЗавершено", ПараметрыШаблона, ВидПредупреждения = БиблиотекаЦветов.ВидПредупреждения.Ошибка )
Первые аргументы - координаты ресурса (подсистема, форма, ключ), дальше параметры подстановки. Компонент ОсновноеСообщение сам сходит в подсистему строковых ресурсов, достанет текст и покажет. Ни одна формулировка не зашита в логику формы.
Помните, в прошлой статье я ругался, что в Элементе нельзя подвинуть кнопку на три пикселя? (Я думаю не помните, но мало ли) И тогда я тут же упомянул лазейку - КонтейнерHTML, куда можно засунуть свой HTML/CSS/JS. Так вот, в этом проекте лазейка превратилась в полноценную фичу.

Диспетчеру нужна карта с машинами и точками маршрута. Штатной карты в Элементе, которая бы меня устроила, нет. Поэтому берём MapLibre GL и живём. Почему MapLibre? А просто потому, что красиво. Без сакрального смысла))
Внутри КонтейнерHTML лежит обычная веб-страница с картой, а общение между Элементом и JS идёт через мост:
метод ГенерацияКарты(): Строка пер ТемаКарты: Строка если КлиентскоеПриложение.ПолучитьТекущуюЦветовуюСхему() == ЦветоваяСхема.Светлая ТемаКарты = СтилиКарты.light_all.Представление() иначе ТемаКарты = СтилиКарты.dark_all.Представление() ; пер HTML_Карта = " <!DOCTYPE html> <html lang='ru'> ... /* Цвета по типам */ .pin.from { background: %{БиблиотекаЦветов.ЦветRGB(БиблиотекаЦветов.Цвет(БиблиотекаЦветов.ЦветаПриложения.Оранжевый))}; } .pin.to { background: %{БиблиотекаЦветов.ЦветRGB(БиблиотекаЦветов.Цвет(БиблиотекаЦветов.ЦветаПриложения.Синий))}; } .pin.via { background: %{БиблиотекаЦветов.ЦветRGB(БиблиотекаЦветов.Цвет(БиблиотекаЦветов.ЦветаПриложения.СреднеТемноСерый))}; } … sources: { 'carto-light': { type: 'raster', tiles: [ 'https://a.basemaps.cartocdn.com/%{ТемаКарты}/{z}/{x}/{y}@2x.png', 'https://b.basemaps.cartocdn.com/%{ТемаКарты}/{z}/{x}/{y}@2x.png', 'https://c.basemaps.cartocdn.com/%{ТемаКарты}/{z}/{x}/{y}@2x.png', 'https://d.basemaps.cartocdn.com/%{ТемаКарты}/{z}/{x}/{y}@2x.png' ], tileSize: 256, attribution: '© <a href=\"https://carto.com/\">CARTO</a>' } }, … center: %{КонстантыПриложения.КоординатыЦентрированияКарты()}, … </script> </body> </html> " возврат HTML_Карта ;
Обратите внимание на %{...} прямо внутри CSS и JS. Цвет маркера берётся из моей БиблиотекаЦветов, координаты центра - из констант приложения, тема карты - из системной цветовой схемы. То есть JS-карта автоматически получает фирменные цвета и стартовую точку конкретного цеха предприятия из типизированного кода, и мне не приходится дублировать эти значения в двух местах. Поменял оттенок в библиотеке -> поменялось и на карте (и вообще везде где этот цвет используется). Магия просто JS сначала является текстом, а потом трансформируется уже в код.
Обратный мост код Элемента -> JavaScript, я сделал через ВызватьМетод (спасибо Элемент, что подарил мне такую возможность):
@ВПроекте метод ДобавитьМаркер(Широта: Число, Долгота: Число, НазваниеТочки: Строка = "", ТипМаркера: Строка = "to") попытка Компоненты.Карта.ВызватьМетод("addMarker", [Широта, Долгота, ТипМаркера, НазваниеТочки]) поймать Исключение: Исключение ; ;
Когда мастер добавляет точку в маршрут заказа, я на форме вызываю Компоненты.КартаМаршрута.ДобавитьМаркер(…), и точка появляется на карте. Так же и с центрированием карты: я использую плавающую кнопку в интерфейсе элемента которая своим вызовом вызывает функцию в JS. Здесь язык мне не помог, а честно заставил спуститься в JS. Но я это спрятал за компонентом Карта с типизированными методами, и снаружи оно выглядит как обычный виджет. То самое, за что я ругал Элемент в первой статье, в итоге дало мне полноценную интерактивную карту в приложении. Здесь должны быть аплодисменты. Ну или картинку с пере обувкой, кому как)) Хотя если думать в корне это костыль, но приятный))
С таймером тот же прикол. В элемент попросту не получится никак реализовать таймер. Ну только если сделать обработчик по таймеру, который каждую секунду будет актуализировать время разницы между текущим временем и временем конкретного статуса, но это дичь. Поэтому я и сделал на JS таймер, которому при каждом обновлении страницы прилетает цифра текущей позиции, и он продолжает отсчитывать спокойно себе на фронте не зависимо от сервера. А при перезапуске также всё актуализирует и продолжит. Удобно))
Ещё хочу предупредить и напомнить: Не забывайте, что при работе с JS в Элементе, вы передаёте строкой поэтому все кавычки нужно экранировать \" и комментарии должны быть закрывающими (многострочными) /* КОД */. Иначе вам посыпятся такие инородные ошибки…
Ну и ещё вкуснятина. Больших сложных форм я почти не рисовал мышкой, да и не получилось бы динамическая подгрузка данных. Но хоть вы и видели уже, что формы делаются через yaml, Интерфейс я генерирую кодом xbsl, и вместе с компонентной моделью это дало невероятный уровень гибкости.
Список из данных одним присваиванием
Ключевой приём по всему проекту: у группы есть свойство Содержимое, которому можно присвоить массив компонентов, созданных в рантайме. Вот так формируется список заказов:
метод СозданиеОбновлениеКомпонентаЗаказов(РазделПереключателя: Строка = "", ВсеВодители: Булево = Ложь) пер КомпонентыЗаказа: Массив<Компонент> для Заказ из ВсеЗаказы пер СтатусЗаказа = ПолучитьСтатусЗаказа(Заказ.Ссылка) если РасчетСтатусовПереключателя(СтатусЗаказа) == РазделПереключателя пер КомпонентЗаказ = новый КомпонентЗаказа() КомпонентЗаказ.ПриИзмененииСтатуса = &ПриИзмененииСтатуса КомпонентЗаказ.Статус = СтатусЗаказа КомпонентЗаказ.Заказ = Заказ КомпонентыЗаказа.Добавить(КомпонентЗаказ) ; ; Компоненты.ГруппаКомпановки.Содержимое = КомпонентыЗаказа ;
Я в цикле создаю экземпляры своего компонента, прокидываю в каждый данные и подписку на событие, складываю в массив и одним присваиванием рендерю весь список. Изменился статус заказа - пересобрал массив, список обновился.
Компонент заказа внутри себя так же генерирует дочерние компоненты точек маршрута и так можно собирать такую матрёшку такой вложенности… Просто ого-го...
Заказ содержит точки, точка содержит грузы. Каждый уровень - самостоятельный компонент со своими свойствами и событиями.
Местами я собираю форму вообще без разметки, полностью в коде. Вот показ грузов точки - форма создаётся, наполняется и открывается прямо в обработчике нажатия без всяких yaml-файлов:
метод ПолучитьИнформациюОГрузеПриНажатии(Источник: Кнопка, Событие: СобытиеПриНажатии) пер КомпонентыГруза: Массив<Компонент> для Груз из Точка.ПеревозимыйГруз пер КомпонентПредставленияГруза = новый КомпонентПредставленияГруза() КомпонентПредставленияГруза.Груз = Груз КомпонентПредставленияГруза.Редактирование = Ложь КомпонентыГруза.Добавить(КомпонентПредставленияГруза) ; знч ФормаГруза = новый Форма() ФормаГруза.Содержимое = новый ПроизвольныйШаблонФормы(Содержимое = новый Группа(Содержимое = КомпонентыГруза)) ФормаГруза.ОткрытьВМодальномОкне() ;
новый Форма(), новый Группа(...) - вся иерархия интерфейса строится как обычные объекты. Форма для меня стала таким же значением, которое можно собрать и открыть, а не статичным артефактом из конфигуратора.
И тоже самое мы можем проворачивать и в контрактах из главы 4 работают и для UI. Подсистема Перевозки реализует контракт и отдаёт кнопку как объект тому, кто её попросит:
А форма настроек собирает все такие кнопки со всех подсистем, не зная, кто их прислал:
@НаКлиенте метод КомпановкаНастроекИспользуемыхФункциональностей() пер КомпонентыКнопок: Массив<Компонент> для ИспользуемаяФункциональность из КонтрактИспользуемаяФункциональность.ПолучитьСервисы() КомпонентыКнопок.Добавить(ИспользуемаяФункциональность.ФункциональнаяОпцияКнопка()) ; Компоненты.КомпановкаНастроекИспользуемыхФункциональностей.Содержимое = КомпонентыКнопок ;
Любая подсистема может подкинуть свой UI-элемент в центральную форму, не создавая зависимостей. Комбинация "интерфейс как объекты" и "внедрение зависимостей через контракты" даёт то, что через типовые механизмы сделать очень тяжело, даже невозможно.
Короче собрать можно всё что хочешь, хоть всё приложение сгенерировать целиком в одном XBSL файле. Как я услышал на днях:
1С:Элемент это вообще не 1С - здесь можно пойти любой дорогой и нет строгих правил и ограничений, можно решить любую задачу совершенно по разному.
Как обычно просто спрашивают:
А это можно сделать? А это?
Можно всё что хочешь и сбоку бантик - вопрос во времени, а так можно всё!
Но, есть но… Да не всё так идеально при таком подходе элемент так пытается торопиться, что забегает слишком далеко вперёд и тем самым что-то может не прорисоваться из-за краха в мозгах. И поэтому я оставлю для вас стыдный лайфхак здесь:
статический метод ВызовПаузы() Пауза(100мс) ;
Плак-плак… Я создал монстра, а это его сдерживающая сила...
Вообще с UI очень много проблем, это прям то место, где бывает очень больно. Простой пример: Хочешь у кнопки поменять размер (у неё есть такое свойство Ширина, Высота), но меняешь на 10 ничего, меняешь на 100 ничего, меняешь на 1000 ничего. Даже не колышется и таких мест в свойствах компонентов очень много. По этому эта ещё одна причина залезть в JS((( Грустно, ну а что делать, хочешь красивую приложуху придётся мучится в 1С:Элемент здесь же в названии есть слово 1С... Ну ладно о боли позже...
Должен отметить: чтобы всё это не превратилось в кашу, повторяющийся UI я вынес в компоненты со своим API: ОсновноеСообщение (плашка уведомления), Переключатель (табы "Активные / Выполненные / Отменённые" со счётчиками), ШапкаПрофиля, ГруппаФильтраОтДо и тд. Каждый - виджет со своими свойствами и событиями.
А чтобы генерируемый интерфейс выглядел единообразно, цвета я нигде не хардкодил. Всё идёт через мою библиотеку цветов как я уже отмечал ранее. Модуль отдаёт цвет в двух видах: как АбсолютныйЦвет для нативных компонентов, как строку rgb(...) для HTML-карты и для SVG-иконок (который кстати так же динамически у меня генерируются). Один источник - два потребителя. Плюс адаптив: внутри компонентов через выбор КлиентскоеУстройство.ВидИнтерфейса (Компьютер / Планшет / Телефон) перестраиваются шрифты и компоновка. Одна кодовая база даёт три вида интерфейса. И даже про прозрачность цветов не забыл))
И её здесь много.
Хочешь настраивать основной цвет приложения из кода динамически - нельзя! На тебе по рукам. Только задать цвет раз и на всегда. И ничего больше не выдумывай...
Есть глобальная область видимости @Глобально

Область видимости при которой та или иная сущность видна из других проектов. Ну и ничего подобного. Работает только если мы делаем библиотеку на Элементе, а если просто в одном проекте хотим что-то переиспользовать в другом то ДОСВИДАНИЯ.
Так же многие вещи как я и говорил ранее не управляются в тех местах из которых должны, даже если судить по описанию документации. Хоть язык и заявлен как само документированный, что-то он не поспевает за собою...
И отдельная боль это лаги. Тут прям сразу скажу по минимуму делайте, что-то через свойства, хоть это и сделано для упрощения жизни, то на практике совершенно не так. Пишешь быстро значение в свойство Значение и оно начинает не успевать за написание и в какой-то момент ты наблюдаешь картину как третья буква слова начинает зациклено мигать не давая тебе ничего дальше написать. И это не прекращается. По итогу идём в yaml-файл зачищаем и пишем заново уже в нём, ну или пишем каждую букву с интервалом в 3 секунды и мольбой что бы Элемент не умер от твоих очумелых пальчиков.
Можно ещё много чего отмечать... Но для этого скорее нужна отдельная статья. По этому суть сложности написания большого проекта на элементе с нуля я думаю вы уловили...
И что мы имеем? titransflow - теперь реальный продукт на 1С:Элементе.
Два месяца. Один разработчик, один архитектор. На выходе - рабочая система с ролями, машиной состояний, интеграцией по HTTP, push-уведомлениями, картой и мобильным клиентом и не считая продуманный UI (из гов… и палок, извините...). На чистом стеке (React + Node + база + отдельно мобилка) я бы этот же объём делал ощутимо дольше и с большим количеством ручной склейки.

Что реально понравилось за время проекта:
Единый код клиента и сервера без ручного REST избавляет от целого класса рутины.
DI через контракты оказался взрослее, чем я ждал от 1С.
Строгая типизация и null-safety ловят ошибки до запуска, а не в цеху у водителя.
Встроенный SQL с проверкой на компиляции - это удобно.
Что бесило:
В основном всё так же остаюсь верен своему прежнему мнению по поводу многих проблем.
Типы иногда разрастаются в монстров на восемь вариантов через палочку. Издержки типизации.
Отладка мобильного клиента (особенно push) - отдельный жанр страданий.
Штатного UI ой как не всегда хватает, и приходится лезть в КонтейнерHTML.
Да и вообще с UI много слёз. Хочешь сделать что бы в Элементе всё выглядело красиво придётся пролить не мало горьких слёз… Вот скажите, что сложно сделать какую-то функцию, возвращающую размер блока/группы/компонента хотя бы не в элементовском абстрактном, а в px… Не могу как ни крути получить размер типового блока(((
Переобулся ли я окончательно? Скажем так: я больше не кручу пальцем у виска на словах "серьёзный проект на 1С (тут в скобочках оставлю ЭЛЕМЕНТ)". Элемент - это не убийца React и не замена Kotlin. Но если у вас внутри компании сложная бизнес-система с ролями, статусами и кучей интеграций, то для таких задач он вполне подходит. Особенно отечественное ПО (где под капотом крутит всё это java, но опустим этот момент). На нём один человек за пару месяцев способен сделать то, на что обычно нужна целая команда. И это факты.
Компьютер по-прежнему понимает только нули и единицы. Просто между ним и мной теперь гораздо больше слоёв - и неизвестно, сколько ещё языков скрыто внутри Элемента. Но при этом всё работает быстро. За это - спасибо…
P.S. Когда я только начал этот проект, у меня в голове крутились одни вопросы: с чего вообще начинать, что делать и как всё это вытянуть. А к финалу я уже дошёл до того, что во сне придумываю решения на обычных языках программирования (python), а потом переношу их на Элемент. Или мы упираемся в стандарты и пытаемся их продавить, или сразу пишем на JS. В комментариях к прошлым статьям Элемент сравнивали с кем угодно. Так вот, покажите мне, на чём вы бы собрали такую же логистику вдвоём за два месяца? Мне правда любопытно.
P.S. На эту статью ушло очень много времени - целых полтора месяца на секундочку (правда параллельно с написанием ещё одной другой статьи, но об этом позже), так что надеюсь вы оцените старания…