Введение
Прочитав публикации Самый примитивный HTTP-сервис в мире и Создание асинхронных виджетов, решил внести свои пять копеек и немного дополнить коллекцию примеров по данной тематике.
Поскольку одним из наиболее распространенных подходов к визуализации данных в web является их отображение в виде изображений в формате svg, было принято решение создать простейший инструмент, позволяющий отображать изображение в формате svg в поле html документа, а также динамически изменять его.
О том, что из этого получилось – читайте ниже.
Системные требования
Платформа 1С:Предприятие, версии не ниже 8.3.5
Описание механизма
Механизм не отличается большой оригинальностью и суть его заключается в том, что содержимое виджета представляет собой html-страницу, которая возвращается при обращении к http-сервису 1С:Предприятие. В процессе отображения страницы клиент обращается к http-сервису 1С:Предприятие для загрузки изображения, которое хранится в виде макета в теле конфигурации. Через определенные промежутки времени, клиент, средствами javascript генерирует http-запросы, возвращающие обновленное состояние виджета. Затем, производится изменение изображения в соответствии с полученным состоянием.
Для манипуляций с изображением воспользуемся популярной библиотекой - Snap.SVG. При достаточно сложной логике изменения изображения в зависимости от состояния, возможно появление большого количества кода javascript, размещенного в теле страницы или отдельных макетах, что в совокупности с отсутствием встроенных в платформу средств разработки и отладки javascript кода, может сделать процесс создания виджета сложным и неудобным. Во избежание озвученной проблемы, мы будем генерировать javascript код для изменения изображения автоматически, при поступлении запроса с клиента, а затем передавать его для выполнения клиенту вместе с состоянием. Также, для уменьшения количества этого кода, клиент будет хранить свое состояние и передавать его вместе с запросом. Таким образом, если состояние с момента последнего запроса не изменилось, необходимость в генерации и выполнении кода отсутствует.
Реализация
Формирование HTML страницы
Создадим обработку ВиджетSVG, которая будет отвечать за формирования html-страницы содержимого виджета.
Для использования в качестве основы нашей страницы создадим макет нижеследующего содержания:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
<meta charset="utf-8">
<script src="{{UrlБиблиотекиSnapSVG}}snap.svg-min.js"></script>
<style>
html, body { margin:0; padding:0; overflow:hidden }
svg { position:fixed; top:0; left:0; height:100%; width:100% }
</style>
</head>
<body style="background-color:{{ЦветФона}}">
<svg id="drawing" style="width:100%;height:100%"></svg>
<script>
// Глобальный выполнятель кода js
var geval = eval;
// Текущее состояние параметров
let state = {};
// Новое состояние, полученное при обновлении
var newState;
// Область рисования
var drawing = Snap('#drawing');
// Перед загрузкой
// Загрузка
Snap.load("{{UrlИзображения}}", function(f) {
drawing.append(f.select("#{{ИмпортируемыйЭлемент}}"));
// После загрузки
});
// Включаем автообновление
var myVar = setInterval(myTimer, {{ПериодОбновления}});
function myTimer() {
xhr = new XMLHttpRequest();
var url = "{{UrlСервисаОбновленияДанных}}";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
newState = json.state;
geval(json.script);
}
}
var data = JSON.stringify(state);
xhr.send(data);
}
</script>
</body>
</html>
фрагмент
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
отвечает за возможность использования svg из поля html документа.
фрагмент
<script src="{{UrlБиблиотекиSnapSVG}}snap.svg-min.js"></script>
подключает библиотеку Snap.SVG. Поскольку url размещения библиотеки может отличаться от url виджета, предусмотрим возможность его настройки.
фрагмент
<style>
html, body { margin:0; padding:0; overflow:hidden }
svg { position:fixed; top:0; left:0; height:100%; width:100% }
</style>
позволяет масштабировать изображение на весь экран и пропорционально изменять его размеры при изменении размеров окна.
Для размещения изображения на странице используется элемент
<svg id="drawing" style="width:100%;height:100%"></svg>
Для обеспечения необходимой функциональности, в конец страницы добавлен нижеследующий скрипт:
<script>
// Глобальный выполнятель кода js
var geval = eval;
// Текущее состояние параметров
let state = {};
// Новое состояние, полученное при обновлении
var newState;
// Область рисования
var drawing = Snap('#drawing');
// Перед загрузкой
// Загрузка
Snap.load("{{UrlИзображения}}", function(f) {
drawing.append(f.select("#{{ИмпортируемыйЭлемент}}"));
// После загрузки
});
// Включаем автообновление
var myVar = setInterval(myTimer, {{ПериодОбновления}});
function myTimer() {
xhr = new XMLHttpRequest();
var url = "{{UrlСервисаОбновленияДанных}}";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
newState = json.state;
geval(json.script);
}
}
var data = JSON.stringify(state);
xhr.send(data);
}
</script>
фрагмент
var geval = eval;
Необходим для того, чтобы при выполнении динамического кода использовалась глобальная область видимости.
Объекты state и newState используются для хранения соответственно текущего и нового состояний страницы.
// Текущее состояние параметров
let state = {};
// Новое состояние, полученное при обновлении
var newState;
фрагмент
var drawing = Snap('#drawing');
создает область рисования и подключает ее к ранее созданному элементу svg, а фрагмент
// Перед загрузкой
// Загрузка
Snap.load("{{UrlИзображения}}", function(f) {
drawing.append(f.select("#{{ИмпортируемыйЭлемент}}"));
// После загрузки
});
осуществляет загрузку изображения, а также добавление необходимой части изображения в область рисования.
Вместо комментариев
// Перед загрузкой
И
// После загрузки
Может быть размещен код javascript.
Фрагмент
var myVar = setInterval(myTimer, {{ПериодОбновления}});
Производит подключение обработчика, который вызывается через определенные промежутки времени, а обработчик
function myTimer() {
xhr = new XMLHttpRequest();
var url = "{{UrlСервисаОбновленияДанных}}";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
var json = JSON.parse(xhr.responseText);
newState = json.state;
geval(json.script);
}
}
var data = JSON.stringify(state);
xhr.send(data);
}
Выполняет запрос обновлений, передавая в качестве параметра текущее состояние, получение ответа и выполнение кода, сформированного сервером.
Для хранения библиотеки Snap.SVG создадим макет snap_svg_min_js и поместим в него код библиотеки. Для загрузки библиотеки создадим http-сервис, обработчик которого имеет нижеследующий вид:
Функция БиблиотекаSnapSVGПолучить(Запрос)
Ответ = Новый HTTPСервисОтвет(200);
Ответ.УстановитьТелоИзДвоичныхДанных(Обработки.ВиджетSVG.ПолучитьМакет("snap_svg_min_js"));
Возврат Ответ;
КонецФункции
Код модуля объекта обработки представлен ниже:
Перем UrlБиблиотекиSnapSVG Экспорт; // Url для загрузки snap.svg-min.js
Перем ЦветФона Экспорт; // Цвет фона страницы
Перем UrlИзображения Экспорт; // Url для загрузки изображения
Перем ПередЗагрузкой Экспорт; // код javascript, выполняемый перед загрузкой изображения
Перем ПослеЗагрузки Экспорт; // Код javascript, выполняемый после загрузки изображения
Перем ИмпортируемыйЭлемент Экспорт; // Id элемента изображения для импорта
Перем UrlСервисаОбновленияДанных Экспорт; // Url сервиса обновления данных
Перем ПериодОбновления Экспорт; // Период отправки запросов обновления
// Формирует HTML код страницы виджета
//
Функция ПолучитьHTML() Экспорт
ТекстHTML = Обработки.ВиджетSVG.ПолучитьМакет("ШаблонHTMLСтраницы").ПолучитьТекст();
ТекстHTML = СтрЗаменить(ТекстHTML, "{{UrlБиблиотекиSnapSVG}}", UrlБиблиотекиSnapSVG);
ТекстHTML = СтрЗаменить(ТекстHTML, "{{ЦветФона}}", ЦветФона);
ТекстHTML = СтрЗаменить(ТекстHTML, "{{UrlИзображения}}", UrlИзображения);
ТекстHTML = СтрЗаменить(ТекстHTML, "// Перед загрузкой", ПередЗагрузкой);
ТекстHTML = СтрЗаменить(ТекстHTML, "// После загрузки", ПослеЗагрузки);
ТекстHTML = СтрЗаменить(ТекстHTML, "{{ИмпортируемыйЭлемент}}", ИмпортируемыйЭлемент);
ТекстHTML = СтрЗаменить(ТекстHTML, "{{UrlСервисаОбновленияДанных}}", UrlСервисаОбновленияДанных);
ТекстHTML = СтрЗаменить(ТекстHTML, "{{ПериодОбновления}}", Формат( ПериодОбновления, "ЧГ=0"));
Возврат ТекстHTML;
КонецФункции
UrlБиблиотекиSnapSVG = "";
ЦветФона = "#696969";
ПередЗагрузкой = "";
ПослеЗагрузки = "";
ПериодОбновления = 1000;
Для формирования результирующего содержимого страницы используется функция ПолучитьHTML. Как можно увидеть, обработка содержит переменные с глобальной областью видимости, соответствующие параметрам html-шаблона. Значения этих переменных должны быть заполнены до вызова функции ПолучитьHTML.
Формирование обновлений
Для реализации механизма обновлений создадим обработку ОбновлениеДанныхSVG.
Перем НовоеСостояние Экспорт; // Новое состояние системы
Перем ТекстJS Экспорт; // Код javascript для выполнения на клиенте
// Получает текущее состояние клиента из запроса
//
Функция ПолучитьТекущееСостояние(Запрос) Экспорт
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Запрос.ПолучитьТелоКакСтроку());
ТекущееСостояние = ПрочитатьJSON(ЧтениеJSON, Истина);
Возврат ТекущееСостояние;
КонецФункции
// Формирует тело ответа клиенту
//
Функция ПолучитьТелоОтвета() Экспорт
ДанныеОтвет = Новый Структура;
ДанныеОтвет.Вставить("state", НовоеСостояние);
ДанныеОтвет.Вставить("script", ТекстJS);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ДанныеОтвет);
Возврат ЗаписьJSON.Закрыть();
КонецФункции
СкриптJS = "";
НовоеСостояние = Новый Структура;
Функция ПолучитьТекущееСостояние используется для получения текущего состояния, переданного клиентом, а функция ПолучитьТелоОтвета для формирования тела ответа.
Генерация кода JavaScript
Для динамической генерации javascript создадим обработку КомандыSVG, которая будет генерировать фрагменты кода javascript при вызове соответствующих методов
Перем ТекстJS Экспорт; // Текст javascript
// Устанавливает атрибуты элемента
//
Функция УстановитьАтрибуты(МассивАтрибутов) Экспорт
Команды = Обработки.КомандыSVG.Создать();
Команды.ТекстJS = Лев(ТекстJS, СтрДлина(ТекстJS) - 1) + ".attr({";
Для каждого Атрибут из МассивАтрибутов Цикл
Команды.ТекстJS = Команды.ТекстJS + Атрибут + ", ";
КонецЦикла;
Команды.ТекстJS = Лев(Команды.ТекстJS, СтрДлина(Команды.ТекстJS) - 2) + "});";
Возврат Команды;
КонецФункции
// Устанавливает стили элемента
//
Функция УстановитьСтили(МассивСтилей) Экспорт
ЗначениеСтили = "";
Для каждого Стиль Из МассивСтилей Цикл
ЗначениеСтили = ЗначениеСтили + Стиль;
КонецЦикла;
МассивАтрибутов = Новый Массив;
МассивАтрибутов.Добавить(АтрибутыSVG.Атрибут("style", """" + ЗначениеСтили + """"));
Возврат УстановитьАтрибуты(МассивАтрибутов);
КонецФункции
// Обновляет состояние
//
Функция ОбновитьСостояние(ИмяМетрики) Экспорт
Команды = Обработки.КомандыSVG.Создать();
Команды.ТекстJS = ТекстJS + "state." + ИмяМетрики + " = newState." + ИмяМетрики + ";";
Возврат Команды;
КонецФункции
// Возвращает область рисования
//
Функция ОбластьРисования() Экспорт
Команды = Обработки.КомандыSVG.Создать();
Команды.ТекстJS = ТекстJS + "drawing;";
Возврат Команды;
КонецФункции
// Добавляет команды
//
Функция Добавить(Команды) Экспорт
Команды = Обработки.КомандыSVG.Создать();
Если ТипЗнч(Команды) = Тип("Строка") Тогда
Команды.ТекстJS = ТекстJS + Команды;
Иначе
Команды.ТекстJS = ТекстJS + Команды.ТекстJS;
КонецЕсли;
Возврат Команды;
КонецФункции
// Находит и возвращает элемент по Id
//
Функция Выбрать(Элемент) Экспорт
Команды = Обработки.КомандыSVG.Создать();
Команды.ТекстJS = Лев(ТекстJS, СтрДлина(ТекстJS) - 1) + ".select('#" + Элемент + "');";
Возврат Команды;
КонецФункции
ТекстJS = "";
Также, для генерации элементов атрибутов и стилей создадим соответствующие общие модули
// Модуль: АтрибутыSVG
// viewBox
//
Функция ОбластьПросмотра(X, Y, Ширина, Высота) Экспорт
СтрX = ЗначениеВСтроку(X);
СтрY = ЗначениеВСтроку(Y);
СтрШирина = ЗначениеВСтроку(Ширина);
СтрВысота = ЗначениеВСтроку(Высота);
Возврат "viewBox: """ + СтрX + " " + СтрY + " " + СтрШирина + " " + СтрВысота + """";
КонецФункции
// width
//
Функция Ширина(Значение = Неопределено) Экспорт
Возврат Атрибут("width", Значение);
КонецФункции
// height
//
Функция Высота(Значение = Неопределено) Экспорт
Возврат Атрибут("height", Значение);
КонецФункции
// text
//
Функция Текст(Значение) Экспорт
Возврат Атрибут("text", Значение);
КонецФункции
// stroke
//
Функция ЦветЛинии(Значение = Неопределено) Экспорт
Возврат Атрибут("stroke", Значение);
КонецФункции
// fill
//
Функция Заливка(Значение = Неопределено) Экспорт
Возврат Атрибут("fill", Значение);
КонецФункции
// stroke-widh
//
Функция ТолщинаЛинии(Значение = Неопределено) Экспорт
Возврат Атрибут("""stroke-width""", Значение);
КонецФункции
// Формирует произвольный атрибут
//
Функция Атрибут(Имя, Значение = Неопределено) Экспорт
Возврат Имя + ": " + ?(Значение = Неопределено, "null", ?(ТипЗнч(Значение) = Тип("Строка"), """" + Значение + """", ЗначениеВСтроку(Значение)));
КонецФункции
// Преобразует значение в строку
//
Функция ЗначениеВСтроку(Значение) Экспорт
Возврат ?(ТипЗнч(Значение) = Тип("Число"), ?(Формат(Значение, "ЧГ=0") = "", "0", Формат(Значение, "ЧГ=0")), Строка(Значение));
КонецФункции
// Модуль: СтилиSVG
// color
//
Функция Цвет(Значение) Экспорт
Возврат Стиль("color", Значение);
КонецФункции
// background-color
//
Функция ЦветФона(Значение) Экспорт
Возврат Стиль("background-color", Значение);
КонецФункции
// opacity
//
Функция Прозрачность(Значение) Экспорт
Возврат Стиль("opacity", Значение);
КонецФункции
// Формирует произвольный стиль
//
Функция Стиль(Имя, Значение) Экспорт
Возврат Имя + ":" + АтрибутыSVG.ЗначениеВСтроку(Значение) + ";";
КонецФункции
Таким образом, мы сможем генерировать фрагменты кода javascript примерно следующим образом:
Процедура РаскраситьТрубопроводы(КомандыSVG)
АтрибутыВода = Новый Массив;
АтрибутыВода.Добавить(АтрибутыSVG.ЦветЛинии("#2E86C1"));
АтрибутыВода.Добавить(АтрибутыSVG.Заливка("#2E86C1"));
АтрибутыПар = Новый Массив;
АтрибутыПар.Добавить(АтрибутыSVG.ЦветЛинии("#F8C471"));
АтрибутыПар.Добавить(АтрибутыSVG.Заливка("none"));
АтрибутыКонденсат = Новый Массив;
АтрибутыКонденсат.Добавить(АтрибутыSVG.ЦветЛинии("#F8C471"));
АтрибутыКонденсат.Добавить(АтрибутыSVG.Заливка("#F8C471"));
АтрибутыИсхСмесь = Новый Массив;
АтрибутыИсхСмесь.Добавить(АтрибутыSVG.ЦветЛинии("#48C9B0"));
АтрибутыИсхСмесь.Добавить(АтрибутыSVG.Заливка("#48C9B0"));
АтрибутыКуб = Новый Массив;
АтрибутыКуб.Добавить(АтрибутыSVG.ЦветЛинии("#873600"));
АтрибутыКуб.Добавить(АтрибутыSVG.Заливка("#873600"));
АтрибутыРектификат = Новый Массив;
АтрибутыРектификат.Добавить(АтрибутыSVG.ЦветЛинии("#8E44AD"));
АтрибутыРектификат.Добавить(АтрибутыSVG.Заливка("#8E44AD"));
КомандыSVG = КомандыSVG
.ОбластьРисования().Выбрать("coollines").УстановитьАтрибуты(АтрибутыВода)
.ОбластьРисования().Выбрать("steamlines").УстановитьАтрибуты(АтрибутыПар)
.ОбластьРисования().Выбрать("condensatlines").УстановитьАтрибуты(АтрибутыКонденсат)
.ОбластьРисования().Выбрать("feedlines").УстановитьАтрибуты(АтрибутыИсхСмесь)
.ОбластьРисования().Выбрать("cubelines").УстановитьАтрибуты(АтрибутыКуб)
.ОбластьРисования().Выбрать("productlines").УстановитьАтрибуты(АтрибутыРектификат);
КонецПроцедуры
Создание виджета
Создание http-сервиса
Дабы не смешивать код, относящийся к виджету с кодом, относящимся к ранее созданной библиотеки, создадим новый http-сервис ДемоВиджетыSVG (mywidgets).
Определим для него три шаблона url, соответствующие html-странице (ВиджетSVG, /sheet), изображению (СхемаSVG, /sheet.svg) и обновлению данных (ОбновлениеДанных, /update).
Создание изображения
В качестве среды, для создания изображений может быть использован любой редактор, позволяющий сохранять результаты в формате svg или в формате, который может быть конвертирован в этот формат. В качестве визуализируемого компонента, под влиянием этой публикации и доклада Юрия Мороза, была выбрана технологическая схема ректификационной установки. Для создания схемы использовался online редактор draw.io, поскольку в нем уже имеется библиотека технологического оборудования. В дальнейшем, полученный svg файл был модифицирован редактором inkscape.
В качестве хранилища исходного изображения используется общий макет СхемаSVG. Для отправки изображения на клиент используется нижеследующий код (обработчик шаблона СхемаSVG).
Функция СхемаSVGПолучить(Запрос)
Ответ = Новый HTTPСервисОтвет(200);
Ответ.УстановитьТелоИзДвоичныхДанных(ПолучитьОбщийМакет("СхемаSVG"));
Возврат Ответ;
КонецФункции
Формирование HTML страницы
Формирование html-страницы производится в обработчике шаблона ВиджетSVG.
Функция ВиджетSVGПолучить(Запрос)
Ответ = Новый HTTPСервисОтвет(200);
Виджет = Обработки.ВиджетSVG.Создать();
// Инициализация объекта
Виджет.UrlБиблиотекиSnapSVG = "../widgets/";
Виджет.UrlИзображения = "sheet.svg";
Виджет.UrlСервисаОбновленияДанных = "update";
Виджет.ИмпортируемыйЭлемент = "sheet";
Виджет.ЦветФона = "#1B2631";
// Создание кода javascript, который будет выполнен после загрузки изображения
КомандыSVG = Обработки.КомандыSVG.Создать();
УстановитьАтрибутыРисунка(КомандыSVG);
УстановитьАтрибутыСхемы(КомандыSVG);
РаскраситьТрубопроводы(КомандыSVG);
ВключитьСепараторы(КомандыSVG);
УстановитьПараметрыВыводаИнформацииОЗаполненииЕмкостей(КомандыSVG);
ОткрытьЗадвижки(КомандыSVG);
ВключитьНасос(КомандыSVG, "p1");
Виджет.ПослеЗагрузки = КомандыSVG.ТекстJS;
Ответ.УстановитьТелоИзСтроки(Виджет.ПолучитьHTML());
Возврат Ответ;
КонецФункции
Пример кода для настройки изображения представлен ниже:
Процедура ВыключитьНасос(КомандыSVG, Имя)
НасосВыкл = Новый Массив;
НасосВыкл.Добавить(АтрибутыSVG.Заливка("none"));
НасосВыкл.Добавить(АтрибутыSVG.ЦветЛинии("#B2B2B2"));
КомандыSVG = КомандыSVG.ОбластьРисования().Выбрать(Имя).УстановитьАтрибуты(НасосВыкл);
КонецПроцедуры
Формирование обновлений
Формирование обновлений реализовано в обработчике шаблона ОбновлениеДанных, примерно следующим образом:
Функция ОбновлениеДанныхПолучить(Запрос)
Ответ = Новый HTTPСервисОтвет(200);
ОбновлениеДанных = Обработки.ОбновлениеДанныхSVG.Создать();
ТекущееСостояние = ОбновлениеДанных.ПолучитьТекущееСостояние(Запрос);
НовоеСостояние = ОбновлениеДанных.НовоеСостояние;
ЗаполнитьНовоеСостояние(НовоеСостояние);
МассивАтрибутов = Новый Массив;
МассивАтрибутов.Добавить(АтрибутыSVG.ЦветЛинии("#ffffff"));
МассивАтрибутов.Добавить(АтрибутыSVG.Заливка("#B2B2B2"));
Команды = Обработки.КомандыSVG.Создать();
ВывестиОбъемЕмкостиПитания(Команды, НовоеСостояние, ТекущееСостояние);
ВывестиОбъемЕмкостиКубовогоОстатка(Команды, НовоеСостояние, ТекущееСостояние);
ВывестиОбъемЕмкостиРектификата(Команды, НовоеСостояние, ТекущееСостояние);
ВывестиСостояниеНасосов(Команды, НовоеСостояние, ТекущееСостояние);
ОбновлениеДанных.ТекстJS = Команды.ТекстJS;
Ответ.УстановитьТелоИзСтроки(ОбновлениеДанных.ПолучитьТелоОтвета());
Возврат Ответ;
КонецФункции
Тестирование
После публикации информационной базы на web-сервере, протестировать результаты можно обратившись к http-сервису из браузера, либо из обработки, содержащей поле html-документа со ссылкой на http-сервис.
Как можно увидеть, внешний вид элементов схемы динамически изменяется во времени.
Заключение
Надеюсь, что настоящая публикация будет полезным примером использования http-сервисов в 1С:Предприятие.