WINOW is not OneScript.web
Минималистичный веб-сервер, построен на нативном TCPСервер, и работает на желудях.
Зачем это нужно, когда есть OneScript.Web, -CGI и т.д.? Отвечаю - для того, чтобы все было на чистом OneScript! И потому, что могу. С полным контролем, от входа двоичных данных на порт, до определения маршрута, получения данных, генерации ответа по шаблону и отправкой обратно клиенту.
Disclaimer вашему вниманию
С релиза 0.9.0 добавлена поддержка платформенного объекта ВебСервер. А это значит, что при запуске приложения под OneScript версии 2.0 и выше будет использоваться именно он. Возможны артефакты в обработке пост запросов с типом multipart/form-data. А так же в этом режиме временно не поддерживаются технологии WebSocket и SSE. Для того, чтобы использовать старый механизм под OneScript 2.0 нужно установить настройку winow.ИспользоватьПрикладнойСервер в истину.
Установка
opm install winowКнига жалоб и пожеланий !
Можно оставить тут https://github.com/autumn-library/winow/issues или тут https://github.com/oscript-library/winow/issues
Что можно сделать ?
Данная библиотека позволит Вам достаточно просто подготовить и запустить:
- микросервис, с гибким API
- быстро сделать МОК для вашего "любимого" удаленного API и наконец-то продолжить комфортную разработку.
- Веб приложение, с отдачей статичных файлов, разграничением доступа по ролям, и генерацией страниц по шаблонам.
- И все, на что хватит фантазии.
Какие возможности ?
В данной библиотеке я постарался реализовать подход в разработке приложений в стиле MVC.
На текущий момент winow позволяет:
- Обрабатывать входящие GET и POST запросы.
- Обеспечивать маршрутизацию входящего запроса до нужного метода.
- Разбирать все входящие параметры.
- Обрабатывать тело входящего POST запроса.
- Работать с печеньками (Cookie).
- Работать с сессиями.
- Отдавать статичные файлы (картинки, архивы и т.д.)
- Работать с шаблонами ответов (Синтаксис шаблона чем-то похож на jinja2, но сильно упрощен).
- Базовая авторизация и управление доступом к страницам по ролям.
- Использовать протокол WebSocket
- Использовать протокол server-sent events. (SSE)
- Отправлять трассировку, метрики и логи через OpenTelemetry.
Ограничения ?
Да! Нет никаких обещаний на тему больших нагрузок. И нет поддержки https, погружаться в историю с шифрованием трафика, я еще не готов.
Как, из чего, зависимости ?
Библиотека разработана с использованием фреймворка для инверсии зависимостей - https://github.com/autumn-library/autumn. Для более эффективной работы с winow, следует ознакомиться. А так же обязательно пройти по ссылке и поставить звездочку, без этого ничего работать не будет.
Хеллоу ворлд !
От слов - к делу. Чтобы понять, как это все работает, давайте сделаем hello-world приложение, которое будет запускаться на localhost:3333 и отвечать простым текстом hello-world.
Первым делом, нам нужна точка входа, которая запустит приложение.
Создадим такой файл:
#Использовать autumn
#Использовать winow
Поделка = Новый Поделка;
Поделка.ЗапуститьПриложение();Теперь можете запустить файл ПриветМир.os и ничего не будет работать. И причин для этого ровно две. Первая - вы не сходили https://github.com/autumn-library/autumn и не поставили звезду. Вторая - мы не создали каталог, с классами, которые обрабатывают логику запросов. Я верю, что вы успели сходить и поставить звезду! Перейдем к созданию логики.
Создаем каталог и файл:
&Контроллер("/")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
&ТочкаМаршрута("/")
Процедура Приветствие(Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> %1 </div>", "Привет новый дивный мир !");
КонецПроцедурыИ снова пробуем запустить ПриветМир.os, и идем в http://localhost:3333/
И чудо свершилось:

Передача параметров в строке запроса.
После продолжительного восторга, двигаемся дальше. На новом примере разберем по частям, как это работает.
Давайте сделаем еще один контроллер, еще более интерактивный. Сделаем так, что приложение будет нас встречать по имени. Имя мы хотим передавать в параметрах строки запроса.
http://localhost:3333/greeter/getparams?name=Nikita&familia=ivanchenko
Где greeter путь до нашего контроллера. И getparams точка входа для метода, который обрабатывает запрос. Все что после ? именные параметры.
Поехали, создаем файл:
&Контроллер("/greeter")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
&ТочкаМаршрута("getparams")
Процедура Приветствие(Запрос, Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
Имя = Запрос.ПараметрыИменные["name"];
Фамилия = Запрос.ПараметрыИменные["familia"];
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Имя: %1 </div>
|<div> Фамилия: %2 </div>", Имя, Фамилия);
КонецПроцедурыОпять запускаем ПриветМир.os, и идем теперь вот так http://localhost:3333/greeter/getparams?name=Никита&familia=Иванченко

Снова полный успех! Но давайте подробней остановимся на каждом этапе этого чуда.
WARNING
Следующий блок документации кажется не на своем месте:
Допустимо передавать именные параметры по имени в метод точки маршрута
&ТочкаМаршрута("getparamsbyname")
Процедура ПроверкаГетПараметровПоИмени(Ответ, ИмяКошки, ИмяСобаки) Экспорт
Ответ.УстановитьТипКонтента("txt");
Ответ.ТелоТекст = СтрШаблон("%1 И %2", "Кошка=" + ИмяКошки, "Собака=" + ИмяСобаки);
КонецПроцедурыФайл, который мы только что сделали, описывает определенную точку в адресной строке. При совпадении с которой перехватывается управление над входящим запросом. Посмотрим поближе.
&Контроллер("/greeter")
Процедура ПриСозданииОбъекта()
КонецПроцедурыВ начале идет конструктор нашего класса, ПриСозданииОбъекта(). Весь код, который в нем написан, будет выполнен при создании. Удобно тут выполнять всякую инициализацию переменных.
Этот метод имеет аннотацию &Контроллер("/greeter") как раз указывает, путь от корня, после которого будет осуществлен перехват.
Стоит отметить что аннотация может быть более длинной, чтобы отвечать логике описания api. Например, &Контроллер("/app/api/v1/greeter") тоже рабочий вариант, только ходить нужно уже вот сюда http://localhost:3333/app/api/v1/greeter
У любого контроллера может быть любое множество методов, которыми он обрабатывает входящий запрос.
&ТочкаМаршрута("getparams")
Процедура Приветствие(Запрос, Ответ) ЭкспортДля того, чтобы процедура контроллера могла понимать, что ее вызывают из запроса, ее нужно пометить аннотацией &ТочкаМаршрута("getparams"). Где параметр аннотации указывает имя в пути, после которого ей нужно сработать.
Так же, чтобы все получилось, процедура должна отвечать нескольким требованиям:
- Быть экспортной
- Принимать на вход параметры, имена которых ограничены и предопределены. Назначение параметров мы разберем по ходу дела.
Запрос, например, хранит всю информацию, которая пришла к нам от клиента. В том числе Запрос.ПараметрыИменные - соответствие, хранящее значения всех параметров, которые переданы после знака ?
Дальше мы лихо эти параметры читаем.
Имя = Запрос.ПараметрыИменные["name"];
Фамилия = Запрос.ПараметрыИменные["familia"];Следующий параметр Ответ, в котором собирается все, что будет отправлено обратно клиенту. Например, вот так:
Ответ.УстановитьТипКонтента("html");Устанавливается заголовок Content-Type, благодаря которому браузер понимает, как отобразить то, что мы ему шлем.
По умолчанию поддерживаются типы:
ОписанияТиповРасширений = Новый Соответствие();
ОписанияТиповРасширений.Вставить("htm","text/html; charset=utf-8");
ОписанияТиповРасширений.Вставить("html","text/html; charset=utf-8");
ОписанияТиповРасширений.Вставить("css","text/css");
ОписанияТиповРасширений.Вставить("js","text/javascript");
ОписанияТиповРасширений.Вставить("jpg","image/jpeg");
ОписанияТиповРасширений.Вставить("jpeg","image/jpeg");
ОписанияТиповРасширений.Вставить("png","image/png");
ОписанияТиповРасширений.Вставить("gif","image/gif");
ОписанияТиповРасширений.Вставить("ico","image/x-icon");
ОписанияТиповРасширений.Вставить("zip","application/x-compressed");
ОписанияТиповРасширений.Вставить("rar","application/x-compressed");
ОписанияТиповРасширений.Вставить("json","application/json");
ОписанияТиповРасширений.Вставить("txt","text/plain; charset=utf-8");Ну и конечно же устанавливаем текст ответа, который вернется клиенту.
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Имя: %1 </div>
|<div> Фамилия: %2 </div>", Имя, Фамилия);Сейчас это не удобно и не красиво. Но к концу нашей беседы мы разберемся - как сделать красиво.
Описание возможных параметров ТочкиМаршрута
Запрос- Объект, содержащий все данные о входящем запросеОтвет- Объект, содержащий все данные об ответе, который будет отправлен пользователюСессия- Объект, хранящий сессионные данные пользователя. Имеет поляДанные, соответствие для хранения любых данных бизнес-логики иЛогин, строковое имя пользователя, после авторизации.
Так же, для удобства, можно получать части объектов Сессия и Запрос.
Логин- Логин, хранящейся в сессии. АналогСессия.Логин.ДанныеСессии- Соответствие с данными, хранящееся в сессии. АналогСессия.Данные.ТекстЗапроса- Полный текст входящего запроса.ЗаголовкиЗапроса- Соответствие заголовков запроса.ТелоЗапроса- Текст тела запроса.ТелоЗапросаСоответствие- Соответствие полей тела запроса.ТелоЗапросаДвоичныеДанные- Тело запроса в виде двоичных данных.МетодЗапроса- Метод (GET, POST, PUT и тд).ПолныйПутьЗапроса- Полный путь запроса.ПутьЗапроса- Путь без параметров.ПараметрыЗапросаИменные- Соответствие, с именными параметрами.ПараметрыЗапросаПорядковые- Массив с запросами.ДатаПолученияЗапроса- Время получения запроса.ДвоичныеДанныеЗапроса- Двоичные данные всего запроса.КукиЗапроса- Куки.АдресУдаленногоУзла- Адрес удаленного узла.ПортУдаленногоУзла- Порт удаленного узла.ТелоЗапросаОбъект- Тело запроса, сериализованное из JSON. (При наличии заголовка"Content-Type:application/json"). Укажите аннотацию параметра&Тип("ИмяКласса")для десериализации в нужный тип.
Еще один способ передачи параметров в строке запроса.
Предыдущий пример показал, как можно передать параметры в строке запроса, при этом параметры имели имена. Теперь рассмотрим пример, когда параметры упорядоченные.
Давайте сделаем наконец калькулятор! И будет он работать вот так:
http://localhost:3333/greeter/calc/<operation>/<first>/<second>/Где calc - точка маршрута. operation - вид операции, будем поддерживать minus и plus. и следом два слагаемых нашего уравнения.
Добавим в наш контрол приветствия новую точку маршрута:
&ТочкаМаршрута("calc")
Процедура Калькулятор(ПараметрыЗапросаПорядковые, Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
Если ПараметрыЗапросаПорядковые.Количество() <> 3 Тогда
Решение = "Неверное число параметров";
ИначеЕсли (Не ПараметрыЗапросаПорядковые[0] = "minus"
И Не ПараметрыЗапросаПорядковые[0] = "plus") Тогда
Решение = "Операция не распознана";
Иначе
Попытка
Число1 = Число(ПараметрыЗапросаПорядковые[1]);
Число2 = Число(ПараметрыЗапросаПорядковые[2]);
Если ПараметрыЗапросаПорядковые[0] = "minus" Тогда
Решение = Число1 - Число2;
Иначе
Решение = Число1 + Число2;
КонецЕсли
Исключение
Решение = "Ошибка конвертации в число"
КонецПопытки;
КонецЕсли;
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Ответ: %1 </div>", Решение);
КонецПроцедурыПерезапустим приложение, и перейдем по ссылке http://localhost:3333/greeter/calc/plus/3/2
И в ответ перед нами будет красоваться

На самом деле, тут все очень просто. Когда мы объявляем точку маршрута &ТочкаМаршрута("calc"), все что дальше в пути через / будет любезно складываться в массив ПараметрыЗапросаПорядковые. А что делать с массивами, вы и без меня знаете.
Шаблоны параметров в пути
Так же есть возможность задавать шаблон адреса маршрута, где можно задавать именные параметры пути.
Например:
&ТочкаМаршрута("calc/{Число1}/multiply/{Число2}")
Процедура ШаблонныеПараметрыУмножение(Ответ, Число1, Число2) Экспорт
Ответ.УстановитьТипКонтента("txt");
Ответ.ТелоТекст = Число(Число1) * Число(Число2);
КонецПроцедурыВ точке маршрута фигурными скобками указываем параметры Число1 и Число2, и эти параметры будут переданы в метод обработчик во время выполнения запроса.
Фигурные скобки описывают именно сегменты пути. Параметры строки запроса в шаблоне не объявляются и объявления не требуют: параметр метода получает значение одноимённого параметра строки запроса автоматически.
// Обработает GET /console/run?Топик=чат&Сообщение=привет
&ТочкаМаршрута("run")
Процедура ВходящееСообщение(Топик, Сообщение) Экспорт
// ...
КонецПроцедурыВходящие POST запросы
С пост запросами, все почти так же просто. Запрос Имеет два поля Тело и ТелоДвоичныеДанные, т.к. пользователь может закинуть нам как текст, так и картинку например.
Давайте потренируемся в обработке таких запросов, усовершенствуем наше приложение и научим его возводить в степень переданное число.
Вводить число мы будем по адресу http://localhost:3333/greeter/inputstepen, где будет форма ввода числа, и кнопка расчета. После расчета мы будем перенаправлены на http://localhost:3333/greeter/resultstepen. Форма будет передавать параметры методом POST.
Для реализации этой задумки добавим в наш контрол приветствия этот код, с двумя новыми точками маршрута:
&ТочкаМаршрута("inputstepen")
Процедура ВводСтепени(Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
Ответ.ТелоТекст =
"<form method=""post"" action=""/greeter/resultstepen"">
|<label for=""chislo"">Введи число:</label><br>
|<input type=""text"" id=""chislo"" name=""chislo""><br>
|<label for=""stepen"">Введи степень:</label><br>
|<input type=""text"" id=""stepen"" name=""stepen""><br><br>
|<input type=""submit"" value=""Посчитать"">
|</form> ";
КонецПроцедуры
&ТочкаМаршрута("resultstepen")
Процедура ВозводительВСтепень(Запрос, Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
ПостПараметры = Парсеры.ПараметрыИзТекста(Запрос.Тело);
Попытка
Решение = Pow(ПостПараметры["chislo"], ПостПараметры["stepen"]);
Исключение
Решение = "Ошибка при расчетах " + ОписаниеОшибки();
КонецПопытки;
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Ответ: %1 </div>", Решение);
КонецПроцедуры

Теперь разберемся, что тут произошло. Не буду останавливаться на описании HTML тегов, для этого в интернете сайтов больше, чем звезд на небе.
Точка маршрута inputstepen показала нам форму, которая при расчете перенаправляет нас на resultstepen и в теле запроса передает параметры формы, которые имеют вид chislo=2&stepen=3. Все что нам осталось, это обработать запрос.
Мы можем парсить самостоятельно, но можно внедрить объект Парсеры, который умеет парсить параметры в таком формате Парсеры.ПараметрыИзТекста(<СтрокаСПараметрами>). Этот метод вернет соответствие со значениями, которые в последствии нужно правильно использовать.
Если во входящем запросе придет заголовок "Content-Type:application/json" тогда его тело автоматом будет распарсено в структуру ТелоЗапросаОбъект с которой можно работать.
Если вам нужна десериализация тела запроса сразу в экземпляр нужного класса (типизированная модель), добавьте к параметру ТелоЗапросаОбъект аннотацию &Тип("ИмяТипа"), где ИмяТипа - это имя требуемого типа. Поддерживаются пользовательские классы, размеченные по правилам библиотеки jason.
&ТочкаМаршрута("postjsonbody")
Процедура ПроверкаПостЗапросаКакОбъект(Ответ, ТелоЗапросаОбъект, ЗаголовкиЗапроса) Экспорт
Ответ.УстановитьТипКонтента("txt");
Ответ.ТелоТекст = СтрШаблон("%1 %2", ТелоЗапросаОбъект.Имя, ТелоЗапросаОбъект.Фамилия);
КонецПроцедурыЕсли во входящем запросе придет заголовок "Content-Type:application/x-www-form-urlencoded" тогда его тело автоматом будет распарсено по именам и значениям, которые будет принимать метод точки маршрута
&ТочкаМаршрута("postformbody")
Процедура ПроверкаПостЗапросаКакФорма(Ответ, Имя, Фамилия) Экспорт
Ответ.УстановитьТипКонтента("txt");
Ответ.ТелоТекст = СтрШаблон("%1 %2", Имя, Фамилия);
КонецПроцедурыРабота с куками
Куки, это возможность сохранить на клиенте, в браузере, какую-либо информацию.
Объекты Запрос и Ответ, которые мы получаем в метод, который мы помечаем как ТочкаМаршрута. Оба этих объекта имеют свойство Куки. Соответственно во входящем запросе их можно читать, а в ответе устанавливать.
Модернизируем файл
&ТочкаМаршрута("setcookie")
Процедура УстановитьКуку(Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
ИмяКуки = "ДатаПоследнегоВхода";
ЗначениеКуки = ТекущаяДата();
НоваяКука = Ответ.Куки.Добавить(ИмяКуки, ЗначениеКуки);
Ответ.ТелоТекст = "<!DOCTYPE html>
|<div> Кука установлена </div>";
КонецПроцедуры
&ТочкаМаршрута("readcookie")
Процедура ПрочитатьКуку(Запрос, Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
ИмяКуки = "ДатаПоследнегоВхода";
ЗначениеКуки = Запрос.Куки.ПолучитьЗначениеПоИмени(ИмяКуки);
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Кука: %1 </div>", ЗначениеКуки);
КонецПроцедуры

Еще раз, не забываем, что куки хранятся на стороне браузера.
Хранение данных сессии
Еще один параметр точки маршрута Сессия имеет поле Данные. По сути, это соответствие, в которое можно записывать и читать любые значения. Эти данные хранятся на сервере, пока он работает. При остановке, данные сессии пропадают. При необходимости, в рамках приложения, можно дописать хранение данных сессии в файлах, базах данных и т.д.
Еще один пример
&ТочкаМаршрута("setsessiondata")
Процедура УстановитьДанныеСессии(Ответ, Сессия) Экспорт
Ответ.УстановитьТипКонтента("html");
ИмяПараметраСессии = "ДатаПоследнегоВхода";
ЗначениеПараметраСессии = ТекущаяДата();
Сессия.Данные[ИмяПараметраСессии] = ЗначениеПараметраСессии;
Ответ.ТелоТекст = "<!DOCTYPE html>
|<div> Данные сессии установлены </div>";
КонецПроцедуры
&ТочкаМаршрута("readsessiondata")
Процедура ПрочитатьДанныеСессии(Ответ, Сессия) Экспорт
Ответ.УстановитьТипКонтента("html");
ИмяПараметраСессии = "ДатаПоследнегоВхода";
ЗначениеПараметраСессии = Сессия.Данные[ИмяПараметраСессии];
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Значение параметра сессии: %1 </div>", ЗначениеПараметраСессии);
КонецПроцедуры

Публикация статичных файлов.
Часто нужно открывать доступ для скачивания всевозможных файлов. Таких как картинки, js-скрипты, css и т.д.
Для этого нужно сконфигурировать сервер, указав в файле autumn-properties.json нужные параметры. Этот файл нужно положить рядом с ПриветМир.os
{ "winow":
{
"КаталогиСФайлами": {
"/images": "./app/files"
}
}
}Про конфигурирование через этот файл расскажу ниже, а пока давайте попробуем сделать наше приложение повеселее и добавить картинок.
Добавим в каталог приложения пару картинок.
app/files/zl1.jpg
app/files/fun/zl2.jpg
Как видим, по заданному пути теперь доступны файлы из каталога, при чем с сохранением внутренней иерархии каталога файлов.
Стоит отметить, что доступны становятся не все файлы сразу, а только те, расширения которых описаны в соответствии
ОписанияТиповРасширений = Новый Соответствие();
ОписанияТиповРасширений.Вставить("htm","text/html; charset=utf-8");
ОписанияТиповРасширений.Вставить("html","text/html; charset=utf-8");
ОписанияТиповРасширений.Вставить("css","text/css");
ОписанияТиповРасширений.Вставить("js","text/javascript");
ОписанияТиповРасширений.Вставить("jpg","image/jpeg");
ОписанияТиповРасширений.Вставить("jpeg","image/jpeg");
ОписанияТиповРасширений.Вставить("png","image/png");
ОписанияТиповРасширений.Вставить("gif","image/gif");
ОписанияТиповРасширений.Вставить("ico","image/x-icon");
ОписанияТиповРасширений.Вставить("zip","application/x-compressed");
ОписанияТиповРасширений.Вставить("rar","application/x-compressed");
ОписанияТиповРасширений.Вставить("json","application/json");
ОписанияТиповРасширений.Вставить("txt","text/plain; charset=utf-8");При желании этот список можно расширить.
Работа с шаблонами страниц.
Если вы дочитали до этого пункта, я в первую очередь Вам благодарен. И в знак уважения, расскажу про механизм шаблонов. Я ведь раньше гордо заявил, что тут возможен подход MVC, так вот вы, наверное, все время задавались вопросом, где же V? И правда, писать код так:
Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
|<div> Имя: %1 </div>
|<div> Фамилия: %2 </div>", Имя, Фамилия);просто не удобно, и мало приличных слов для такого подхода можно подобрать, и ни в одном не будет буквы V. Но у меня есть решение!
Сразу покажу пример, а потом разберем по строчкам. Давайте отобразим страницу, на которой выведем текущее время, совершенно псевдослучайное число, динамически выведем случайное количество строк, и попробуем поиграться с условиями.
Поехали!
&Контроллер("/demoviews")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
&Отображение("./app/view/view1.html")
&ТочкаМаршрута("demo1")
Процедура ДемонстрацияОтображения(Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
ГСЧ = Новый ГенераторСлучайныхЧисел();
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 10);
Массив = Новый Массив();
Для Сч = 1 по СлучайноеЧисло Цикл
Массив.Добавить(Строка(Новый УникальныйИдентификатор()));
КонецЦикла;
Модель = Новый Структура();
Модель.Вставить("СлучайноеЧисло", СлучайноеЧисло);
Модель.Вставить("МассивСтрок", Массив);
Ответ.Модель = Модель;
КонецПроцедурыЧто тут нового? Во первых у точки маршрута появилась аннотация &Отображение("./app/view/view1.html"). А во вторых - определяется структура и устанавливается в Ответ.Модель. В этом весь секрет. После работы метода, на сцену выходит шаблонизатор, найдет указанный шаблон и разложит данные из модели, в соответствии с разметкой.
<!doctype html>
<html>
<head>
<title>Демонстрация работы отображений</title>
</head>
<body>
<div>Привет! Это отображение из шаблона. Точное время {{ ТекущаяДата() }}
<br>
<div>Ты это не увидишь, но тут объявляются переменные</div>
{%
ОднаПеременная = 1;
ВтораяПеременная = "Секрет";
%}
</div>
<div> Вот твое случайное число {{ Модель.СлучайноеЧисло }} </div>
{% Если Модель.СлучайноеЧисло > 5 Тогда %}
<div>Случайное число БОЛЬШЕ пяти</div>
{% Иначе %}
<div>Случайное число НЕ больше пяти</div>
{% КонецЕсли; %}
<div>Давай выведу строки из массива:</div>
{% Для Каждого СтрокаИзМассива из Модель.МассивСтрок Цикл %}
<div>Значение строки: {{ СтрокаИзМассива }} и оно достаточно случайно</div>
{% КонецЦикла; %}
<div>Ранее я объявил переменные, теперь покажу их</div>
<div>ОднаПеременная = {{ ОднаПеременная }}</div>
<div>ВтораяПеременная = {{ ВтораяПеременная }}</div>
</body>
</html>Если присмотреться, то шаблон это просто HTML разметка, которую смешали с 1сным кодом. Вот это коктейль получился!
Основные принципы разметки:
Выражения - обозначаются тегами. {{ <Выражение> }}. Тут может быть:
- Любое выражение на 1С, которое возвращает значение
{{ 1 + 3 }} - Переменная
{{ Модель.ЛюбоеЗначение }} - Функция
{{ Макс(1,5,9,7) }}
Операторы - обозначаются тегами. {% <КодНа1С> %}.
Это полноценный код на 1С. Можно объявлять переменные, взаимодействовать с Модель, использовать управляющие блоки(Циклы, Условия)

Обработчики шаблонов.
Может быть так, что до или после рендера модели в шаблоне, нужно выполнить некие манипуляции с текстом шаблона. Для выполнения этой операции нужно зарегистрировать обработчики событий до рендера и после. Например:
<div>
@ТекстЗаменыДоРендера@
{{Модель}}
@ТекстЗаменыПослеРендера@
</div>В этом шаблоне, мы хотим заменить вставки, неким текстом. Для этого добавим поделку два желудя.
&Желудь
&Прозвище("ПередОбработкойОтображения")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
Процедура Преобразовать(ТекстШаблона) Экспорт
ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыДоРендера@", "Шапка");
КонецПроцедуры&Желудь
&Прозвище("ПослеОбработкиОтображения")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
Процедура Преобразовать(ТекстШаблона) Экспорт
ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыПослеРендера@", "Подвал");
КонецПроцедурыТут мы добавили желуди с Прозвище "ПередОбработкойОтображения" и "ПослеОбработкиОтображения". Таких желудей может быть несколько. Но у каждого такого желудя должна быть процедура с именем ``Преобразовать```, в которую будет передан текст шаблона.
Компоненты.
Писать шаблоны круто, но что может быть еще круче? Писать меньше шаблонов, и переиспользовать уже имеющиеся. Представим, что вам в разных местах нужно отображать одну и туже информацию, (таблицы, элементы меню, и т.д.). для решения этой задачи, шаблон имеет секретную функцию {{ ВывестиПоШаблону(<Путь до шаблона>, <Модель для шаблона>) }}
Давайте покажу, как это работает
&Отображение("./app/view/view1.html")
&ТочкаМаршрута("demo1")
Процедура ДемонстрацияОтображения(Ответ) Экспорт
Ответ.УстановитьТипКонтента("html");
ГСЧ = Новый ГенераторСлучайныхЧисел();
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 10);
Массив = Новый Массив();
Для Сч = 1 по СлучайноеЧисло Цикл
Массив.Добавить(Строка(Новый УникальныйИдентификатор()));
КонецЦикла;
Модель = Новый Структура();
Модель.Вставить("СлучайноеЧисло", СлучайноеЧисло);
Модель.Вставить("МассивСтрок", Массив);
// добавим в модель второй массив
МассивФруктов = Новый Массив();
МассивФруктов.Добавить("Яблоко");
МассивФруктов.Добавить("Апельсин");
МассивФруктов.Добавить("Банан");
МассивФруктов.Добавить("Желудь");
Модель.Вставить("ВторойМассив", МассивФруктов);
Ответ.Модель = Модель;
КонецПроцедуры <!doctype html>
<html>
<head>
<title>Демонстрация работы отображений</title>
</head>
<body>
<div>Привет! Это отображение из шаблона. Точное время {{ ТекущаяДата() }}
<br>
<div>Ты это не увидишь, но тут объявляются переменные</div>
{%
ОднаПеременная = 1;
ВтораяПеременная = "Секрет";
%}
</div>
<div> Вот твое случайное число {{ Модель.СлучайноеЧисло }} </div>
{% Если Модель.СлучайноеЧисло > 5 Тогда %}
<div>Случайное число БОЛЬШЕ пяти</div>
{% Иначе %}
<div>Случайное число НЕ больше пяти</div>
{% КонецЕсли; %}
<br>
<div>Давай выведу строки из массива:</div>
{{ ВывестиПоШаблону("./app/view/printarray.html", Модель.МассивСтрок) }}
<br>
<div>Давай выведу строки из второго массива:</div>
{{ ВывестиПоШаблону("./app/view/printarray.html", Модель.ВторойМассив) }}
<br>
<div>Ранее я объявил переменные, теперь покажу их</div>
<div>ОднаПеременная = {{ ОднаПеременная }}</div>
<div>ВтораяПеременная = {{ ВтораяПеременная }}</div>
</body>
</html><div>
{% Для Каждого СтрокаИзМассива из Модель Цикл %}
<div>Значение строки: {{ СтрокаИзМассива }}</div>
{% КонецЦикла; %}
</div>
Общее отображение контрола.
Для удобства разработки веб приложения хочется разделить отображения, и добавить что-то общее для всех точек маршрута. Например, общая html разметка, с заголовками, меню, подвалом и тд. Для этих целей есть возможность с помощью аннотации в конструкторе контрола указать общий шаблон.
&Контроллер("/demoviews")
&Отображение(Шаблон = "./hwapp/view/main.html", Метод = "ПолучитьМодельКонтрола")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
Функция ПолучитьМодельКонтрола(Запрос) Экспорт
Модель = Новый Структура("Заголовок, Дата", "Демонстрация работы отображений", Запрос.ДатаПолучения);
Возврат Модель;
КонецФункцииГде &Отображение(Шаблон = "./hwapp/view/main.html", Метод = "ПолучитьМодельКонтрола") аннотация, указывает где расположен шаблон, и каким методом для него формируется модель с данными. Параметры этого метода так же могут быть выбраны, аналогично методам точек маршрута.
А вот так выглядит общий шаблон
<!doctype html>
<html>
<head>
<title>{{Модель.Заголовок}}</title>
</head>
<body>
<div>Шапка страницы! Дата получения запроса: {{Модель.Дата}}</div>
@Контент
<div>Подвал страницы</div>
</body>
</html>Где тег @Контент будет заменен результатом ответа точки маршрута.
Однако бывают ситуации, когда у контроллера есть отображение, но какая точка маршрута должна возвращать ответ, без его применения. В такой ситуации, для метода точки маршрута нужно добавить аннотацию &НеВыводитОтображениеКонтроллера. Например, вот так:
&Отображение("./app/view/view1.html")
&ТочкаМаршрута("demo2")
&НеВыводитОтображениеКонтроллера
Процедура ДемонстрацияОтображенияБезОбщегоОтображения(Ответ) Экспорт
...
КонецПроцедурыОтветы по ошибкам.
Обрабатывая входящие запросы, могут случиться исключения. Ну кто с первого раза напишет правильно код? Сервер при исключении вернет страницу с кодом 500. Шаблон этой страницы можно переопределить. Моделью там будет структура
Ответ.Модель = Новый Структура();
Ответ.Модель.Вставить("КодСостояния", 500);
Ответ.Модель.Вставить("ТекстСообщения", ТекстОшибки);
Ответ.Модель.Вставить("Запрос", Запрос);Ну а какой пользователь с первого раза введет без ошибки адрес ресурса? Сервер вернет ему 404. Шаблон этой ошибки так же можно переопределить. Модель там следующая
Ответ.Модель = Новый Структура();
Ответ.Модель.Вставить("КодСостояния", 404);
Ответ.Модель.Вставить("ТекстСообщения", "Страница не найдена");
Ответ.Модель.Вставить("Запрос", Запрос);А вот таким не замысловатым способом можно переопределить шаблон стандартной ошибки
&ФинальныйШтрих
Процедура ПостИнициализация() Экспорт
ОбщийКонтейнер.МенеджерОтображений.УстановитьШаблон404("<!DOCTYPE html>
|<div><h1> {{ Модель.КодСостояния }} </h1></div>
|<div> {{ Модель.ТекстСообщения }} </div>
|<div> Искомый ресурс {{ Модель.Запрос.Путь }} не найден </div>");
КонецПроцедурыПеренаправление
Иногда бывает так, что нужно с одной страницы, перенаправить пользователя на другую.
Для этого, у объекта Ответ есть метод Перенаправить(<Адрес куда перенаправить>). Например, подобная точка маршрута будет перенаправлять запрос в корень приложения
&ТочкаМаршрута("/redir")
Процедура Перенаправление(Ответ) Экспорт
Ответ.Перенаправить("/");
КонецПроцедурыЗагрузка настроек из файла
Настройки порта, имени хоста, каталогов файлов и приложений можно можно хранить в json файле, и загружать при старте приложения.
Файл обязательно должен называться autumn-properties.json / autumn-properties.yml / autumn-properties.yaml, и быть в корне запуска сервера или в подкаталоге src/. Если какие-то значения не указаны, то они имеют значения по умолчанию.
Пример:
{ "winow":
{
"Порт": 3331, // по умолчанию 3333
"АвтоСтарт": true, // Управляет автоматическим запуском при Поделка.ЗапуститьПриложение() (по умолчанию Истина)
"ИмяХоста": "MySuperAPPHost", // по умолчанию localhost
"КаталогСПриложениями": "./hwapp", // по умолчанию "./app"
"РазмерБуфера": 1024, // Размер порции в байтах, которыми читаются данные из TCP соединения. (по умолчанию 1024)
"КаталогиСФайлами": {
"/images": "./hwapp/files" // значения по умолчанию нет
}
}
}Управление доступом
Для управления доступом к точке маршрута, предусмотрена аннотация &Роли("<Список ролей через запятую>"). Все очень просто, и остается ответить только на один вопрос - как эти роли раздать, и как хранить данные входа пользователей. Пока это mvp, точного ответа не дам. Разработчик может самостоятельно придумать, как и где хранить группы и пароли. Я только покажу, как их подключить в наше приложение.
&Пластилин
Перем МенеджерДоступа Экспорт;
&Контроллер("/sec")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
&ФинальныйШтрих
Процедура ПроинициализироватьРоли() Экспорт
// Инициализация данных входа "пользователей"
МенеджерДоступа.ДобавитьТокен("Админ", "123");
МенеджерДоступа.ДобавитьТокен("Пользователь", "111");
// Назначение ролей "пользователям"
МенеджерДоступа.ДобавитьРольЛогина("Админ", "Администраторы");
МенеджерДоступа.ДобавитьРольЛогина("Админ", "Пользователи");
МенеджерДоступа.ДобавитьРольЛогина("Пользователь", "Пользователи");
КонецПроцедуры
// Точка доступная роли пользователи
&Роли("Пользователи")
&ТочкаМаршрута("user")
Процедура Пользователь(Ответ) Экспорт
Ответ.ТелоТекст = "Пользователи";
КонецПроцедуры
// Точка доступная роли Администраторы
&Роли("Администраторы")
&ТочкаМаршрута("admin")
Процедура Админ(Ответ) Экспорт
Ответ.ТелоТекст = "Админка";
КонецПроцедуры
// обычная точка, доступная всем
&ТочкаМаршрута("free")
Процедура Все(Ответ) Экспорт
Ответ.ТелоТекст = "Все подрят";
КонецПроцедуры
// Точка доступная ролям администраторы, пользователи.
&Роли("Администраторы, Пользователи")
&ТочкаМаршрута("usradm")
Процедура АдминыИПользователи(Ответ) Экспорт
Ответ.ТелоТекст = "Админы и пользователи";
КонецПроцедурыРабота с протоколом WebSocket
В winow в экспериментальном виде реализована поддержка веб-сокетов. Спека не полная, поддерживаются пока только текстовые сообщения. Разберем работу протокола на примере онлайн чата. Я не буду углубляться в часть фронта. Вот пример реализации на фронте. Основная суть такая - когда мы заходим на контрол /chat осуществляется проверка, залогинился пользователь или нет. Если нет, переадресуем на страницу ввода логина.
После ввода логина мы попадаем на страницу с чатом, где осуществляется подключение вебсокета, и начинается обмен сообщениями.
А теперь разберем пример того, что происходит на стороне сервера. Обработкой входящих сообщений занимается такой же контроллер с точками маршрута, к которым мы привыкли. Каждая точка маршрута является "топиком" в рамках которого общается одно соединение веб сокета. В примере ниже у нас контроллер по адресу chat и точкой маршрута message. У нас одно соединение, которое обменивается сообщениями в топике /chat/message. Точка маршрута по обработке сообщений может принимать несколько параметров:
- Идентификатор - идентификатор сессии, в которой можно хранить разные данные.
- Топик - имя топика, в рамках которого происходит общение
- Сообщение - расшифрованное сообщение, которое пришло от клиента.
Для того, чтобы отправлять сообщения, нужно получить желудь БрокерСообщенийВебСокетов. Который умеет следующие действия с сообщениями:
- ОтправитьСообщение(Топик, Сообщение, Идентификатор) - Отправляет определенному клиенту сообщение в указанный топик.
- ОтправитьСообщениеВсем(Топик, Сообщение) - Отправляет сообщение всем клиентам, подключенным к указанному топику.
- ОтправитьСообщениеСписку(Топик, Сообщение, СписокИдентификаторов) - Отправляет сообщение массиву клиентов, подписанных на указанный топик.
- ОтправитьСообщениеВсемКроме(Топик, Сообщение, СписокИсключенийИдентификаторов) - Отправляет сообщение всем, клиентам подписанным на указанный топик, кроме массива, переданного как параметр.
Так же контроллер может иметь методы, помеченные аннотациями &ПриПодключенииВебСокета("/имя/топика") и ПриОтключенииВебСокета("/имя/топика"). Которые будут вызваны, после соответствующих событий, и принимать Идентификатор клиента, с которым произошло событие.
Вот полный пример с комментариями.
&Пластилин Перем БрокерСообщенийВебСокетов; // Инжектим желудь, который управляет отправкой сообщений
Перем КешИменПользователей;
&Контроллер("/chat") // помечаем контроллер и инициализируем кеш.
Процедура ПриСозданииОбъекта()
КешИменПользователей = Новый Соответствие();
КонецПроцедуры
&ТочкаМаршрута("message") // Обработчик входящего сообщения
Процедура ВходящееСообщение(Идентификатор, Топик, Сообщение) Экспорт
// клиент понимает два вида сообщений, которые он отправил сам, и которые отправлены другими клиентами. Они по-разному отображаются на фронте.
// Тут мы получаем из кеша имя пользователя по идентификатору соединения и отправляем клиентам.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Получено сообщение %1 от %2", Сообщение, ИмяПользователя));
Массив = Новый Массив();
Массив.Добавить(Идентификатор);
ТекстПолучения = ФорматированноеСообщение(ИмяПользователя, Сообщение, Истина);
ТекстОтправки = ФорматированноеСообщение(ИмяПользователя, Сообщение, Ложь);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсемКроме(Топик, ТекстПолучения, Массив);
БрокерСообщенийВебСокетов.ОтправитьСообщениеСписку(Топик, ТекстОтправки, Массив);
КонецПроцедуры
&Отображение("./hwapp/view/chat.html")
&ТочкаМаршрута("") // точка маршрута, которая отдаем страницу клиента.
Процедура Главная(Сессия, Ответ) Экспорт
// если пользователь не закеширован, перенаправляем на страницу логина.
Имя = КешИменПользователей.Получить(Сессия.Идентификатор());
Если Имя = Неопределено Тогда
Ответ.Перенаправить("/chat/login");
КонецЕсли;
КонецПроцедуры
&ТочкаМаршрута("login") // страница ввода логина.
&Отображение("./hwapp/view/chatlogin.html")
Процедура Логин() Экспорт
КонецПроцедуры
&ТочкаМаршрута("loginprocess") // обработка введенного логина, с кешированием имени.
Процедура ОбработкаЛогина(ИмяПользователя, Сессия, Ответ) Экспорт
Если НЕ ЗначениеЗаполнено(ИмяПользователя) Тогда
ГенераторСлучайныхЧисел = Новый ГенераторСлучайныхЧисел(ТекущаяУниверсальнаяДатаВМиллисекундах());
ИмяПользователя = "Noname" + Строка(ГенераторСлучайныхЧисел.СлучайноеЧисло(1, 999));
КонецЕсли;
Сообщить(СтрШаблон("Регистрация пользователя %1", ИмяПользователя));
КешИменПользователей.Вставить(Сессия.Идентификатор(), ИмяПользователя);
Ответ.Перенаправить("/chat");
КонецПроцедуры
&ПриПодключенииВебСокета("/chat/message") // подписка на подключение пользователя
Процедура ПриПодключенииПользователя(Идентификатор) Экспорт
// сообщим всем, что зашел новый пользователь.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Подключился %1", ИмяПользователя));
ТекстСообщенияГостю = ФорматированноеСообщение("Оракул", "Привет " + ИмяПользователя + " !", Истина);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсем("/chat/message", ТекстСообщенияГостю);
КонецПроцедуры
&ПриОтключенииВебСокета("/chat/message") // подписка на отключение пользователя.
Процедура ПриОтключенииПользователя(Идентификатор) Экспорт
// сообщим всем, что пользователь вышел.
ИмяПользователя = КешИменПользователей.Получить(Идентификатор);
Сообщить(СтрШаблон("Отключился %1", ИмяПользователя));
ТекстСообщения = ФорматированноеСообщение("Оракул", ИмяПользователя + " покинул чат", Истина);
БрокерСообщенийВебСокетов.ОтправитьСообщениеВсем("/chat/message", ТекстСообщения);
КонецПроцедуры
Функция ФорматированноеСообщение(Автор, Текст, Получен)
ОбъектДляПарсинга = Новый Структура("Author, Time, Text, rcv", Автор, Формат(ТекущаяДата(), "ДФ=ЧЧ:мм"), Текст, Получен);
Запись = новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, ОбъектДляПарсинга);
Возврат Запись.Закрыть();
КонецФункцииВот результат наших трудов.

Работа с механизмом server-sent events.
Подробно можно прочитать на вики.
Для реализации работы с механизмом нужно выполнить несколько шагов.
- Зарегистрировать топик, в конструкторе контроллера.
- Добавить обработчики событий, которые будут вызываться при подключении и отключении клиента. (опционально)
- Посылать сообщения клиенту в топики, при необходимости в соответствии с логикой приложения.
Рассмотрим на примере:
// Подключаем необходимые зависимости
&Пластилин Перем БрокерСообщенийСобытийСервера;
&пластилин Перем ФабрикаОтветов;
&Контроллер("/sse")
&Отображение(Шаблон = "./hwapp/view/main_sse.html")
Процедура ПриСозданииОбъекта(&Пластилин ТопикиСерверныхСобытий)
ИмяТопика = "/sse/acorndiscussion";
// регистрируем топик и обработчики открытия и закрытия
ТопикиСерверныхСобытий.Добавить(ИмяТопика,
Новый Действие(ЭтотОбъект, "НовоеПодключениеССЕ"),
Новый Действие(ЭтотОбъект, "ОтключениеССЕ"));
КонецПроцедуры
Процедура НовоеПодключениеССЕ(Сессия, ИД) Экспорт
// Код обработчика открытия соединения
КонецПроцедуры
Процедура ОтключениеССЕ(Сессия, ИД) Экспорт
// Код обработчика закрытия соединения
КонецПроцедуры
Процедура ОтправитьСообщение()
// Создаем сообщение
Сообщение = ФабрикаОтветов.СерверноеСобытие();
Сообщение.ТипСобытия("like");
Сообщение.ДобавитьСтроку("Некий текст");
// Отправим сообщение всем клиентам, слушающим топик.
БрокерСообщенийСобытийСервера.ОтправитьСообщениеВсем(ИмяТопика, Сообщение);
КонецПроцедурыПример клиента который подписывается на топик, и вызывает события в зависимости от типа полученного сообщения :
eventSource = new EventSource('sse/acorndiscussion');
eventSource.addEventListener('like', function(e) {
likeElem.innerHTML = e.data;
});
eventSource.addEventListener('watch', function(e) {
watchElem.innerHTML = e.data;
});
eventSource.addEventListener('newComment', function(e) {
addComment(e.data);
});Полный пример можно посмотреть примерах - контрол и клиент.
Апи объектов:
БрокерСообщенийВебСокетов
- ОтправитьСообщениеВсем(Топик, Сообщение): Отправка сообщения всем;
- ОтправитьСообщениеПоИдСоединения(ИдСоединения, Сообщение): Отправка сообщения конкретному клиенту;
- ОтправитьСообщениеСписку(Топик, Сообщение, МассивИдентификаторов): Отправка сообщения массиву клиентов;
- ОтправитьСообщениеВсемКроме(Топик, Сообщение, МассивИсключенийИдентификаторов): Отправка сообщения с исключающим массивом клиентов;
ТопикиСерверныхСобытий
- Существует(Топик): Проверка существования топика;
- Добавить(Топик, ОбработчикОткрытия, ОбработчикЗакрытия): Добавление топика. С возможностью подписки на события открытия и закрытия соединения. Тут принимаются объекты Действие, которые должны иметь интрефейс:
Процедура ИмяОбработчика(Сессия, ИД) ЭкспортГде сессия - идентификатор сессии, и идентифкатор конкретного соединения.
Сообщение. Получается из фабрики ответов. Сообщение = ФабрикаОтветов.СерверноеСобытие();
- ТипСобытия(ТипСобытия): Установка типа события;
- ДобавитьСтроку(Строка): Добавление строки в сообщение;
- Идентификатор(ид) : Установка идентификатора сообщения;
Пример реактивного интерфейса на server sent events

OpenTelemetry
winow интегрирован с OpenTelemetry через библиотеки opentelemetry и autumn-opentelemetry.
Поддерживается отправка всех трёх сигналов — трассировки, метрик и логов. На пользовательских классах работают аннотации наблюдения и метрик из autumn-opentelemetry: любой ваш желудь можно разметить и получать по нему спаны и измерения.
Сверх этого winow добавляет своё: трассировку входящих HTTP-запросов и распределённую трассировку между сервисами. Ничего писать для этого не нужно — достаточно включить телеметрию в настройках.
Про сами аннотации, метрики и настройку SDK читайте документацию autumn-opentelemetry.
Включение
По умолчанию телеметрия выключена: пока флаг не взведён, SDK не поднимается, экспортёры не стартуют и накладных расходов нет.
{
"winow": {
"Порт": 3333
},
"otel": {
"enabled": true,
"service": {
"name": "мой-сервис"
}
}
}Ничего подключать в точке входа не нужно: winow сам тянет autumn и autumn-opentelemetry.
#Использовать winow
Поделка = Новый Поделка;
Поделка.ЗапуститьПриложение();Остальные параметры — адрес коллектора, протокол, сэмплирование, лимиты — описаны в документации opentelemetry.
Логи
Логи приложения можно отправлять тем же транспортом, что и спаны. Для этого в конфигурацию logos добавляется аппендер ОтелАппендерLogos:
{
"logos": {
"logger": {
"rootLogger": {
"level": "INFO",
"appenders": ["otel", "console"]
}
},
"appender": {
"otel": {
"type": "ОтелАппендерLogos",
"level": "INFO"
},
"console": {
"type": "ВыводЛогаВКонсоль",
"level": "INFO"
}
}
}
}Записи, сделанные во время обработки запроса, попадут в бэкенд с идентификаторами трассировки и спана, поэтому лог можно смотреть рядом с трейсом.
Что попадает в серверный спан
Ниже — про спан, который winow заводит на каждый входящий HTTP-запрос. Спаны и метрики от аннотаций наблюдения устроены иначе и описаны в документации autumn-opentelemetry.
Имя спана — метод запроса и шаблон маршрута, например GET /calc/{Число1}/multiply/{Число2}. В имени стоит шаблон, а не конкретный путь, иначе имён операций стало бы столько же, сколько различных URL. Если запрос не сопоставился ни одному маршруту, именем остаётся один метод.
Атрибуты соответствуют HTTP semantic conventions:
http.request.method,http.route,http.response.status_codeurl.scheme,url.path,url.queryserver.address,server.port— из заголовкаHostclient.address,client.portuser_agent.original
Ответы 5xx помечают спан статусом ошибки. Коды 4xx не помечают: по соглашениям это ошибка вызывающей стороны, а не обработчика.
Распределённая трассировка
Если во входящем запросе есть заголовок traceparent, winow продолжит начатую трассировку: серверный спан станет дочерним для спана вызывающей стороны, и оба окажутся в одном трейсе.
В обратную сторону контекст сам не уедет. Исходящий вызов нужно оборачивать собственным спаном вида Клиент и уже из него внедрять контекст в заголовки: если просто прокинуть заголовки, вызываемый сервис станет дочерним для серверного спана, и сам сетевой вызов в трейсе не будет виден — ни его длительность, ни ошибка.
#Использовать opentelemetry
#Использовать 1connector
Перем Трассировщик;
Перем Пропагаторы;
&Контроллер("/заказы")
Процедура ПриСозданииОбъекта(&Пластилин ОтелSdk)
Трассировщик = ОтелSdk.ПолучитьТрассировщик("заказы");
Пропагаторы = ОтелSdk.Пропагаторы();
КонецПроцедуры
&ТочкаМаршрута("оформить")
Процедура Оформить(Ответ, ТелоЗапроса) Экспорт
Спан = Трассировщик.НачатьСпан("POST /склад/резерв", ОтелВидСпана.Клиент());
Область = Спан.СделатьТекущим();
Попытка
ЗаголовкиЗапроса = Новый Соответствие();
Пропагаторы.Внедрить(ОтелКонтекст.Текущий(), ЗаголовкиЗапроса);
ДопПараметры = Новый Структура("Заголовки", ЗаголовкиЗапроса);
Результат = КоннекторHTTP.Post("http://localhost:3334/склад/резерв", ТелоЗапроса, , ДопПараметры);
Спан.УстановитьАтрибут("http.response.status_code", Результат.КодСостояния);
Исключение
Спан.ЗаписатьИсключение(ИнформацияОбОшибке());
Спан.УстановитьСтатус(ОтелКодСтатуса.Ошибка(), ОписаниеОшибки());
Область.Закрыть();
Спан.Завершить();
ВызватьИсключение;
КонецПопытки;
Область.Закрыть();
Спан.Завершить();
КонецПроцедурыРабочий пример из двух микросервисов лежит в каталогах example и example-tracing.
Спаны методов
Желуди самого winow размечены аннотациями инструментирования, поэтому при включённой трассировке спаны появляются не только вокруг запроса, но и вокруг методов контроллеров, менеджеров доступа, отображений и сессий. Это даёт видимость внутри обработки запроса, но и стоит времени на каждом вызове — учитывайте это, если включаете телеметрию под нагрузкой.
Спецификация OpenAPI и Swagger UI
winow умеет автоматически строить спецификацию OpenAPI 3.1 по зарегистрированным точкам маршрутов и отдавать интерактивную документацию Swagger UI. Ничего описывать вручную не нужно — спецификация формируется из уже существующих контроллеров.
Включение
По умолчанию функциональность выключена, чтобы не менять поведение существующих приложений. Чтобы включить, добавьте в autumn-properties.json настройку winow.openapi.Включено:
{
"winow": {
"openapi": {
"Включено": true
}
}
}После запуска приложения станут доступны три адреса — здесь и далее при настройках по умолчанию, то есть при winow.Порт = 3333 и нетронутых БазовыйПуть и ПутьСпецификации:
http://localhost:3333/swagger-ui.html— страница Swagger UI.http://localhost:3333/v3/api-docs— сама спецификация в формате JSON.http://localhost:3333/v3/api-docs.yaml— она же в формате YAML.
Адреса по умолчанию совпадают с springdoc-openapi 2.x. Адрес YAML отдельно не настраивается: он всегда получается из ПутьСпецификации добавлением суффикса .yaml.

Что попадает в спецификацию
Для каждой точки маршрута winow определяет:
Путь — из аннотаций
&Контроллери&ТочкаМаршрута, включая шаблоны параметров пути вида{id}.Параметры пути (
in: path) — все шаблоны{...}из адреса точки маршрута.Параметры строки запроса (
in: query) — именованные параметры метода, кроме служебных (Запрос,Ответ,Сессияи т.д.).Тело запроса — если метод принимает тело (
ТелоЗапроса,ТелоЗапросаОбъект,ДанныеФормы,ДвоичныеДанныеЗапросаи т.п.). Тип содержимого выбирается по параметру:ДанныеФормы→multipart/form-data, двоичные данные →application/octet-stream, иначеapplication/json. Если для параметраТелоЗапросаОбъектчерез аннотацию&Типуказан тип, тело тоже считается заполненным. Обязательность тела объявляется аннотацией&Заполненона параметре тела — той же, которой отмечают обязательные поля схемы:bsl&ТочкаМаршрута("товары") Процедура ЗавестиТовар(Ответ, &Заполнено &Тип("Товар") ТелоЗапросаОбъект) ЭкспортБез неё тело считается необязательным (
requestBody.requiredпо спецификации и так по умолчанию ложь, поэтому ключ не пишется вовсе). Учтите, чтоvalidateэту аннотацию на параметре метода не проверяет — валидатор обходит свойства объекта, а не аннотации параметра, — поэтому здесь она объявляет контракт, но не обеспечивает его: см. autumn-validate#4.HTTP-метод — по умолчанию
get, а для точек маршрута, читающих тело запроса, —post. Список методов задаётся параметромМетоданнотации&ТочкаМаршрута(через запятую; допустимыget,put,post,delete,options,head,patch,trace). Метод — часть маршрута, а не пометка поверх него, поэтому объявляется там же, где адрес.WARNING
Пока
Методвлияет только на спецификацию: маршрутизатор по-прежнему принимает точку маршрута любым HTTP-методом и на неподдерживаемый отвечает не405, а как обычно. Не полагайтесь на объявление как на ограничение — см. #131.Безопасность — если для точки маршрута заданы роли через аннотацию
&Роли, в спецификацию добавляется схемаbasicAuthи список требуемых ролей.Группа — раздел документации, в который попадает операция (в спецификации это
tags). По умолчанию адрес контроллера, задаётся явно аннотацией&Группаили параметромГруппыаннотации&Операция.Произвольные параметры запроса — если метод принимает
ПараметрыЗапросаИменные, в спецификацию добавляется параметр со свободным составом полей: перечислить их поимённо нельзя, но известно, что точка маршрута принимает произвольные пары имя-значение.Позиционные сегменты пути — если метод принимает
ПараметрыЗапросаПорядковые, это отмечается в описании операции. Параметром такие сегменты не описать: по OpenAPI параметр сin: pathобязан быть обязательным и присутствовать в шаблоне пути, а количество сегментов заранее неизвестно.
Схемы типов
Если у параметра ТелоЗапросаОбъект аннотацией &Тип указан тип, winow строит по нему схему в components/schemas, а тело запроса ссылается на неё. Состав полей берётся из аннотаций, которыми тип уже размечен для сериализации и валидации, — размечать тип отдельно ради документации не нужно:
| Аннотация | В схеме |
|---|---|
&Сериализуемое("имя") | имя свойства в JSON |
&Сериализуемое(Обязательное = Истина), &Заполнено | попадает в required |
&Несериализуемое | свойство исключается из схемы |
&Тип("Строка") | type, а для пользовательского типа — $ref |
&Минимум(N), &Максимум(N) | minimum и maximum |
&Размер(Минимум = N, Максимум = N) | minLength/maxLength, minItems/maxItems либо minProperties/maxProperties — смотря какого типа схема |
&Шаблон("рег.выр.") | pattern |
&ОдинИз(А, Б, В) | enum |
&Кратно(N) | multipleOf |
&Формат("email") | format |
&Истина, &Ложь | boolean с единственным допустимым значением (const) |
&ДляКаждого("Значение") | последующие ограничения относятся к элементам: items у массива, additionalProperties у объекта |
&ДляКаждого("Ключ") | последующие ограничения относятся к ключам объекта: propertyNames |
Двоичное тело (ДвоичныеДанныеЗапроса) схемой не описывается: сырые байты — не строка JSON, тип содержимого уже назван ключом application/octet-stream, и по спецификации 3.1 произвольный двоичный файл показывается пустым объектом.
Поля, допускающие null
&Сериализуемое(Обязательное = Истина) значит «ключ есть в JSON всегда» — а при пустом значении поля jason выдаёт "поле": null. Поэтому у таких полей тип в схеме допускает null. Отдельного nullable, как в 3.0, в 3.1 нет: null становится ещё одним членом списка типов.
"comment": { "type": ["string", "null"] }Рядом со ссылкой так не выйдет: соседние с $ref ключи применяются вместе со ссылкой, и значению пришлось бы удовлетворять обеим схемам разом. Обнуляемая ссылка выражается выбором:
"altAddress": { "anyOf": [ { "$ref": "#/components/schemas/Adres" }, { "type": "null" } ] }&Заполнено обнуляемость отменяет: validate требует непустого значения, и null ему не подходит. Так же действуют &Истина и &Ложь — они называют конкретное значение.
Поле без Обязательное при пустом значении из JSON просто исчезает, а не становится null, поэтому оно не обнуляемое — оно необязательное, и в required не попадает.
Вложенные пользовательские типы описываются отдельными схемами и связываются ссылками, поэтому одинаковые типы не дублируются в документе.
&Минимум и &Максимум сравнивают значение целиком, а не его длину, поэтому им соответствуют minimum и maximum, а не minLength и maxLength. Длину и число элементов ограничивает &Размер, и какая пара ключевых слов ему отвечает, зависит от типа схемы.
Аннотации свойства читаются в порядке написания: &ДляКаждого ничего не ограничивает сам, а переадресует последующие ограничения элементам коллекции или её ключам. По той же причине &Заполнено после &ДляКаждого требует заполненности элементов, а не наличия самого поля, и в required не попадает.
Тип элементов коллекции в OneScript нигде не объявлен, поэтому без &ДляКаждого про содержимое массива сказать нечего и items остаётся пустым — «элемент любого вида».
Ключи components/schemas по спецификации обязаны совпадать с ^[a-zA-Z0-9._-]+$, поэтому кириллические имена классов транслитерируются: ЗаявкаOpenApiТест становится ZayavkaOpenApiTest. Исходное имя класса не теряется — оно попадает в description схемы. Если разные классы дают одинаковую латиницу, к повторному имени добавляется числовой суффикс.
Уточнение описания
Необязательные аннотации помогают сделать документацию точнее. Их набор смоделирован по springdoc-openapi: есть богатая &Операция с параметром на каждое поле операции, и при этом остаются отдельные аннотации на те же поля.
&Операция(Сводка, Описание, Группы, Устарело, Идентификатор)— всё, что говорят об операции словами, в одном месте. Обе формы равноправны, выбирают ту, что короче в конкретном месте, а значения складываются — группы объединяются, устаревание срабатывает от любой из двух.Сводка— одна короткая строка (summary). Именно её Swagger UI показывает в заголовке свёрнутой строки. Без неё туда идёт имя процедуры контроллера, а оно называет реализацию, а не то, что операция даёт потребителю.Описание— всё, что длиннее одной строки (description). К нему winow дописывает выведенное сам: требуемые роли, приём позиционных сегментов пути, чем заменить устаревшую операцию. Авторский текст идёт первым.Группы— имена групп через запятую (tags).Устарело— признак устаревшей операции (deprecated).Идентификатор— имя операции (operationId). Без него строится из HTTP-метода и пути. Объявленное имя всё равно проходит транслитерацию и проверку на уникальность.
bsl&ТочкаМаршрута("товары/{Артикул}") &Операция(Сводка = "Товар по артикулу", Описание = "Ищет товар в каталоге. Если товара нет, отвечает 404.", Группы = "Каталог") Процедура Товар(Ответ, Артикул) Экспорт&Группа("Имя", Описание = "...")— группа, в которую попадает операция. Ставится и над конструктором контроллера, и над точкой маршрута: группа контроллера относится ко всем его операциям, группа точки маршрута её дополняет. Аннотацию можно указать несколько раз. Без неё группой служит адрес контроллера, поэтому всё, что висит на корневом, сваливается в одну группу/. Описание достаточно задать при одном упоминании группы — оно попадёт в разделtagsдокумента и будет показано у всей группы.Названа группой, а не тегом, из-за пересечения имён: класс
АннотацияТегуже есть в oneunit, где&Тегпомечает тесты для выборочного прогона. Классы с одинаковым именем вытесняют друг друга при загрузке, и какой победит — зависело бы от порядка подключения библиотек.&Устарело("чем пользоваться вместо")— помечает операцию устаревшей (deprecated), Swagger UI показывает её зачёркнутой. Ставится над точкой маршрута, над конструктором контроллера — тогда устаревшими считаются все его операции, — или над отдельным параметром метода. Текст добавляется к описанию операции: сама пометка не говорит, куда переходить. Своя пометка точки маршрута уточняет пометку контроллера.&Возвращает(Тип, Код, Описание, Элемент, ТипСодержимого)— описание возможного ответа. Точка маршрута заполняет объектОтвети ничего не возвращает, поэтому тип ответа вывести неоткуда — его объявляют этой аннотацией. Её можно указать несколько раз, по одной на код состояния. Как только объявлен хотя бы один ответ, он замещает ответ200по умолчанию.Для ответа-коллекции тип элементов называют параметром
Элемент— сам по себе он нигде не хранится. У массива он попадает вitems, у соответствия — вadditionalProperties:bsl&ТочкаМаршрута("") &Возвращает(Тип = "Массив", Элемент = "Товар", Описание = "Найденные товары") Процедура Товары(Ответ) ЭкспортБез
Элементколлекция описывается как «элемент любого вида» — заворачивать список в тип-обёртку ради схемы не нужно.Тип содержимого ответа задаётся параметром
ТипСодержимого, по умолчаниюapplication/json. Вывести его неоткуда: winow задаёт заголовок вызовомОтвет.УстановитьТипКонтентауже во время обработки запроса, статически этого не видно.bsl&Возвращает(Тип = "Строка", ТипСодержимого = "text/csv", Описание = "Выгрузка")Для
application/octet-streamсхема не пишется — по той же причине, что и у двоичного тела запроса.Заголовок ответа из объявления не заполняется
ТипСодержимогопопадает только в документ. ЗаголовокContent-Typeсервер по-прежнему ставит сам — изОтвет.УстановитьТипКонтента, а без него отдаётtext/html. То есть объявитьtext/csvи забыть выставить тип в коде — значит получить документ, расходящийся с ответом. За согласованность пока отвечает автор.
&ТочкаМаршрута("{id}")
&Операция(Сводка = "Получить товар по идентификатору")
&Возвращает(Тип = "Товар", Описание = "Найденный товар")
&Возвращает(Код = 404, Описание = "Товар не найден")
Процедура Товар(Ответ, Id) Экспорт
// ...
КонецПроцедуры&Контроллер("/api/goods")
Процедура ПриСозданииОбъекта()
КонецПроцедуры
&ТочкаМаршрута("{id}")
&Операция(Сводка = "Получить товар по идентификатору")
Процедура Товар(Ответ, Id) Экспорт
Ответ.УстановитьТипКонтента("json");
// ...
КонецПроцедуры
&ТочкаМаршрута("", Метод = "post")
&Операция(Сводка = "Создать товар")
Процедура Создать(Ответ, ТелоЗапросаОбъект) Экспорт
// ...
КонецПроцедурыНастройки
Все настройки задаются в блоке winow.openapi файла autumn-properties.json:
| Настройка | По умолчанию | Назначение |
|---|---|---|
Включено | false | Включает генерацию спецификации и Swagger UI. |
БазовыйПуть | /swagger-ui.html | Путь, по которому отдается страница Swagger UI. |
ПутьСпецификации | /v3/api-docs | Канонический путь спецификации: по нему отдается JSON, по нему же плюс .yaml — YAML. |
Заголовок | winow API | Заголовок API (info.title). |
Версия | 1.0.0 | Версия API (info.version). |
Описание | "" | Описание API (info.description). |
Сводка | "" | Краткая сводка об API в одну строку (info.summary). |
УсловияИспользования | "" | Адрес страницы с условиями использования (info.termsOfService). |
Контакт.Имя | "" | Имя ответственного за API (info.contact.name). |
Контакт.Адрес | "" | Адрес страницы поддержки (info.contact.url). |
Контакт.Почта | "" | Электронная почта поддержки (info.contact.email). |
Лицензия.Имя | "" | Название лицензии (info.license.name). |
Лицензия.Адрес | "" | Адрес текста лицензии (info.license.url). |
Лицензия.Идентификатор | "" | Выражение SPDX, определяющее лицензию (info.license.identifier). |
РесурсыSwaggerUI | CDN jsdelivr | Базовый URL, откуда страница берет js/css Swagger UI. |
АдресСервера | из winow.ИмяХоста и winow.Порт | Базовый адрес API в разделе servers. |
Раздел servers — это адрес, к которому Swagger UI приписывает пути при выполнении запроса по кнопке «Try it out». Адрес выбирается по убыванию достоверности:
- настройка
АдресСервера, если задана; - заголовки запроса за спецификацией —
X-Forwarded-Proto,X-Forwarded-Host,X-Forwarded-Prefix, иначеHost; winow.ИмяХостаиwinow.Порт.
За обратным прокси это работает без настройки: nginx проставляет X-Forwarded-* штатно, и приложение попадает в спецификацию под тем адресом, по которому доступно снаружи, вместе с https и префиксом пути. Настройка нужна для случаев, которые из заголовков не вывести.
Блоки contact и license попадают в документ, только если заданы. Лицензия требует названия: по спецификации license.name обязателен, поэтому один только Лицензия.Адрес лицензией не считается и в документ не идёт.
Лицензия.Идентификатор и Лицензия.Адрес по спецификации взаимоисключающи, поэтому при заданном идентификаторе адрес в документ не попадает.
По умолчанию страница Swagger UI подгружает свои ресурсы с публичного CDN. Если нужна работа офлайн, скачайте пакет swagger-ui-dist, положите его файлы в каталог статики (см. winow.КаталогиСФайлами) и укажите путь к ним в настройке РесурсыSwaggerUI:
{
"winow": {
"КаталогиСФайлами": {
"/swagger-assets": "./app/swagger-ui-dist"
},
"openapi": {
"Включено": true,
"РесурсыSwaggerUI": "/swagger-assets"
}
}
}Использование cli
winow предоставляет интерфейс командной строки. Запуск приложения становится еще проще. Для этого нужно установить пакет winow-cli.
Контейнеризация
Приложение на winow можно, конечно, запустить в контейнере.
Вот небольшой пример, как это сделать.
Нужно в один каталог положить:
- Само приложение app, которое содержит скрипт запуска, контролы, файлы и картинки,
autumn-properties.jsonсо всеми настройками и т.д. - Dockerfile для того, чтобы собрать образ, и прокинуть в него все файлы.
- Скрипт запуска сервера docker-entrypoint.sh.
- И все это дело удобно собирать одним скриптом start.sh.
