Skip to content

Минималистичный веб-сервер на нативном OneScript

Notifications You must be signed in to change notification settings

autumn-library/winow

Repository files navigation

WINOW is not OneScript.web

Минималистичный веб-сервер, построен на нативном TCPСервер, и работает на желудях.

Зачем это нужно, когда есть OneScript.Web, -CGI и т.д.? Отвечаю - для того, чтобы все было на чистом OneScript! И потому, что могу. С полным контролем, от входа двоичных данных на порт, до определения маршрута, получения данных, генерации ответа по шаблону и отправкой обратно клиенту.

Установка

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)

Ограничения ?

Да! Нет никаких обещаний на тему больших нагрузок. И нет поддержки https, погружаться в историю с шифрованием трафика, я еще не готов.

Как, из чего, зависимости ?

Библиотека разработана с использованием фреймворка для инверсии зависимостей - https://github.com/autumn-library/autumn. Для более эффективной работы с winow, следует ознакомиться. А так же обязательно пройти по ссылке и поставить звездочку, без этого ничего работать не будет.

Хеллоу ворлд !

От слов - к делу. Чтобы понять, как это все работает, давайте сделаем hello-world приложение, которое будет запускаться на localhost:3333 и отвечать простым текстом hello-world.

Первым делом, нам нужна точка входа, которая запустит приложение.

Создадим такой файл:

ПриветМир.os
#Использовать autumn
#Использовать winow

Поделка = Новый Поделка;
Поделка.ЗапуститьПриложение();

Теперь можете запустить файл ПриветМир.os и ничего не будет работать. И причин для этого ровно две. Первая - вы не сходили https://github.com/autumn-library/autumn и не поставили звезду. Вторая - мы не создали каталог, с классами, которые обрабатывают логику запросов. Я верю, что вы успели сходить и поставить звезду! Перейдем к созданию логики.

Создаем каталог и файл:

app/КонтролПриветствия.os
&Контроллер("/")
Процедура ПриСозданииОбъекта()

КонецПроцедуры

&ТочкаМаршрута("/")
Процедура Приветствие(Ответ) Экспорт

	Ответ.УстановитьТипКонтента("html");

	Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
                                |<div> %1 </div>",  "Привет новый дивный мир !");

КонецПроцедуры

И снова пробуем запустить ПриветМир.os, и идем в http://localhost:3333/

И чудо свершилось:

hw1

Передача параметров в строке запроса.

После продолжительного восторга, двигаемся дальше. На новом примере разберем по частям, как это работает.

Давайте сделаем еще один контроллер, еще более интерактивный. Сделаем так, что приложение будет нас встречать по имени. Имя мы хотим передавать в параметрах строки запроса.

http://localhost:3333/greeter/getparams?name=Nikita&familia=ivanchenko

Где greeter путь до нашего контроллера. И getparams точка входа для метода, который обрабатывает запрос. Все что после ? именные параметры.

Поехали, создаем файл:

app/ИнтерактивныйКонтролПриветствия.os
&Контроллер("/greeter")
Процедура ПриСозданииОбъекта()

КонецПроцедуры

&ТочкаМаршрута("getparams")
Процедура Приветствие(Запрос, Ответ) Экспорт

	Ответ.УстановитьТипКонтента("html");

	Имя = Запрос.ПараметрыИменные["name"];
	Фамилия = Запрос.ПараметрыИменные["familia"];

	Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
								|<div> Имя: %1 </div>
								|<div> Фамилия: %2 </div>",  Имя, Фамилия);

КонецПроцедуры

Опять запускаем ПриветМир.os, и идем теперь вот так http://localhost:3333/greeter/getparams?name=Никита&familia=Иванченко

hw2

Снова полный успех! Но давайте подробней остановимся на каждом этапе этого чуда.

Допустимо передавать именные параметры по имени в метод точки маршрута

&ТочкаМаршрута("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 и тд).

  • ПолныйПутьЗапроса - Полный путь запроса.

  • ПутьЗапроса - Путь без параметров.

  • ПараметрыЗапросаИменные - Соответствие, с именными параметрами.

  • ПараметрыЗапросаПорядковые - Массив с запросами.

  • ДатаПолученияЗапроса - Время получения запроса.

  • ДвоичныеДанныеЗапроса - Двоичные данные всего запроса.

  • КукиЗапроса - Куки.

Еще один способ передачи параметров в строке запроса.

Предыдущий пример показал, как можно передать параметры в строке запроса, при этом параметры имели имена. Теперь рассмотрим пример, когда параметры упорядоченные.

Давайте сделаем наконец калькулятор! И будет он работать вот так:

http://localhost:3333/greeter/calc/<operation>/<first>/<second>/

Где calc - точка маршрута. operation - вид операции, будем поддерживать minus и plus. и следом два слагаемых нашего уравнения.

Добавим в наш

app/ИнтерактивныйКонтролПриветствия.os

новую точку маршрута:

&ТочкаМаршрута("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

И в ответ перед нами будет красоваться

hw3

На самом деле, тут все очень просто. Когда мы объявляем точку маршрута &ТочкаМаршрута("calc"), все что дальше в пути через / будет любезно складываться в массив ПараметрыЗапросаПорядковые. А что делать с массивами, вы и без меня знаете.

Шаблоны параметров в строке запроса

Так же есть возможность задавать шаблон строки маршрута, где можно задавать именные параметры.

Например:

&ТочкаМаршрута("calc/{Число1}/multiply/{Число2}")
Процедура ШаблонныеПараметрыУмножение(Ответ, Число1, Число2) Экспорт
	Ответ.УстановитьТипКонтента("txt");
	Ответ.ТелоТекст = Число(Число1) * Число(Число2);
КонецПроцедуры

В точке маршрута фигурными скобками указываем параметры Число1 и Число2, и эти параметры будут переданы в метод обработчик во время выполнения запроса.

Входящие POST запросы

С пост запросами, все почти так же просто. Запрос Имеет два поля Тело и ТелоДвоичныеДанные, т.к. пользователь может закинуть нам как текст, так и картинку например.

Давайте потренируемся в обработке таких запросов, усовершенствуем наше приложение и научим его возводить в степень переданное число.

Вводить число мы будем по адресу http://localhost:3333/greeter/inputstepen, где будет форма ввода числа, и кнопка расчета. После расчета мы будем перенаправлены на http://localhost:3333/greeter/resultstepen. Форма будет передавать параметры методом POST.

Для реализации этой задумки добавим в наш

app/ИнтерактивныйКонтролПриветствия.os

этот код, с двумя новыми точками маршрута

&ТочкаМаршрута("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>",  Решение);

КонецПроцедуры

hw4

hw5

Теперь разберемся, что тут произошло. Не буду останавливаться на описании HTML тегов, для этого в интернете сайтов больше, чем звезд на небе.

Точка маршрута inputstepen показала нам форму, которая при расчете перенаправляет нас на resultstepen и в теле запроса передает параметры формы, которые имеют вид chislo=2&stepen=3. Все что нам осталось, это обработать запрос.

Мы можем парсить самостоятельно, но можно внедрить объект Парсеры, который умеет парсить параметры в таком формате Парсеры.ПараметрыИзТекста(<СтрокаСПараметрами>). Этот метод вернет соответствие со значениями, которые в последствии нужно правильно использовать.

Если во входящем запросе придет заголовок "Content-Type:application/json" тогда его тело автоматом будет распарсено в структуру ТелоЗапросОбъект с которой можно работать.

&ТочкаМаршрута("postjsonbody")
Процедура ПроверкаПостЗапросаКакОбъект(Ответ, ТелоЗапросОбъект, ЗаголовкиЗапроса) Экспорт
	Ответ.УстановитьТипКонтента("txt");
	Ответ.ТелоТекст = СтрШаблон("%1 %2", ТелоЗапросОбъект.Имя, ТелоЗапросОбъект.Фамилия);
КонецПроцедуры

Если во входящем запросе придет заголовок "Content-Type:application/x-www-form-urlencoded" тогда его тело автоматом будет распарсено по именам и значениям, которые будет принимать метод точки маршрута

&ТочкаМаршрута("postformbody")
Процедура ПроверкаПостЗапросаКакФорма(Ответ, Имя, Фамилия) Экспорт
	Ответ.УстановитьТипКонтента("txt");
	Ответ.ТелоТекст = СтрШаблон("%1 %2", Имя, Фамилия);
КонецПроцедуры

Работа с куками

Куки, это возможность сохранить на клиенте, в браузере, какую-либо информацию.

Объекты Запрос и Ответ, которые мы получаем в метод, который мы помечаем как ТочкаМаршрута. Оба этих объекта имеют свойство Куки. Соответственно во входящем запросе их можно читать, а в ответе устанавливать.

Модернизируем файл

app/ИнтерактивныйКонтролПриветствия.os
&ТочкаМаршрута("setcookie")
Процедура УстановитьКуку(Ответ) Экспорт

	Ответ.УстановитьТипКонтента("html");

	ИмяКуки = "ДатаПоследнегоВхода";
	ЗначениеКуки = ТекущаяДата();

	НоваяКука = Ответ.Куки.Добавить(ИмяКуки, ЗначениеКуки);

	Ответ.ТелоТекст = "<!DOCTYPE html>
						|<div> Кука установлена </div>";

КонецПроцедуры

&ТочкаМаршрута("readcookie")
Процедура ПрочитатьКуку(Запрос, Ответ) Экспорт

	Ответ.УстановитьТипКонтента("html");

	ИмяКуки = "ДатаПоследнегоВхода";

	ЗначениеКуки = Запрос.Куки.ПолучитьЗначениеПоИмени(ИмяКуки);

	Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
								|<div> Кука: %1 </div>", ЗначениеКуки);

КонецПроцедуры

hw6

hw7

Еще раз, не забываем, что куки хранятся на стороне браузера.

Хранение данных сессии

Еще один параметр точки маршрута Сессия имеет поле Данные. По сути, это соответствие, в которое можно записывать и читать любые значения. Эти данные хранятся на сервере, пока он работает. При остановке, данные сессии пропадают. При необходимости, в рамках приложения, можно дописать хранение данных сессии в файлах, базах данных и т.д.

Еще один пример

app/ИнтерактивныйКонтролПриветствия.os
&ТочкаМаршрута("setsessiondata")
Процедура УстановитьДанныеСессии(Ответ, Сессия) Экспорт

	Ответ.УстановитьТипКонтента("html");

	ИмяПараметраСессии = "ДатаПоследнегоВхода";
	ЗначениеПараметраСессии  = ТекущаяДата();

	
	Сессия.Данные[ИмяПараметраСессии] = ЗначениеПараметраСессии;

	Ответ.ТелоТекст = "<!DOCTYPE html>
						|<div> Данные сессии установлены </div>";

КонецПроцедуры

&ТочкаМаршрута("readsessiondata")
Процедура ПрочитатьДанныеСессии(Ответ, Сессия) Экспорт

	Ответ.УстановитьТипКонтента("html");

	ИмяПараметраСессии = "ДатаПоследнегоВхода";
	ЗначениеПараметраСессии  = Сессия.Данные[ИмяПараметраСессии];

	Ответ.ТелоТекст = СтрШаблон("<!DOCTYPE html>
								|<div> Значение параметра сессии: %1 </div>", ЗначениеПараметраСессии);

КонецПроцедуры

hw8

hw9

Публикация статичных файлов.

Часто нужно открывать доступ для скачивания всевозможных файлов. Таких как картинки, js-скрипты, css и т.д.

Для этого нужно сконфигурировать сервер, указав в файле autumn-properties.json нужные параметры. Этот файл нужно положить рядом с ПриветМир.os

autumn-properties.json
{ "winow":
  {
    "КаталогиСФайлами": {
      "/images": "./app/files"
   }
  }
}

Про конфигурирование через этот файл расскажу ниже, а пока давайте попробуем сделать наше приложение повеселее и добавить картинок.

Добавим в каталог приложения пару картинок.

app/files/zl1.jpg
app/files/fun/zl2.jpg

hw10

Как видим, по заданному пути теперь доступны файлы из каталога, при чем с сохранением внутренней иерархии каталога файлов.

Стоит отметить, что доступны становятся не все файлы сразу, а только те, расширения которых описаны в соответствии

ОписанияТиповРасширений = Новый Соответствие();
ОписанияТиповРасширений.Вставить("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. Но у меня есть решение!

Сразу покажу пример, а потом разберем по строчкам. Давайте отобразим страницу, на которой выведем текущее время, совершенно псевдослучайное число, динамически выведем случайное количество строк, и попробуем поиграться с условиями.

Поехали!

app/ИнтерактивныйКонтролПриветствия.os
&Контроллер("/demoviews")
Процедура ПриСозданииОбъекта()

КонецПроцедуры

&Отображение("./app/view/view1.html")
&ТочкаМаршрута("demo1")
Процедура ДемонстрацияОтображения(Ответ) Экспорт

	Ответ.УстановитьТипКонтента("html");

	ГСЧ = Новый ГенераторСлучайныхЧисел();
	
	СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 10);

	Массив = Новый Массив();

	Для Сч = 1 по СлучайноеЧисло Цикл
		Массив.Добавить(Строка(Новый УникальныйИдентификатор()));
	КонецЦикла;

	Модель = Новый Структура();
	Модель.Вставить("СлучайноеЧисло", СлучайноеЧисло);
	Модель.Вставить("МассивСтрок", Массив);

	Ответ.Модель = Модель;

КонецПроцедуры

Что тут нового? Во первых у точки маршрута появилась аннотация &Отображение("./app/view/view1.html"). А во вторых - определяется структура и устанавливается в Ответ.Модель. В этом весь секрет. После работы метода, на сцену выходит шаблонизатор, найдет указанный шаблон и разложит данные из модели, в соответствии с разметкой.

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С. Можно объявлять переменные, взаимодействовать с Модель, использовать управляющие блоки(Циклы, Условия)

hw11

Обработчики шаблонов.

Может быть так, что до или после рендера модели в шаблоне, нужно выполнить некие манипуляции с текстом шаблона. Для выполнения этой операции нужно зарегистрировать обработчики событий до рендера и после. Например:

<div>
@ТекстЗаменыДоРендера@
{{Модель}}
@ТекстЗаменыПослеРендера@
</div>

В этом шаблоне, мы хотим заменить вставки, неким текстом. Для этого добавим поделку два желудя.

&Желудь
&Прозвище("ПередОбработкойОтображения")
Процедура ПриСозданииОбъекта()
	
КонецПроцедуры

Процедура Преобразовать(ТекстШаблона) Экспорт
	ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыДоРендера@", "Шапка");
КонецПроцедуры
&Желудь
&Прозвище("ПослеОбработкиОтображения")
Процедура ПриСозданииОбъекта()
	
КонецПроцедуры

Процедура Преобразовать(ТекстШаблона) Экспорт
	ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыПослеРендера@", "Подвал");
КонецПроцедуры

Тут мы добавили желуди с Прозвище "ПередОбработкойОтображения" и "ПослеОбработкиОтображения". Таких желудей может быть несколько. Но у каждого такого желудя должна быть процедура с именем ``Преобразовать```, в которую будет передан текст шаблона.

Компоненты.

Писать шаблоны круто, но что может быть еще круче? Писать меньше шаблонов, и переиспользовать уже имеющиеся. Представим, что вам в разных местах нужно отображать одну и туже информацию, (таблицы, элементы меню, и т.д.). для решения этой задачи, шаблон имеет секретную функцию {{ ВывестиПоШаблону(<Путь до шаблона>, <Модель для шаблона>) }}

Давайте покажу, как это работает

app/ИнтерактивныйКонтролПриветствия.os
&Отображение("./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>

    {% КонецЕсли; %}

    <br>
    <div>Давай выведу строки из массива:</div>

    {{ ВывестиПоШаблону("./app/view/printarray.html", Модель.МассивСтрок) }}
    
    <br>
    <div>Давай выведу строки из второго массива:</div>

    {{ ВывестиПоШаблону("./app/view/printarray.html", Модель.ВторойМассив) }}

    <br>
    <div>Ранее я объявил переменные, теперь покажу их</div>
    <div>ОднаПеременная = {{ ОднаПеременная }}</div>
    <div>ВтораяПеременная = {{ ВтораяПеременная }}</div>

    </body>
    </html>
app/view/printarray.html
<div>
{% Для Каждого СтрокаИзМассива из Модель Цикл %}

    <div>Значение строки: {{ СтрокаИзМассива }}</div>

{% КонецЦикла; %}
</div>

hw12

Общее отображение контрола.

Для удобства разработки веб приложения хочется разделить отображения, и добавить что-то общее для всех точек маршрута. Например, общая 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);
Ответ.Модель.Вставить("ТекстСообщения", "Страница не найдена");
Ответ.Модель.Вставить("Запрос", Запрос);

А вот таким не замысловатым способом можно переопределить шаблон стандартной ошибки

app/КонтролПриветствия.os
&ФинальныйШтрих
Процедура ПостИнициализация() Экспорт
	ОбщийКонтейнер.МенеджерОтображений.УстановитьШаблон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, точного ответа не дам. Разработчик может самостоятельно придумать, как и где хранить группы и пароли. Я только покажу, как их подключить в наше приложение.

app/КонтролСУправлениемДоступом.os
&Пластилин
Перем МенеджерДоступа Экспорт;

&Контроллер("/sec")
Процедура ПриСозданииОбъекта()
	
КонецПроцедуры

&ФинальныйШтрих
Процедура ПроинициализироватьРоли() Экспорт 
	
	// Инициализация данных входа "пользователей"
	МенеджерДоступа.ДобавитьТокен("Админ", "123");	
	МенеджерДоступа.ДобавитьТокен("Пользователь", "111");

	// Назначение ролей "пользователям"
	МенеджерДоступа.ДобавитьРольЛогина("Админ", "Администраторы");
	МенеджерДоступа.ДобавитьРольЛогина("Админ", "Пользователи");

	МенеджерДоступа.ДобавитьРольЛогина("Пользователь", "Пользователи");
КонецПроцедуры

// Точка доступная роли пользователи
&Роли("Пользователи")
&ТочкаМаршрута("user")
Процедура Пользователь(Ответ) Экспорт
	Ответ.ТелоТекст = "Пользователи";
КонецПроцедуры

// Точка доступная роли Администраторы
&Роли("Администраторы")
&ТочкаМаршрута("admin")
Процедура Админ(Ответ) Экспорт
	Ответ.ТелоТекст = "Админка";
КонецПроцедуры

// обычная точка, доступная всем
&ТочкаМаршрута("free")
Процедура Все(Ответ) Экспорт
	Ответ.ТелоТекст = "Все подрят";
КонецПроцедуры

// Точка доступная ролям администраторы, пользователи.
&Роли("Администраторы, Пользователи")
&ТочкаМаршрута("usradm")
Процедура АдминыИПользователи(Ответ) Экспорт
	Ответ.ТелоТекст = "Админы и пользователи";
КонецПроцедуры

Работа с протоколом WebSocket

В winow в экспериментальном виде реализована поддержка веб-сокетов. Спека не полная, поддерживаются пока только текстовые сообщения. Разберем работу протокола на примере онлайн чата. Я не буду углубляться в часть фронта. Вот пример реализации на фронте. Основная суть такая - когда мы заходим на контрол /chat осуществляется проверка, залогинился пользователь или нет. Если нет, переадресуем на страницу ввода логина.

После ввода логина мы попадаем на страницу с чатом, где осуществляется подключение вебсокета, и начинается обмен сообщениями.

А теперь разберем пример того, что происходит на стороне сервера. Обработкой входящих сообщений занимается такой же контроллер с точками маршрута, к которым мы привыкли. Каждая точка маршрута является "топиком" в рамках которого общается одно соединение веб сокета. В примере ниже у нас контроллер по адресу chat и точкой маршрута message. У нас одно соединение, которое обменивается сообщениями в топике /chat/message. Точка маршрута по обработке сообщений может принимать несколько параметров:

  • Идентификатор - идентификатор сессии, в которой можно хранить разные данные.
  • Топик - имя топика, в рамках которого происходит общение
  • Сообщение - расшифрованное сообщение, которое пришло от клиента.

Для того, чтобы отправлять сообщения, нужно получить желудь БрокерСообщенийВебСокетов. Который умеет следующие действия с сообщениями:

  • ОтправитьСообщение(Топик, Сообщение, Идентификатор) - Отправляет определенному клиенту сообщение в указанный топик.
  • ОтправитьСообщениеВсем(Топик, Сообщение) - Отправляет сообщение всем клиентам, подключенным к указанному топику.
  • ОтправитьСообщениеСписку(Топик, Сообщение, СписокИдентификаторов) - Отправляет сообщение массиву клиентов, подписанных на указанный топик.
  • ОтправитьСообщениеВсемКроме(Топик, Сообщение, СписокИсключенийИдентификаторов) - Отправляет сообщение всем, клиентам подписанным на указанный топик, кроме массива, переданного как параметр.

Так же контроллер может иметь методы, помеченные аннотациями &ПриПодключенииВебСокета("/имя/топика") и ПриОтключенииВебСокета("/имя/топика"). Которые будут вызваны, после соответствующих событий, и принимать Идентификатор клиента, с которым произошло событие.

Вот полный пример с комментариями.

ВебСокетЧат.os
&Пластилин Перем БрокерСообщенийВебСокетов; // Инжектим желудь, который управляет отправкой сообщений

Перем КешИменПользователей;

&Контроллер("/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(Запись, ОбъектДляПарсинга);

	Возврат Запись.Закрыть();
КонецФункции

Вот результат наших трудов.

ws

Работа с механизмом server-sent events.

Подробно можно прочитать на вики.

Для реализации работы с механизмом нужно выполнить несколько шагов.

  1. Зарегистрировать топик, в конструкторе контроллера.
  2. Добавить обработчики событий, которые будут вызываться при подключении и отключении клиента. (опционально)
  3. Посылать сообщения клиенту в топики, при необходимости в соответствии с логикой приложения.

Рассмотрим на примере:

// Подключаем необходимые зависимости
&Пластилин Перем БрокерСообщенийСобытийСервера;
&пластилин Перем ФабрикаОтветов;

&Контроллер("/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

sse

Использование cli

winow предоставляет интерфейс командной строки. Запуск приложения становится еще проще. Для этого нужно установить пакет winow-cli.

Контейнеризация

Приложение на winow можно, конечно, запустить в контейнере.

Вот небольшой пример, как это сделать.

Нужно в один каталог положить:

  • Само приложение app, которое содержит скрипт запуска, контролы, файлы и картинки, autumn-properties.json со всеми настройками и т.д.
  • Dockerfile для того, чтобы собрать образ, и прокинуть в него все файлы.
  • Скрипт запуска сервера docker-entrypoint.sh.
  • И все это дело удобно собирать одним скриптом start.sh.