Встраиваем React приложение в форму 1с
В этой статье я хочу написать о том, как можно интегрировать react приложение на форму 1с.
Предистория
Все началось с того, что я случайно наткнулся на видео с конференции Инфостарт в ютубе где автор рассказывает о том, как можно встроить web приложение в 1с. Так же он объясняет зачем это нужно, где этот подход уже встречается в типовых конфигурациях. С какими проблемами придется столкнуться при реализации. И как их можно решить. Но формат доклада подразумевал больше теории чем практики. В этой статье я хочу рассказать, как встроить "Список задач" написанной на React в 1с на практическом примере.
React
Создаем react приложение, с typescript
npm create vite@latest my-project
Устанавливаем зависимости
npm i
Встраивать в 1с мы будем через макет с типом двоичные данные. По этому наш билд должен быть единственным файлом HTML. Для этого нужно установить плагин для Vite и немного его настроить.
npm install vite-plugin-singlefile --save-dev
Далее в файле vite.config.ts его нужно подключить. Содержание файла ниже
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { viteSingleFile } from 'vite-plugin-singlefile'
export default defineConfig({
build: {
target: "es2018"
},
plugins: [
react(),
viteSingleFile()
],
})
В этом же файле нужно сказать сборщику Vite, что билд должен быть на JavaScript версии es2018. Это нужно потому-что в 1с поле HTMLДокумента, в зависимости от версии платформы, использует не самую последнюю версию JS. В нашем случае es2018 поддерживается с версии 1с платформы 8.3.24
Далее нам нужно подключить библиотеку, с помощью которой очень удобно можно настроить взаимодействия react и 1с. Этой библиотекой с нами любезно поделился автор доклада, про который я писал выше Игорь Апресов. Пробуем ее установить:
npm install react-1cv8-web-app
И получаем ошибку

Это все из-за того, что в зависимостях библиотеки React должен быть версии 18.3, а мы ставили последнюю версию, на данный момент - 19.1.
Я не стал заморачиваться, пошел в github на страничку Игоря, забрал себе все, что там есть и исходный код этой библиотеки скопировал в свой проект. В этой статье я не буду приводить весь исходный код react. Если будет интересно - ссылки на исходники будут в конце. Остановлюсь только на моментах связанных с работой с 1с.
Импортируем из библиотеки V8WebAppProvider и V8Proxy.
import {V8WebAppProvider, V8Proxy} from './lib/react-1cv8-web-app-main'
С помощью V8WebAppProvider оборачиваем все приложение
<V8WebAppProvider>
<h1>Список задач</h1>
...
</V8WebAppProvider>
А с помощью функции V8Proxy мы можем обмениваться данными с 1с. Ее использование похоже на fetch. Для примера, что бы получить список документов, в нашем случае код будет такой:
useEffect(() => {
V8Proxy.fetch('ПолучитьСписокЗадач').then((res) => {
setListTasks(res._value ? res.json() : [])
})
}, []);
Тут мы ждем когда DOM дерево отрендерится и делаем запрос в 1с. ПолучитьСписокЗадач - это название события, по которому в 1с мы можем понять, что нужно сделать. В нашем случае достать запросом список документов.
Еще один ньюанс на который стоит обратить внимание, это то - что если из 1с к нам придет пустой массив, библиотека react-1cv8-web-app-main в ответе вернет пустую строку (что странно), а в свою очередь пустую строку не удастся преобразовать в JSON, что приведет к ошибке. По этому, такой случай нужно предусмотреть. В текущем примере, если документов нет, то мы локальному стэйту сразу присваиваем пустой массив.
Если нужно передать данные из react в 1с, то мы указываем их вторым параметром. Например в нашем случае, при удалении документы, мы должны отправить в 1с идентификатор по которому мы поймем, какой конкретно документ нужно удалить. В коде ниже это "number". Следующей строкой мы опять получаем список документов после удаления.
const onDeleteDoc = async (number: string) => {
await V8Proxy.fetch('УдалитьЗадачу', { number });
await V8Proxy.fetch('ПолучитьСписокЗадач').then((res) => {
setListTasks(res._value ? res.json() : [])
})
}
Тут еще хочу отметить одну странность. 1с не понимает такое свойств CSS как gap. Это не большая проблема, и это можно легко обойти, но неприятно. Проверял на платформе 8.3.27. Возможно в следующих версиях это поправят.
Все. React приложение готово. Теперь его нужно сбилдить:
npm build
В папке dist появится файл index.html. Это и есть наше приложение которое мы будем встраивать в 1С.
1С
Создаем пустую конфигурацию. Добавляем документ "Задачи" и в нем реквизит "Название", с типом строка. Собственно это и будет наши задачи, которыми мы будет управлять через React (Создавать/Проводить/Удалять)
Так же добавим обработку "Список задач", в ней создаем форму, а также добавляем в ней "Макет" с типом двоичные данные. В него мы загружаем файл index.html которое мы сделали выше.
В форме создаем реквизит "Список задач" с типом строка, перетаскиваем на форму, выбираем что это Поле HTML документа
В модуле формы, в обработке "ПриСозданииНаСервер" загружаем наше приложение в "Список задач" из двоичных данных
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
СтрокаHTML = РеквизитФормыВЗначение("Объект").ПолучитьМакет("Макет");
АдресХранилищаHTML = ПоместитьВоВременноеХранилище(СтрокаHTML, УникальныйИдентификатор);
СписокЗадач = ПолучитьНавигационнуюСсылкуИнформационнойБазы() + "/" + АдресХранилищаHTML;
КонецПроцедуры
Обмен данными между 1с и React происходит с помощью обработчика "ПриНажатии" поля HTML Документа. В нем мы должны подписаться на события из React.
&НаКлиенте
Процедура СписокЗадачПриНажатии(Элемент, ДанныеСобытия, СтандартнаяОбработка)
РеактивныйКлиент.ОбработатьСообщение(ЭтотОбъект, Элементы.СписокЗадач, ДанныеСобытия, СтандартнаяОбработка,
Новый ОписаниеОповещения("ОбработатьСообщение", ЭтотОбъект)
);
КонецПроцедуры
Функцию с событиями которые мы будем обрабатывать вынесем в общий модуль "Реактивный клиент"
// Перехватывает сообщение из события *ПриНажатии поля HTTP документа и формирует обработку оповещения
//
// Параметры:
// ЭтотОбъект - ФормаКлиентскогоПриложения - Форма с которой нужно взаимодействовать
// ЭлементПолеHTML - ПолеФормы - ПолеHTMLДокумента в которое загружено реактивное приложение
// ДанныеСобытия - ФиксированнаяСтруктура - См. описание ПриНажатии в синтаксис-помощнике
// СтандартнаяОбработка - Булево - Возвращаемый. Устанавливается в Ложь если событие реактивного приложения.
//
Процедура ОбработатьСообщение(ЭтотОбъект, ЭлементПолеHTML, ДанныеСобытия, СтандартнаяОбработка) Экспорт
ЭлементHTML = ДанныеСобытия["Element"];
Если ЭлементHTML["id"] = "V8WebAppEventRequestForwarder" Тогда
СтандартнаяОбработка = Ложь;
Сообщение = НовоеСообщение();
Сообщение.ИмяСобытия = ЭлементHTML.getAttribute("v8eventname");
Сообщение.Ид = ЭлементHTML.getAttribute("v8uuid");
Тип = ЭлементHTML.getAttribute("v8type");
Если Тип = "json" Или Тип = "string" Тогда
Сообщение.Значение = ПрочитатьЗначениеJSON(ЭлементHTML["value"]);
КонецЕсли;
// Обработчики по-умолчанию
Если Сообщение.ИмяСобытия = "ПолучитьСписокЗадач" Тогда
РеактивныйКлиент.ОтправитьОтвет(ЭлементПолеHTML, Сообщение, РаботаСДокментом.ПолучитьСписокЗадач());
КонецЕсли;
Если Сообщение.ИмяСобытия = "СоздатьЗадачу" Тогда
РеактивныйКлиент.ОтправитьОтвет(ЭлементПолеHTML, Сообщение);
РаботаСДокментом.СоздатьЗадачу(Сообщение.Значение["name"])
КонецЕсли;
Если Сообщение.ИмяСобытия = "УдалитьЗадачу" Тогда
РеактивныйКлиент.ОтправитьОтвет(ЭлементПолеHTML, Сообщение);
РаботаСДокментом.УдалитьЗадачу(Сообщение.Значение["number"])
КонецЕсли;
Если Сообщение.ИмяСобытия = "ПровестиЗадачу" Тогда
РеактивныйКлиент.ОтправитьОтвет(ЭлементПолеHTML, Сообщение);
РаботаСДокментом.ПровестиЗадачу(Сообщение.Значение["number"])
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Тут нам нужно подготовить ответ
// Конструктор сообщения, получаемого от реактивного приложения
//
// Возвращаемое значение:
// Структура:
// * ИмяСобытия - Строка - для ветвления по событиям
// * Ид - Строка - Идентификатор сообщения
// * Значение - Структура, Массив, Число, Строка, Булево, ДвоичныеДанные, Неопределенно - параметры события
//
Функция НовоеСообщение() Экспорт
Результат = Новый Структура;
Результат.Вставить("ИмяСобытия", "");
Результат.Вставить("Ид", "");
Результат.Вставить("Значение", "");
Возврат Результат;
КонецФункции
Так же в этом модуле делаем функцию "ОтправитьОтвет", с помощью которой в зависимости от типа данных, которые мы собираемся отпраить в React преобразуем в нужный формат.
// Ответ на сообщение от реактивного приложения.
// Каждое сообщение должно получить свой ответ! Хотя бы пустой.
//
// Параметры:
// ЭлементПолеHTML - ПолеФормы - ПолеHTMLДокумента в которое загружено реактивное приложение
// Сообщение - См. НовыйСообщение - Сообщение, на которое готовится ответ
// Ответ - Структура, Массив, Число, Строка, Булево, Неопределенно - Ответ, который будет отправлен
//
Процедура ОтправитьОтвет(ЭлементПолеHTML, Сообщение, Ответ = Неопределено) Экспорт
ЭлементHTML = ЭлементПолеHTML.Документ["defaultView"];
Прокси = ЭлементHTML["V8Proxy"];
Пакет = "";
Если ЗначениеЗаполнено(Ответ) Тогда
Если ТипЗнч(Ответ) = Тип("Структура")
Или ТипЗнч(Ответ) = Тип("ФиксированнаяСтруктура")
Или ТипЗнч(Ответ) = Тип("Соответствие")
Или ТипЗнч(Ответ) = Тип("ФиксированноеСоответствие")
Или ТипЗнч(Ответ) = Тип("Массив")
Или ТипЗнч(Ответ) = Тип("ФиксированныйМассив") Тогда
Пакет = ЗаписатьЗначениеJSON(Ответ);
ИначеЕсли ТипЗнч(Ответ) = Тип("ДвоичныеДанные") Тогда
Пакет = Base64Строка(Ответ)
Иначе
Пакет = XMLСтрока(Ответ);
КонецЕсли;
КонецЕсли;
Прокси.sendResponse(Сообщение.Ид, Пакет);
КонецПроцедуры
Ниже привожу листинг уже модуля "РаботаСДокментом", который выполняется на сервере, где есть функции для получения списка документа, создания, удаления, и проведения.
Процедура СоздатьЗадачу(Имя) Экспорт
НовыйДокумент = Документы.Задача.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДата();
НовыйДокумент.Название = Имя;
НовыйДокумент.Записать();
КонецПроцедуры
Процедура УдалитьЗадачу(Номер) Экспорт
НовыйДокумент = Документы.Задача.НайтиПоНомеру(Номер).ПолучитьОбъект();
НовыйДокумент.Удалить();
КонецПроцедуры
Процедура ПровестиЗадачу(Номер) Экспорт
Документ = Документы.Задача.НайтиПоНомеру(Номер).ПолучитьОбъект();
Если Документ.Проведен Тогда
Документ.Записать(РежимЗаписиДокумента.ОтменаПроведения);
Иначе
Документ.Записать(РежимЗаписиДокумента.Проведение);
КонецЕсли;
КонецПроцедуры
Функция ПолучитьСписокЗадач() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Задача.Номер КАК Номер,
| Задача.Дата КАК Дата,
| Задача.Название КАК Название,
| Задача.Проведен КАК Проведен
|ИЗ
| Документ.Задача КАК Задача";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
МассивСпискаЗадач = Новый Массив;
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Задача = Новый Структура;
Задача.Вставить("Номер", ВыборкаДетальныеЗаписи.Номер);
Задача.Вставить("Дата", Формат(ВыборкаДетальныеЗаписи.Дата,"ДЛФ=ДВ"));
Задача.Вставить("Название", ВыборкаДетальныеЗаписи.Название);
Задача.Вставить("Проведен", ВыборкаДетальныеЗаписи.Проведен);
МассивСпискаЗадач.Добавить(Задача);
КонецЦикла;
Возврат МассивСпискаЗадач;
КонецФункции
Осталась еще одна проблема. При создании документа из 1с, react приложение не обновляется. Что бы это исправить Можно опять выполнить загрузку нашего приложения из макета, тогда мы при первом рендере уже получим актуальный список задач. Для начала в форме нашей обработки, мы должны загрузку приложения из макета вынести в отдельную процедуру и сделать ее экспортной.
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
ЗагрузитьМакет();
КонецПроцедуры
&НаСервере
Процедура ЗагрузитьМакет() Экспорт
СтрокаHTML = РеквизитФормыВЗначение("Объект").ПолучитьМакет("Макет");
АдресХранилищаHTML = ПоместитьВоВременноеХранилище(СтрокаHTML, УникальныйИдентификатор);
СписокЗадач = ПолучитьНавигационнуюСсылкуИнформационнойБазы() + "/" + АдресХранилищаHTML;
КонецПроцедуры
Далее в форме документа, в событии ПослеЗаписи можно вызвать экспортную процедуру с загрузкой макета
&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
ОткрытыеОкна = ПолучитьОкна();
Для Каждого ОкноСпискаЗадач из ОткрытыеОкна Цикл
Если ОкноСпискаЗадач.Заголовок = "Список задач" Тогда
ОкноСпискаЗадач.Содержимое[0].ЭтаФорма.ЗагрузитьМакет();
КонецЕсли;
КонецЦикла
КонецПроцедуры
Заключение
На этом все. В этой статье не стояла задача сделать полноценное приложение Список задач. Цель - продемонстрировать на практике как можно реализовать взаимодействие 1с и React. Если есть вопросы и желание обсудить жду вас в Telegram :)