diff --git a/.vitepress/config.mts b/.vitepress/config.mts index eae9f72..7eefe0f 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -18,7 +18,8 @@ export default defineConfig({ 'script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-ZHG1MYKGV9');" - ] + ], + ['link', { rel: 'icon', href: '/docs/favicon.ico' }] ], appearance: 'dark', @@ -51,6 +52,10 @@ export default defineConfig({ }, + ignoreDeadLinks: [ + /^https?:\/\/localhost/ + ], + themeConfig: { // https://vitepress.dev/reference/default-theme-config @@ -67,7 +72,8 @@ export default defineConfig({ contentRoot: 'docs/', contentDirs: [ { text: 'Начало работы', dir: 'getting-started' }, - { text: 'Использование фреймворка', dir: 'framework-elements' } + { text: 'Использование фреймворка', dir: 'framework-elements' }, + { text: 'Winow', dir: 'winow' } ], collapsed: false, }), diff --git a/docs/public/acorn-ol.png b/docs/public/acorn-ol.png deleted file mode 100644 index 8415b66..0000000 Binary files a/docs/public/acorn-ol.png and /dev/null differ diff --git a/docs/static/acorn.png b/docs/static/acorn.png deleted file mode 100644 index e55e4a7..0000000 Binary files a/docs/static/acorn.png and /dev/null differ diff --git a/docs/static/telegram-white-icon.svg b/docs/static/telegram-white-icon.svg deleted file mode 100644 index 471c7fa..0000000 --- a/docs/static/telegram-white-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/static/winow/hw1.png b/docs/static/winow/hw1.png new file mode 100644 index 0000000..1272abb Binary files /dev/null and b/docs/static/winow/hw1.png differ diff --git a/docs/static/winow/hw10.png b/docs/static/winow/hw10.png new file mode 100644 index 0000000..093b7e1 Binary files /dev/null and b/docs/static/winow/hw10.png differ diff --git a/docs/static/winow/hw11.png b/docs/static/winow/hw11.png new file mode 100644 index 0000000..6fd9505 Binary files /dev/null and b/docs/static/winow/hw11.png differ diff --git a/docs/static/winow/hw12.png b/docs/static/winow/hw12.png new file mode 100644 index 0000000..42164ca Binary files /dev/null and b/docs/static/winow/hw12.png differ diff --git a/docs/static/winow/hw2.png b/docs/static/winow/hw2.png new file mode 100644 index 0000000..9438e32 Binary files /dev/null and b/docs/static/winow/hw2.png differ diff --git a/docs/static/winow/hw3.png b/docs/static/winow/hw3.png new file mode 100644 index 0000000..60959af Binary files /dev/null and b/docs/static/winow/hw3.png differ diff --git a/docs/static/winow/hw4.png b/docs/static/winow/hw4.png new file mode 100644 index 0000000..0a13686 Binary files /dev/null and b/docs/static/winow/hw4.png differ diff --git a/docs/static/winow/hw5.png b/docs/static/winow/hw5.png new file mode 100644 index 0000000..217ebcd Binary files /dev/null and b/docs/static/winow/hw5.png differ diff --git a/docs/static/winow/hw6.png b/docs/static/winow/hw6.png new file mode 100644 index 0000000..30de072 Binary files /dev/null and b/docs/static/winow/hw6.png differ diff --git a/docs/static/winow/hw7.png b/docs/static/winow/hw7.png new file mode 100644 index 0000000..f4ba94a Binary files /dev/null and b/docs/static/winow/hw7.png differ diff --git a/docs/static/winow/hw8.png b/docs/static/winow/hw8.png new file mode 100644 index 0000000..65bacba Binary files /dev/null and b/docs/static/winow/hw8.png differ diff --git a/docs/static/winow/hw9.png b/docs/static/winow/hw9.png new file mode 100644 index 0000000..01ccc16 Binary files /dev/null and b/docs/static/winow/hw9.png differ diff --git a/docs/static/winow/sse.gif b/docs/static/winow/sse.gif new file mode 100644 index 0000000..dfe7c20 Binary files /dev/null and b/docs/static/winow/sse.gif differ diff --git a/docs/static/winow/ws-chat.gif b/docs/static/winow/ws-chat.gif new file mode 100644 index 0000000..2e0e95b Binary files /dev/null and b/docs/static/winow/ws-chat.gif differ diff --git a/docs/winow/index.md b/docs/winow/index.md new file mode 100644 index 0000000..ec1f11c --- /dev/null +++ b/docs/winow/index.md @@ -0,0 +1,1299 @@ +# 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 +``` + +```bsl +#Использовать autumn +#Использовать winow + +Поделка = Новый Поделка; +Поделка.ЗапуститьПриложение(); +``` + +Теперь можете запустить файл ```ПриветМир.os``` и ничего не будет работать. И причин для этого ровно две. Первая - вы не сходили https://github.com/autumn-library/autumn и не поставили звезду. Вторая - мы не создали каталог, с классами, которые обрабатывают логику запросов. Я верю, что вы успели сходить и поставить звезду! Перейдем к созданию логики. + +Создаем каталог и файл: + +``` +app/КонтролПриветствия.os +``` + +```bsl +&Контроллер("/") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +&ТочкаМаршрута("/") +Процедура Приветствие(Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + Ответ.ТелоТекст = СтрШаблон(" + |
%1
", "Привет новый дивный мир !"); + +КонецПроцедуры +``` + +И снова пробуем запустить ```ПриветМир.os```, и идем в http://localhost:3333/ + +И чудо свершилось: + +![hw1](../static/winow/hw1.png) + +## Передача параметров в строке запроса. + +После продолжительного восторга, двигаемся дальше. На новом примере разберем по частям, как это работает. + +Давайте сделаем еще один контроллер, еще более интерактивный. Сделаем так, что приложение будет нас встречать по имени. Имя мы хотим передавать в параметрах строки запроса. + +http://localhost:3333/greeter/getparams?name=Nikita&familia=ivanchenko + +Где ```greeter``` путь до нашего контроллера. И ```getparams``` точка входа для метода, который обрабатывает запрос. Все что после ```?``` именные параметры. + +Поехали, создаем файл: + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +```bsl +&Контроллер("/greeter") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +&ТочкаМаршрута("getparams") +Процедура Приветствие(Запрос, Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + Имя = Запрос.ПараметрыИменные["name"]; + Фамилия = Запрос.ПараметрыИменные["familia"]; + + Ответ.ТелоТекст = СтрШаблон(" + |
Имя: %1
+ |
Фамилия: %2
", Имя, Фамилия); + +КонецПроцедуры +``` + +Опять запускаем ```ПриветМир.os```, и идем теперь вот так http://localhost:3333/greeter/getparams?name=Никита&familia=Иванченко + +![hw2](../static/winow/hw2.png) + +Снова полный успех! Но давайте подробней остановимся на каждом этапе этого чуда. + +Допустимо передавать именные параметры по имени в метод точки маршрута + +```bsl +&ТочкаМаршрута("getparamsbyname") +Процедура ПроверкаГетПараметровПоИмени(Ответ, ИмяКошки, ИмяСобаки) Экспорт + Ответ.УстановитьТипКонтента("txt"); + Ответ.ТелоТекст = СтрШаблон("%1 И %2", "Кошка=" + ИмяКошки, "Собака=" + ИмяСобаки); +КонецПроцедуры +``` + +Файл, который мы только что сделали, описывает определенную точку в адресной строке. При совпадении с которой перехватывается управление над входящим запросом. Посмотрим поближе. + +```bsl +&Контроллер("/greeter") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры +``` + +В начале идет конструктор нашего класса, ```ПриСозданииОбъекта()```. Весь код, который в нем написан, будет выполнен при создании. Удобно тут выполнять всякую инициализацию переменных. + +Этот метод имеет аннотацию ```&Контроллер("/greeter")``` как раз указывает, путь от корня, после которого будет осуществлен перехват. + +Стоит отметить что аннотация может быть более длинной, чтобы отвечать логике описания api. Например, ```&Контроллер("/app/api/v1/greeter")``` тоже рабочий вариант, только ходить нужно уже вот сюда http://localhost:3333/app/api/v1/greeter + +У любого контроллера может быть любое множество методов, которыми он обрабатывает входящий запрос. + +```bsl +&ТочкаМаршрута("getparams") +Процедура Приветствие(Запрос, Ответ) Экспорт +``` + +Для того, чтобы процедура контроллера могла понимать, что ее вызывают из запроса, ее нужно пометить аннотацией ```&ТочкаМаршрута("getparams")```. Где параметр аннотации указывает имя в пути, после которого ей нужно сработать. + +Так же, чтобы все получилось, процедура должна отвечать нескольким требованиям: + +- Быть экспортной +- Принимать на вход параметры, имена которых ограничены и предопределены. Назначение параметров мы разберем по ходу дела. + +```Запрос```, например, хранит всю информацию, которая пришла к нам от клиента. В том числе ```Запрос.ПараметрыИменные``` - соответствие, хранящее значения всех параметров, которые переданы после знака ```?``` + +Дальше мы лихо эти параметры читаем. + +```bsl +Имя = Запрос.ПараметрыИменные["name"]; +Фамилия = Запрос.ПараметрыИменные["familia"]; +``` + +Следующий параметр ```Ответ```, в котором собирается все, что будет отправлено обратно клиенту. Например, вот так: + +```bsl +Ответ.УстановитьТипКонтента("html"); +``` + +Устанавливается заголовок ```Content-Type```, благодаря которому браузер понимает, как отобразить то, что мы ему шлем. + +По умолчанию поддерживаются типы: + +```bsl +ОписанияТиповРасширений = Новый Соответствие(); +ОписанияТиповРасширений.Вставить("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"); +``` + +Ну и конечно же устанавливаем текст ответа, который вернется клиенту. + +```bsl +Ответ.ТелоТекст = СтрШаблон(" + |
Имя: %1
+ |
Фамилия: %2
", Имя, Фамилия); +``` + +Сейчас это не удобно и не красиво. Но к концу нашей беседы мы разберемся - как сделать красиво. + +## Описание возможных параметров ТочкиМаршрута + +* ```Запрос``` - Объект, содержащий все данные о входящем запросе + +* ```Ответ``` - Объект, содержащий все данные об ответе, который будет отправлен пользователю + +* ```Сессия``` - Объект, хранящий сессионные данные пользователя. Имеет поля ```Данные```, соответствие для хранения любых данных бизнес-логики и ```Логин```, строковое имя пользователя, после авторизации. + +Так же, для удобства, можно получать части объектов ```Сессия``` и ```Запрос```. + +* ```Логин``` - Логин, хранящейся в сессии. Аналог ```Сессия.Логин```. + +* ```ДанныеСессии``` - Соответствие с данными, хранящееся в сессии. Аналог ```Сессия.Данные```. + +* ```ТекстЗапроса``` - Полный текст входящего запроса. + +* ```ЗаголовкиЗапроса``` - Соответствие заголовков запроса. + +* ```ТелоЗапроса``` - Текст тела запроса. + +* ```ТелоЗапросаДвоичныеДанные``` - Тело запроса в виде двоичных данных. + +* ```МетодЗапроса``` - Метод (GET, POST, PUT и тд). + +* ```ПолныйПутьЗапроса``` - Полный путь запроса. + +* ```ПутьЗапроса``` - Путь без параметров. + +* ```ПараметрыЗапросаИменные``` - Соответствие, с именными параметрами. + +* ```ПараметрыЗапросаПорядковые``` - Массив с запросами. + +* ```ДатаПолученияЗапроса``` - Время получения запроса. + +* ```ДвоичныеДанныеЗапроса``` - Двоичные данные всего запроса. + +* ```КукиЗапроса``` - Куки. + +## Еще один способ передачи параметров в строке запроса. + +Предыдущий пример показал, как можно передать параметры в строке запроса, при этом параметры имели имена. Теперь рассмотрим пример, когда параметры упорядоченные. + +Давайте сделаем наконец калькулятор! И будет он работать вот так: + +``` +http://localhost:3333/greeter/calc//// +``` +Где ```calc``` - точка маршрута. ```operation``` - вид операции, будем поддерживать minus и plus. и следом два слагаемых нашего уравнения. + +Добавим в наш + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +новую точку маршрута: + +```bsl +&ТочкаМаршрута("calc") +Процедура Калькулятор(ПараметрыЗапросаПорядковые, Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + Если ПараметрыЗапросаПорядковые.Количество() <> 3 Тогда + + Решение = "Неверное число параметров"; + + ИначеЕсли Не (ПараметрыЗапросаПорядковые[0] = "minus" + И Не ПараметрыЗапросаПорядковые[0] = "plus") Тогда + Решение = "Операция не распознана"; + + Иначе + Попытка + Число1 = Число(ПараметрыЗапросаПорядковые[1]); + Число2 = Число(ПараметрыЗапросаПорядковые[2]); + + Если ПараметрыЗапросаПорядковые[0] = "minus" Тогда + Решение = Число1 - Число2; + Иначе + Решение = Число1 + Число2; + КонецЕсли + + Исключение + Решение = "Ошибка конвертации в число" + КонецПопытки; + КонецЕсли; + + Ответ.ТелоТекст = СтрШаблон(" + |
Ответ: %1
", Решение); + +КонецПроцедуры +``` + +Перезапустим приложение, и перейдем по ссылке http://localhost:3333/greeter/calc/plus/3/2 + +И в ответ перед нами будет красоваться + +![hw3](../static/winow/hw3.png) + +На самом деле, тут все очень просто. Когда мы объявляем точку маршрута ```&ТочкаМаршрута("calc")```, все что дальше в пути через ```/``` будет любезно складываться в массив ```ПараметрыЗапросаПорядковые```. А что делать с массивами, вы и без меня знаете. + +## Шаблоны параметров в строке запроса + +Так же есть возможность задавать шаблон строки маршрута, где можно задавать именные параметры. + +Например: + +```bsl +&ТочкаМаршрута("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 +``` + +этот код, с двумя новыми точками маршрута + +```bsl +&ТочкаМаршрута("inputstepen") +Процедура ВводСтепени(Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + Ответ.ТелоТекст = + "
+ |
+ |
+ |
+ |

+ | + |
"; + +КонецПроцедуры + +&ТочкаМаршрута("resultstepen") +Процедура ВозводительВСтепень(Запрос, Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ПостПараметры = Парсеры.ПараметрыИзТекста(Запрос.Тело); + + Попытка + Решение = Pow(ПостПараметры["chislo"], ПостПараметры["stepen"]); + Исключение + Решение = "Ошибка при расчетах " + ОписаниеОшибки(); + КонецПопытки; + + Ответ.ТелоТекст = СтрШаблон(" + |
Ответ: %1
", Решение); + +КонецПроцедуры +``` + +![hw4](../static/winow/hw4.png) + +![hw5](../static/winow/hw5.png) + +Теперь разберемся, что тут произошло. Не буду останавливаться на описании HTML тегов, для этого в интернете сайтов больше, чем звезд на небе. + +Точка маршрута ```inputstepen``` показала нам форму, которая при расчете перенаправляет нас на ```resultstepen``` и в теле запроса передает параметры формы, которые имеют вид ```chislo=2&stepen=3```. Все что нам осталось, это обработать запрос. + +Мы можем парсить самостоятельно, но можно внедрить объект ```Парсеры```, который умеет парсить параметры в таком формате ```Парсеры.ПараметрыИзТекста(<СтрокаСПараметрами>)```. Этот метод вернет соответствие со значениями, которые в последствии нужно правильно использовать. + +Если во входящем запросе придет заголовок ```"Content-Type:application/json"``` тогда его тело автоматом будет распарсено в структуру ```ТелоЗапросОбъект``` с которой можно работать. + +```bsl +&ТочкаМаршрута("postjsonbody") +Процедура ПроверкаПостЗапросаКакОбъект(Ответ, ТелоЗапросОбъект, ЗаголовкиЗапроса) Экспорт + Ответ.УстановитьТипКонтента("txt"); + Ответ.ТелоТекст = СтрШаблон("%1 %2", ТелоЗапросОбъект.Имя, ТелоЗапросОбъект.Фамилия); +КонецПроцедуры +``` + +Если во входящем запросе придет заголовок ```"Content-Type:application/x-www-form-urlencoded"``` тогда его тело автоматом будет распарсено по именам и значениям, которые будет принимать метод точки маршрута + +```bsl +&ТочкаМаршрута("postformbody") +Процедура ПроверкаПостЗапросаКакФорма(Ответ, Имя, Фамилия) Экспорт + Ответ.УстановитьТипКонтента("txt"); + Ответ.ТелоТекст = СтрШаблон("%1 %2", Имя, Фамилия); +КонецПроцедуры +``` + +## Работа с куками + +Куки, это возможность сохранить на клиенте, в браузере, какую-либо информацию. + +Объекты ```Запрос``` и ```Ответ```, которые мы получаем в метод, который мы помечаем как ```ТочкаМаршрута```. Оба этих объекта имеют свойство ```Куки```. Соответственно во входящем запросе их можно читать, а в ответе устанавливать. + +Модернизируем файл + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +```bsl +&ТочкаМаршрута("setcookie") +Процедура УстановитьКуку(Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ИмяКуки = "ДатаПоследнегоВхода"; + ЗначениеКуки = ТекущаяДата(); + + НоваяКука = Ответ.Куки.Добавить(ИмяКуки, ЗначениеКуки); + + Ответ.ТелоТекст = " + |
Кука установлена
"; + +КонецПроцедуры + +&ТочкаМаршрута("readcookie") +Процедура ПрочитатьКуку(Запрос, Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ИмяКуки = "ДатаПоследнегоВхода"; + + ЗначениеКуки = Запрос.Куки.ПолучитьЗначениеПоИмени(ИмяКуки); + + Ответ.ТелоТекст = СтрШаблон(" + |
Кука: %1
", ЗначениеКуки); + +КонецПроцедуры +``` + +![hw6](../static/winow/hw6.png) + +![hw7](../static/winow/hw7.png) + +Еще раз, не забываем, что куки хранятся на стороне браузера. + +## Хранение данных сессии + +Еще один параметр точки маршрута ```Сессия``` имеет поле ```Данные```. По сути, это соответствие, в которое можно записывать и читать любые значения. Эти данные хранятся на сервере, пока он работает. При остановке, данные сессии пропадают. При необходимости, в рамках приложения, можно дописать хранение данных сессии в файлах, базах данных и т.д. + +Еще один пример + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +```bsl +&ТочкаМаршрута("setsessiondata") +Процедура УстановитьДанныеСессии(Ответ, Сессия) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ИмяПараметраСессии = "ДатаПоследнегоВхода"; + ЗначениеПараметраСессии = ТекущаяДата(); + + + Сессия.Данные[ИмяПараметраСессии] = ЗначениеПараметраСессии; + + Ответ.ТелоТекст = " + |
Данные сессии установлены
"; + +КонецПроцедуры + +&ТочкаМаршрута("readsessiondata") +Процедура ПрочитатьДанныеСессии(Ответ, Сессия) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ИмяПараметраСессии = "ДатаПоследнегоВхода"; + ЗначениеПараметраСессии = Сессия.Данные[ИмяПараметраСессии]; + + Ответ.ТелоТекст = СтрШаблон(" + |
Значение параметра сессии: %1
", ЗначениеПараметраСессии); + +КонецПроцедуры +``` + +![hw8](../static/winow/hw8.png) + +![hw9](../static/winow/hw9.png) + +## Публикация статичных файлов. + +Часто нужно открывать доступ для скачивания всевозможных файлов. Таких как картинки, js-скрипты, css и т.д. + +Для этого нужно сконфигурировать сервер, указав в файле ```autumn-properties.json``` нужные параметры. Этот файл нужно положить рядом с ```ПриветМир.os``` + +``` +autumn-properties.json +``` +```JSON +{ "winow": + { + "КаталогиСФайлами": { + "/images": "./app/files" + } + } +} +``` + +Про конфигурирование через этот файл расскажу ниже, а пока давайте попробуем сделать наше приложение повеселее и добавить картинок. + +Добавим в каталог приложения пару картинок. + +``` +app/files/zl1.jpg +app/files/fun/zl2.jpg +``` + +![hw10](../static/winow/hw10.png) + +Как видим, по заданному пути теперь доступны файлы из каталога, при чем с сохранением внутренней иерархии каталога файлов. + +Стоит отметить, что доступны становятся не все файлы сразу, а только те, расширения которых описаны в соответствии + +```bsl +ОписанияТиповРасширений = Новый Соответствие(); +ОписанияТиповРасширений.Вставить("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? И правда, писать код так: + +```bsl +Ответ.ТелоТекст = СтрШаблон(" + |
Имя: %1
+ |
Фамилия: %2
", Имя, Фамилия); +``` + +просто не удобно, и мало приличных слов для такого подхода можно подобрать, и ни в одном не будет буквы V. Но у меня есть решение! + +Сразу покажу пример, а потом разберем по строчкам. Давайте отобразим страницу, на которой выведем текущее время, совершенно псевдослучайное число, динамически выведем случайное количество строк, и попробуем поиграться с условиями. + +Поехали! + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +```bsl +&Контроллер("/demoviews") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +&Отображение("./app/view/view1.html") +&ТочкаМаршрута("demo1") +Процедура ДемонстрацияОтображения(Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ГСЧ = Новый ГенераторСлучайныхЧисел(); + + СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 10); + + Массив = Новый Массив(); + + Для Сч = 1 по СлучайноеЧисло Цикл + Массив.Добавить(Строка(Новый УникальныйИдентификатор())); + КонецЦикла; + + Модель = Новый Структура(); + Модель.Вставить("СлучайноеЧисло", СлучайноеЧисло); + Модель.Вставить("МассивСтрок", Массив); + + Ответ.Модель = Модель; + +КонецПроцедуры +``` + +Что тут нового? Во первых у точки маршрута появилась аннотация ```&Отображение("./app/view/view1.html")```. А во вторых - определяется структура и устанавливается в ```Ответ.Модель```. В этом весь секрет. После работы метода, на сцену выходит шаблонизатор, найдет указанный шаблон и разложит данные из модели, в соответствии с разметкой. + +``` +app/view/view1.html +``` + + + +```html + + + + Демонстрация работы отображений + + + +
Привет! Это отображение из шаблона. Точное время {{ ТекущаяДата() }} +
+
Ты это не увидишь, но тут объявляются переменные
+ {% + ОднаПеременная = 1; + ВтораяПеременная = "Секрет"; + %} +
+
Вот твое случайное число {{ Модель.СлучайноеЧисло }}
+ + {% Если Модель.СлучайноеЧисло > 5 Тогда %} + +
Случайное число БОЛЬШЕ пяти
+ + {% Иначе %} + +
Случайное число НЕ больше пяти
+ + {% КонецЕсли; %} + +
Давай выведу строки из массива:
+ + {% Для Каждого СтрокаИзМассива из Модель.МассивСтрок Цикл %} + +
Значение строки: {{ СтрокаИзМассива }} и оно достаточно случайно
+ + {% КонецЦикла; %} + +
Ранее я объявил переменные, теперь покажу их
+
ОднаПеременная = {{ ОднаПеременная }}
+
ВтораяПеременная = {{ ВтораяПеременная }}
+ + + +``` + + + +Если присмотреться, то шаблон это просто HTML разметка, которую смешали с 1сным кодом. Вот это коктейль получился! + +Основные принципы разметки: +::: v-pre +Выражения - обозначаются тегами. ```{{ <Выражение> }}```. Тут может быть: + +- Любое выражение на 1С, которое возвращает значение ```{{ 1 + 3 }}``` +- Переменная ```{{ Модель.ЛюбоеЗначение }}``` +- Функция ```{{ Макс(1,5,9,7) }}``` + + + +Операторы - обозначаются тегами. ```{% <КодНа1С> %}```. + + +::: +Это полноценный код на 1С. Можно объявлять переменные, взаимодействовать с ```Модель```, использовать управляющие блоки(Циклы, Условия) + +![hw11](../static/winow/hw11.png) + +## Обработчики шаблонов. + +Может быть так, что до или после рендера модели в шаблоне, нужно выполнить некие манипуляции с текстом шаблона. Для выполнения этой операции нужно зарегистрировать обработчики событий до рендера и после. Например: + + +::: v-pre +```html +
+@ТекстЗаменыДоРендера@ +{{Модель}} +@ТекстЗаменыПослеРендера@ +
+``` +::: + + +В этом шаблоне, мы хотим заменить вставки, неким текстом. Для этого добавим поделку два желудя. + +```bsl +&Желудь +&Прозвище("ПередОбработкойОтображения") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +Процедура Преобразовать(ТекстШаблона) Экспорт + ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыДоРендера@", "Шапка"); +КонецПроцедуры +``` + +```bsl +&Желудь +&Прозвище("ПослеОбработкиОтображения") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +Процедура Преобразовать(ТекстШаблона) Экспорт + ТекстШаблона = СтрЗаменить(ТекстШаблона, "@ТекстЗаменыПослеРендера@", "Подвал"); +КонецПроцедуры +``` + +Тут мы добавили желуди с ```Прозвище``` "ПередОбработкойОтображения" и "ПослеОбработкиОтображения". Таких желудей может быть несколько. Но у каждого такого желудя должна быть процедура с именем ``Преобразовать```, в которую будет передан текст шаблона. + +## Компоненты. + + +::: v-pre +Писать шаблоны круто, но что может быть еще круче? Писать меньше шаблонов, и переиспользовать уже имеющиеся. Представим, что вам в разных местах нужно отображать одну и туже информацию, (таблицы, элементы меню, и т.д.). для решения этой задачи, шаблон имеет секретную функцию ```{{ ВывестиПоШаблону(<Путь до шаблона>, <Модель для шаблона>) }}``` +::: + + +Давайте покажу, как это работает + +``` +app/ИнтерактивныйКонтролПриветствия.os +``` + +```bsl +&Отображение("./app/view/view1.html") +&ТочкаМаршрута("demo1") +Процедура ДемонстрацияОтображения(Ответ) Экспорт + + Ответ.УстановитьТипКонтента("html"); + + ГСЧ = Новый ГенераторСлучайныхЧисел(); + + СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 10); + + Массив = Новый Массив(); + + Для Сч = 1 по СлучайноеЧисло Цикл + Массив.Добавить(Строка(Новый УникальныйИдентификатор())); + КонецЦикла; + + Модель = Новый Структура(); + Модель.Вставить("СлучайноеЧисло", СлучайноеЧисло); + Модель.Вставить("МассивСтрок", Массив); + + // добавим в модель второй массив + МассивФруктов = Новый Массив(); + МассивФруктов.Добавить("Яблоко"); + МассивФруктов.Добавить("Апельсин"); + МассивФруктов.Добавить("Банан"); + МассивФруктов.Добавить("Желудь"); + + Модель.Вставить("ВторойМассив", МассивФруктов); + + Ответ.Модель = Модель; + +КонецПроцедуры +``` + +Шаблоны: + +``` +app/view/view1.html +``` + + + +```html + + + + Демонстрация работы отображений + + + + +
Привет! Это отображение из шаблона. Точное время {{ ТекущаяДата() }} +
+
Ты это не увидишь, но тут объявляются переменные
+ {% + ОднаПеременная = 1; + ВтораяПеременная = "Секрет"; + %} +
+
Вот твое случайное число {{ Модель.СлучайноеЧисло }}
+ + {% Если Модель.СлучайноеЧисло > 5 Тогда %} + +
Случайное число БОЛЬШЕ пяти
+ + {% Иначе %} + +
Случайное число НЕ больше пяти
+ + {% КонецЕсли; %} + +
+
Давай выведу строки из массива:
+ + {{ ВывестиПоШаблону("./app/view/printarray.html", Модель.МассивСтрок) }} + +
+
Давай выведу строки из второго массива:
+ + {{ ВывестиПоШаблону("./app/view/printarray.html", Модель.ВторойМассив) }} + +
+
Ранее я объявил переменные, теперь покажу их
+
ОднаПеременная = {{ ОднаПеременная }}
+
ВтораяПеременная = {{ ВтораяПеременная }}
+ + + +``` + + + +``` +app/view/printarray.html +``` + + + +```html +
+{% Для Каждого СтрокаИзМассива из Модель Цикл %} + +
Значение строки: {{ СтрокаИзМассива }}
+ +{% КонецЦикла; %} +
+``` + + + +![hw12](../static/winow/hw12.png) + +## Общее отображение контрола. + +Для удобства разработки веб приложения хочется разделить отображения, и добавить что-то общее для всех точек маршрута. Например, общая html разметка, с заголовками, меню, подвалом и тд. Для этих целей есть возможность с помощью аннотации в конструкторе контрола указать общий шаблон. + +```bsl +&Контроллер("/demoviews") +&Отображение(Шаблон = "./hwapp/view/main.html", Метод = "ПолучитьМодельКонтрола") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +Функция ПолучитьМодельКонтрола(Запрос) Экспорт + Модель = Новый Структура("Заголовок, Дата", "Демонстрация работы отображений", Запрос.ДатаПолучения); + + Возврат Модель; +КонецФункции +``` + +Где ```&Отображение(Шаблон = "./hwapp/view/main.html", Метод = "ПолучитьМодельКонтрола")``` аннотация, указывает где расположен шаблон, и каким методом для него формируется модель с данными. Параметры этого метода так же могут быть выбраны, аналогично методам точек маршрута. + +А вот так выглядит общий шаблон + + + +```html + + + +{{Модель.Заголовок}} + + +
Шапка страницы! Дата получения запроса: {{Модель.Дата}}
+ +@Контент + +
Подвал страницы
+ + +``` + + + +Где тег ```@Контент``` будет заменен результатом ответа точки маршрута. + +Однако бывают ситуации, когда у контроллера есть отображение, но какая точка маршрута должна возвращать ответ, без его применения. В такой ситуации, для метода точки маршрута нужно добавить аннотацию ```&НеВыводитОтображениеКонтроллера```. Например, вот так: + +```bsl +&Отображение("./app/view/view1.html") +&ТочкаМаршрута("demo2") +&НеВыводитОтображениеКонтроллера +Процедура ДемонстрацияОтображенияБезОбщегоОтображения(Ответ) Экспорт + ... +КонецПроцедуры +``` + + +## Ответы по ошибкам. + +Обрабатывая входящие запросы, могут случиться исключения. Ну кто с первого раза напишет правильно код? Сервер при исключении вернет страницу с кодом 500. Шаблон этой страницы можно переопределить. Моделью там будет структура + +```bsl +Ответ.Модель = Новый Структура(); +Ответ.Модель.Вставить("КодСостояния", 500); +Ответ.Модель.Вставить("ТекстСообщения", ТекстОшибки); +Ответ.Модель.Вставить("Запрос", Запрос); +``` + +Ну а какой пользователь с первого раза введет без ошибки адрес ресурса? Сервер вернет ему 404. Шаблон этой ошибки так же можно переопределить. Модель там следующая + +```bsl +Ответ.Модель = Новый Структура(); +Ответ.Модель.Вставить("КодСостояния", 404); +Ответ.Модель.Вставить("ТекстСообщения", "Страница не найдена"); +Ответ.Модель.Вставить("Запрос", Запрос); +``` + +А вот таким не замысловатым способом можно переопределить шаблон стандартной ошибки + +``` +app/КонтролПриветствия.os +``` + + + +```bsl +&ФинальныйШтрих +Процедура ПостИнициализация() Экспорт + ОбщийКонтейнер.МенеджерОтображений.УстановитьШаблон404(" + |

{{ Модель.КодСостояния }}

+ |
{{ Модель.ТекстСообщения }}
+ |
Искомый ресурс {{ Модель.Запрос.Путь }} не найден
"); +КонецПроцедуры +``` + + + +## Перенаправление + +Иногда бывает так, что нужно с одной страницы, перенаправить позльзователя на другую. + +Для этого, у объекта ```Ответ``` есть метод ```Перенаправить(<Адрес куда перенаправить>)```. Например, подобная точка маршрута будет перенаправлять запрос в корень приложения + +```bsl +&ТочкаМаршрута("/redir") +Процедура Перенаправление(Ответ) Экспорт + Ответ.Перенаправить("/"); +КонецПроцедуры +``` + +## Загрузка настроек из файла + +Настройки порта, имени хоста, каталогов файлов и приложений можно можно хранить в json файле, и загружать при старте приложения. + +Файл обязательно должен называться ```autumn-properties.json``` / ```autumn-properties.yml``` / ```autumn-properties.yaml```, и быть в корне запуска сервера или в подкаталоге ```src/```. Если какие-то значения не указаны, то они имеют значения по умолчанию. + +Пример: + +```JSON +{ "winow": + { + "Порт": 3331, // по умолчанию 3333 + "АвтоСтарт": true, // Управляет автоматическим запуском при Поделка.ЗапуститьПриложение() (по умолчанию Истина) + "ИмяХоста": "MySuperAPPHost", // по умолчанию localhost + "КаталогСПриложениями": "./hwapp", // по умолчанию "./app" + "РазмерБуфера": 1024, // Размер порции в байтах, которыми читаются данные из TCP соединения. (по умолчанию 1024) + "КаталогиСФайлами": { + "/images": "./hwapp/files" // значения по умолчанию нет + } + } +} +``` + +## Управление доступом + +Для управления доступом к точке маршрута, предусмотрена аннотация ```&Роли("<Список ролей через запятую>")```. Все очень просто, и остается ответить только на один вопрос - как эти роли раздать, и как хранить данные входа пользователей. Пока это mvp, точного ответа не дам. Разработчик может самостоятельно придумать, как и где хранить группы и пароли. Я только покажу, как их подключить в наше приложение. + +``` +app/КонтролСУправлениемДоступом.os +``` + +```bsl +&Пластилин +Перем МенеджерДоступа Экспорт; + +&Контроллер("/sec") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры + +&ФинальныйШтрих +Процедура ПроинициализироватьРоли() Экспорт + + // Инициализация данных входа "пользователей" + МенеджерДоступа.ДобавитьТокен("Админ", "123"); + МенеджерДоступа.ДобавитьТокен("Пользователь", "111"); + + // Назначение ролей "пользователям" + МенеджерДоступа.ДобавитьРольЛогина("Админ", "Администраторы"); + МенеджерДоступа.ДобавитьРольЛогина("Админ", "Пользователи"); + + МенеджерДоступа.ДобавитьРольЛогина("Пользователь", "Пользователи"); +КонецПроцедуры + +// Точка доступная роли пользователи +&Роли("Пользователи") +&ТочкаМаршрута("user") +Процедура Пользователь(Ответ) Экспорт + Ответ.ТелоТекст = "Пользователи"; +КонецПроцедуры + +// Точка доступная роли Администраторы +&Роли("Администраторы") +&ТочкаМаршрута("admin") +Процедура Админ(Ответ) Экспорт + Ответ.ТелоТекст = "Админка"; +КонецПроцедуры + +// обычная точка, доступная всем +&ТочкаМаршрута("free") +Процедура Все(Ответ) Экспорт + Ответ.ТелоТекст = "Все подрят"; +КонецПроцедуры + +// Точка доступная ролям администраторы, пользователи. +&Роли("Администраторы, Пользователи") +&ТочкаМаршрута("usradm") +Процедура АдминыИПользователи(Ответ) Экспорт + Ответ.ТелоТекст = "Админы и пользователи"; +КонецПроцедуры +``` + +## Работа с протоколом WebSocket + +В winow в экспериментальном виде реализована поддержка веб-сокетов. Спека не полная, поддерживаются пока только текстовые сообщения. Разберем работу протокола на примере онлайн чата. Я не буду углубляться в часть фронта. Вот [пример](https://github.com/autumn-library/winow/blob/master/example/hwapp/view/chat.html) реализации на фронте. Основная суть такая - когда мы заходим на контрол ```/chat``` осуществляется проверка, залогинился пользователь или нет. Если нет, переадресуем на страницу ввода логина. + +После ввода логина мы попадаем на страницу с чатом, где осуществляется подключение вебсокета, и начинается обмен сообщениями. + +А теперь разберем пример того, что происходит на стороне сервера. Обработкой входящих сообщений занимается такой же контроллер с точками маршрута, к которым мы привыкли. Каждая точка маршрута является "топиком" в рамках которого общается одно соединение веб сокета. В примере ниже у нас контроллер по адресу ```chat``` и точкой маршрута ```message```. У нас одно соединение, которое обменивается сообщениями в топике ```/chat/message```. Точка маршрута по обработке сообщений может принимать несколько параметров: +- Идентификатор - идентификатор сессии, в которой можно хранить разные данные. +- Топик - имя топика, в рамках которого происходит общение +- Сообщение - расшифрованное сообщение, которое пришло от клиента. + +Для того, чтобы отправлять сообщения, нужно получить желудь ```БрокерСообщенийВебСокетов```. Который умеет следующие действия с сообщениями: + +- ОтправитьСообщение(Топик, Сообщение, Идентификатор) - Отправляет определенному клиенту сообщение в указанный топик. +- ОтправитьСообщениеВсем(Топик, Сообщение) - Отправляет сообщение всем клиентам, подключенным к указанному топику. +- ОтправитьСообщениеСписку(Топик, Сообщение, СписокИдентификаторов) - Отправляет сообщение массиву клиентов, подписанных на указанный топик. +- ОтправитьСообщениеВсемКроме(Топик, Сообщение, СписокИсключенийИдентификаторов) - Отправляет сообщение всем, клиентам подписанным на указанный топик, кроме массива, переданного как параметр. + +Так же контроллер может иметь методы, помеченные аннотациями ```&ПриПодключенииВебСокета("/имя/топика")``` и ```ПриОтключенииВебСокета("/имя/топика")```. Которые будут вызваны, после соответствующих событий, и принимать ```Идентификатор``` клиента, с которым произошло событие. + +Вот полный пример с комментариями. + +``` +ВебСокетЧат.os +``` + +```bsl +&Пластилин Перем БрокерСообщенийВебСокетов; // Инжектим желудь, который управляет отправкой сообщений + +Перем КешИменПользователей; + +&Контроллер("/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](../static/winow/ws-chat.gif) + +## Работа с механизмом server-sent events. + +Подробно можно прочитать на [вики](https://ru.wikipedia.org/wiki/Server-sent_events). + +Для реализации работы с механизмом нужно выполнить несколько шагов. + +1. Зарегистрировать топик, в конструкторе контроллера. +2. Добавить обработчики событий, которые будут вызываться при подключении и отключении клиента. (опционально) +3. Посылать сообщения клиенту в топики, при необходимости в соответствии с логикой приложения. + +Рассмотрим на примере: + +```bsl + +// Подключаем необходимые зависимости +&Пластилин Перем БрокерСообщенийСобытийСервера; +&пластилин Перем ФабрикаОтветов; + +&Контроллер("/sse") +&Отображение(Шаблон = "./hwapp/view/main_sse.html") +Процедура ПриСозданииОбъекта(&Пластилин ТопикиСерверныхСобытий) + + ИмяТопика = "/sse/acorndiscussion"; + + // регистрируем топик и обработчики открытия и закрытия + ТопикиСерверныхСобытий.Добавить(ИмяТопика, + Новый Действие(ЭтотОбъект, "НовоеПодключениеССЕ"), + Новый Действие(ЭтотОбъект, "ОтключениеССЕ")); + +КонецПроцедуры + +Процедура НовоеПодключениеССЕ(Сессия, ИД) Экспорт + + // Код обработчика открытия соединения + +КонецПроцедуры + +Процедура ОтключениеССЕ(Сессия, ИД) Экспорт + + // Код обработчика закрытия соединения + +КонецПроцедуры + +Процедура ОтправитьСообщение() + // Создаем сообщение + Сообщение = ФабрикаОтветов.СерверноеСобытие(); + Сообщение.ТипСобытия("like"); + Сообщение.ДобавитьСтроку("Некий текст"); + + // Отправим сообщение всем клиентам, слушающим топик. + БрокерСообщенийСобытийСервера.ОтправитьСообщениеВсем(ИмяТопика, Сообщение); +КонецПроцедуры + +``` + +Пример клиента который подписывается на топик, и вызывает события в зависимости от типа полученного сообщения : + +```js +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); +}); + +``` + +Полный пример можно посмотреть примерах - [контрол](https://github.com/autumn-library/winow/blob/master/example/hwapp/СерверныеСобытия.os) и [клиент](https://github.com/autumn-library/winow/blob/master/example/hwapp/view/main_sse.html). + +Апи объектов: + +```БрокерСообщенийВебСокетов``` + +- ОтправитьСообщениеВсем(Топик, Сообщение): Отправка сообщения всем; +- ОтправитьСообщениеПоИдСоединения(ИдСоединения, Сообщение): Отправка сообщения конкретному клиенту; +- ОтправитьСообщениеСписку(Топик, Сообщение, МассивИдентификаторов): Отправка сообщения массиву клиентов; +- ОтправитьСообщениеВсемКроме(Топик, Сообщение, МассивИсключенийИдентификаторов): Отправка сообщения с исключающим массивом клиентов; + +```ТопикиСерверныхСобытий``` + +- Существует(Топик): Проверка существования топика; +- Добавить(Топик, ОбработчикОткрытия, ОбработчикЗакрытия): Добавление топика. С возможностью подписки на события открытия и закрытия соединения. Тут принимаются объекты Действие, которые должны иметь интрефейс: ```Процедура ИмяОбработчика(Сессия, ИД) Экспорт``` Где сессия - идентификатор сессии, и идентифкатор конкретного соединения. + +```Сообщение```. Получается из фабрики ответов. ```Сообщение = ФабрикаОтветов.СерверноеСобытие();``` + +- ТипСобытия(ТипСобытия): Установка типа события; +- ДобавитьСтроку(Строка): Добавление строки в сообщение; +- Идентификатор(ид) : Установка идентификатора сообщения; + +Пример реактивного интерфейса на server sent events + +![sse](../static/winow/sse.gif) + +## Использование cli + +winow предоставляет интерфейс командной строки. Запуск приложения становится еще проще. Для этого нужно установить пакет [winow-cli](https://github.com/autumn-library/winow-cli). + +## Контейнеризация + +Приложение на winow можно, конечно, запустить в контейнере. + +Вот небольшой пример, как это сделать. + +Нужно в один каталог положить: +* Само приложение [app](https://github.com/autumn-library/winow/blob/master/docker/app/), которое содержит скрипт запуска, контролы, файлы и картинки, ```autumn-properties.json``` со всеми настройками и т.д. +* [Dockerfile](https://github.com/autumn-library/winow/blob/master/docker/Dockerfile) для того, чтобы собрать образ, и прокинуть в него все файлы. +* Скрипт запуска сервера [docker-entrypoint.sh](https://github.com/autumn-library/winow/blob/master/docker/docker-entrypoint.sh). +* И все это дело удобно собирать одним скриптом [start.sh](https://github.com/autumn-library/winow/blob/master/docker/start.sh). \ No newline at end of file