Строим гостевую книгу в Libretto Jet
Оглавление
Домашняя страница гостевой книги
Сохранение сообщений в базе данных
В данном посте мы покажем, как создаются веб-приложения с помощью Libretto Jet.
Libretto Jet – среда разработки веб-приложений, реализующая единую модель веб-программирования.
Единая модель веб–программирования – технология, в которой единый язык программирования используется как для кодирования на сервере и клиенте, так и для запросов к базам данных.
Продемонстрируем работу Libretto Jet на примере задачи построения гостевой книги.
Гостевая книга
Гостевая книга – веб–приложение, которое дает возможность пользователю оставлять комментарии и другие сообщения, а также обмениваться мнениями с другими посетителями. Ниже мы реализуем этот сервис, который будет выглядеть примерно так:
Начинаем работу
О том, как запустить свой Web-проект в Libretto IDE рассказано здесь.
Структура проекта – это директория с названием проекта, в которой содержатся две под-директории:
libretto
– для файлов с Libretto-кодом.public
– для ресурсов и публичных файлов, например, изображений, Javascript кода, CSS-файлов, и т.д.
Назовем наш проект gbook
.
Директория libretto
содержит четыре файла – пока почти пустых:
- в
model.ltt
будет организована работа со структурами данных (это на сервере), - во
view.ltt
будет определен внешний вид гостевой книги (в клиентском браузере), url.ltt
будет отвечать за взаимодействие между клиентом и сервером,- в
control.ltt
определены технические методы работы среды Libretto Jet.
Поведение Libretto-программы не зависит от файловой структуры проекта. Можно определить несколько пакетов в одном файле, а можно один пакет – в нескольких файлах (пакет – замкнутый независимый модуль Libretto–программы). Компилятор транслирует все файлы из директории libretto
и всех ее под-директорий.
В директории public
проекта gbook
находится библиотека JQuery, которую использует в своей работе Libretto Jet.
Hello, world!
Начнем представление Libretto Jet с реализации классического приветствия "Hello, world!"
. Определим для этого метод hello
:
// url.ltt package url use com/teacode/jet def hello = jet/out: <h1>Hello, world!</h1>
Метод hello
определен в пакете url
. Мы сразу можем загрузить страницу из hello
в браузер, благодаря следующей возможности Libretto Jet:
Libretto Jet позволяет вызывать методы из пакета url
напрямую из адресной строки браузера.
Вызовем метод hello
:
Метод hello
возвращает браузеру веб-страницу (в нашем случае – восклицание Hello, world!
). Страница пересылается с помощью библиотечного метода jet/out
. Этот метод определен в базовой библиотеке Libretto Jet com/teacode/jet
, которая импортируется в строке 4.
В определении метода hello
продемонстрированы две отличительные особенности языка Libretto.
HTML и XML – выражения.
HTML-выражение в определении метода
hello
является обычной структурой языка Libretto, которую можно свободно перемешивать с другим кодом.
В Libretto можно использовать XML-структуры как обычные выражения языка. HTML–выражения также записываются в строгом XML–формате.
- Синтаксический сахар.
В Libretto запись f:x
является синтаксическим сахаром для f(x)
. В более общем случае запись f(x1, x2): x3, x4
эквивалентна f(x1, x2, x3, x4)
.
Заметим, что страницу приветствия можно легко параметризовать:
// url.ltt package url use com/teacode/jet def hello = jet/out: <h1>Hello, #{ this.jet/string('name) }!</h1>
Теперь:
О том, как такой код работает, расскажем чуть позже. А пока начнем реализацию гостевой книги.
Домашняя страница гостевой книги
Сформируем домашнюю страницу гостевой книги. Эта задача имеет два этапа – генерирование страницы, и ее передача клиенту. Метод page
, рисующий страницу, расположим в пакете view
(в файле view.ltt
), а передающий страницу метод home
определим в пакете url
в файле url.ltt
.
Так будет выглядеть url.ltt
после определения в нем метода home
:
// url.ltt package url use com/teacode/jet use view def home = jet/out: view/page
Здесь:
- Контроллер веб-приложения "гостевая книга" формируется в пакете
url
. - Импортируется библиотека
com/teacode/jet
, которая реализует среду веб-разработки Libretto Jet. - Импортируется пакет
view
, в котором будет определен внешний вид гостевой книги.
Определим теперь сам метод page
в рамках пакета view
:
// view.ltt package view def page = <html> <head><title>Гостевая книга</title></head> <body> <h1>Гостевая книга</h1> </body> </html>
Вызовем метод home
из адресной строки браузера:
home
из пакета url
по умолчанию используется для определения домашней (корневой) страницы веб–приложения. Поэтому home
в адресной строке может быть опущен (рис. 1а).
Ввод сообщения
Для публикации сообщения в гостевой книге пользователь должен задать свое имя и написать текст сообщения. Организуем эту возможность, определив блок редактирования сообщений в отдельном методе editor
и включив этот блок в домашнюю страницу:
// view.ltt package view use com/teacode/jet def editor = <div> <p>Имя:</p> <div><input size="30" type="text"/></div> <p>Сообщение:</p> <textarea cols="50" rows="3"></textarea> <button>Добавить</button> </div> def page = <html> <head> <title>Гостевая книга</title> <script src="/jquery/jquery.min.js"/> <script>#{jet/jetjs}</script> </head> <body> <h1>Гостевая книга</h1> #{editor} </body> </html>
Обратите внимание на то, что метод editor
вызывается внутри HTML-выражения (строка 24). Выражение внутри скобок #{...}
вычисляется, и полученное значение подставляется в HTML-код.
Libretto Jet на клиенте. Мы включили в заголовок домашней страницы библиотеку JQuery и скрипт библиотеки Libretto Jet:
<script src="/jquery/jquery.min.js"/>
<script>#{jet/jetjs}</script>
Они необходимы для работы Libretto Jet на клиенте. Скрипт загружается из библиотеки com/teacode/jet
с помощью метода jetjs
.
В результате получаем:
Управление кнопкой
Чтобы поля редактирования заработали, нам нужно научиться передавать их содержимое от клиента серверу. Сначала научимся управлять кнопкой.
Начнем с простейшего примера – привяжем к кнопке "Добавить" инструкцию «выбросить окошко с надписью Hello, World!
»:
// view.ltt package view use com/teacode/jet def editor = <div> <p>Имя:</p> <div><input size="30" type="text"/></div> <p>Сообщение:</p> <textarea cols="50" rows="3"></textarea> <button onclick=#{jet/ajax: 'publish}>Добавить</button> </div> def page = <html> <head> <title>Гостевая книга</title> <script src="/jquery/jquery.min.js"/> <script>#{jet/jetjs}</script> </head> <body> <h1>Гостевая книга</h1> #{editor} </body> </html>
- К кнопке "Добавить" в строке 12 мы привязали обработчик события
onclick
.
- Метод
jet/ajax
генерирует Javascript-код, который при нажатии кнопки "Добавить" вызывает методpublish
из пакетаurl
в асинхронном режиме (в стиле AJAX).
Определим сам метод publish
:
// url.ltt package url use com/teacode/jet use view def home = jet/out: view/page def publish = jet/alert("Hello, World!")
Метод jet/alert
задает клиенту инструкцию выбросить окошко "Hello, World!"
в асинхронном режиме. Libretto Jet предлагает около двадцати различных видов асинхронных инструкций, предназначенных для клиента – append
, prepend
, replace
, remove
, eval
и других. Некоторыми из них мы воспользуемся ниже.
Нажмем на кнопку, чтобы проверить ее работу:
Передача параметров на сервер
Теперь нам нужно научиться передавать на сервер текущие значения элементов <input/>
и <textarea/>
. Выборку тех элементов, значения которых мы хотим передать, можно задать как аргумент метода jet/ajax
. Расширим код view.ltt
следующим образом:
// view.ltt package view use com/teacode/jet def editor = <div> <p>Имя:</p> <div><input name="name" class="param" size="30" type="text"/></div> <p>Сообщение:</p> <textarea name="msg" class="param" cols="50" rows="3"></textarea> <button onclick=#{jet/ajax: 'publish, (), ".param"}>Добавить</button> </div> def page = <html> <head> <title>Гостевая книга</title> <script src="/jquery/jquery.min.js"/> <script>#{jet/jetjs}</script> </head> <body> <h1>Гостевая книга</h1> #{editor} </body> </html>
Изменения:
- Выборка элементов, значения которых нужно передать на сервер, задана в третьем аргументе метода
jet/ajax
(строка 12). Селектор".param"
означает, что методуpublish
должны быть переданы значения всех элементов из классаparam
.
- Элементам
input
иtextarea
добавлен атрибутclass="param"
. Кроме того, добавлены атрибутыname
, используемые для доступа к конкретным значениям (name="name"
(с. 9) иname="msg"
(с. 11)).
Селекторы в Libretto Jet имеют синтаксис, эквивалентный синтаксису селекторов в JQuery.
Второй аргумент jet/ajax
отвечает за передачу на сервер констант (аналог input/hidden
), но таких констант в нашем примере нет.
Изменим метод publish
, заставив его выкидывать в окне значение поля с id="name"
:
// url.ltt package url use com/teacode/jet use view def home = jet/out: view/page def publish = jet/alert(this.jet/string('name))Метод
jet/string
получает значение элемента по его name
в строковом виде. Веб параметры передаются в контексте метода publish
, поэтому используем this
.
Перегружаем домашнюю страницу, вводим имя и нажимаем на кнопку:
Публикация сообщений
Наш следующий шаг – научить приложение публиковать ленту пришедших сообщений.
Во-первых, добавим во view.ltt
метод, генерирующий внешний вид сообщения. Метод post
(с.15), получая в качестве аргументов имя пользователя и его сообщение, формирует соответствующий HTML-элемент:
// view.ltt package view use com/teacode/jet def editor = <div> <p>Имя:</p> <div><input name="name" class="param" size="30" type="text"/></div> <p>Сообщение:</p> <textarea name="msg" class="param" cols="50" rows="3"></textarea> <button onclick=#{jet/ajax: 'publish, (), ".param"}>Добавить</button> </div> def post(name:String?, text:String?) = <div> <p style="font-size:80%; font-weight: bold; margin-top:20px;">#{name}</p> <p>#{text}</p> </div> def page = <html> <head> <title>Гостевая книга</title> <script src="/jquery/jquery.min.js"/> <script>#{jet/jetjs}</script> </head> <body> <h1>Гостевая книга</h1> <div id="contents"></div> <hr/> #{editor} </body> </html>
Кроме того, в HTML–страницу из метода page
добавлен элемент <div id="contents"></div>
, в котором будет формироваться лента сообщений (c.30).
Нам осталось переделать метод publish
так, чтобы он начал управлять публикацией сообщений:
// url.ltt package url use com/teacode/jet use view def home = jet/out: view/page def publish = { fix name = this.jet/string('name) fix msg = this.jet/string('msg) jet/append("#contents", view/post(name, msg)) }Мы добавили в
publish
получение параметров 'name
и 'msg
, содержащих имя пользователя и текст сообщения. Кроме того, инструкция jet/alert
заменена на другую асинхронную инструкцию – jet/append(selector, expr)
. Эта инструкция побуждает браузер найти все элементы, соответствующие выборке selector
, и добавить к их контенту выражение expr
.
В нашем случае инструкция append
позволяет добавить сообщение к элементу с id="contents"
.
Проверяем:
Как видим, наш сервис научился формировать ленту сообщений. Заметим, что после добавления сообщения в полях остаются неподчищенные значения от предыдущего ввода. Но это легко исправить:
// url.ltt package url use com/teacode/jet use view def home = jet/out: view/page def publish = { fix name = this.jet/string('name) fix msg = this.jet/string('msg) jet/append("#contents", view/post(name, msg)). jet/val(".param", "") }Добавлен вызов метода
jet/val
, который задает новые – пустые – значения полям ввода. Теперь все в порядке:
Сохранение сообщений в базе данных
Нам остался заключительный шаг – сохранение сообщений в базе данных. К настоящему моменту гостевая книга уже выглядит как настоящая, однако лента сообщений нигде не сохраняется, видна она только одному пользователю, и если страницу перезагрузить, то все исчезнет.
Прежде чем добавить в наш проект долговременное хранение соообщений, скажем несколько слов о том, как в Libretto организована работа с базами данных. Вот три важных особенности:
- В Libretto встроена поддержка баз данных типа ключ/значение: если
map
– это некоторый объект базы данных, тоmap!key
– это значение объектаmap
на ключеkey
.
- Мы будем использовать дополнительные методы работы с мэпами. Метод
m.string(key)
берет значение ключаkey
в мэпеm
и интерпретирует его как строчку. Методm.maps(key)
берет значения ключаkey
в мэпеm
и интерпретирует их как последовательность мэпов. Методm.addl(key, val)
добавляет еще одно значениеval
ключуkey
в качестве последнего.
- Все операции с базами данных могут проводиться только в рамках транзакций. Транзакции обеспечивают атомарность кода и реализуют оптимистический подход к разрешению коллизий. Если в процессе выполнения транзакции возникла ошибка, то транзакция откатывается, затем выполняется повторная попытка её выполнения.
- Libretto сам играет роль языка запросов к базам данных (вместо привычного SQL), обеспечивая тем самым преимущества единой среды программирования.
Вернемся к нашему проекту. Организуем работу с базой данных в пакете model
(файл model.ltt
):
// model.ltt package model use com/teacode/jet use com/teacode/data use view def db(#expr) = jet/trand("gbase", expr) def add(name:String?, msg:String?) = db: data/root.addl('posts, data/new {'name to name; 'text to msg}) def posts = db: data/root.maps('posts) as p. view/post(p.string('name), p.string('text))
Прокомментируем:
- Строка 5 подключает библиотеку для работы с базами
com/teacode/data
. - Строка 8 определяет одноместный метод
db(epxr)
, организующий транзакции в базе с именем"gbase"
. Библиотечный методjet/trand(basename, expr)
создает транзакцию в базе с именемbasename
и выполняет в рамках этой транзакции выражениеexpr
. Если базы с именемbasename
не существует, то Libretto Jet создает эту базу, пользуясь ресурсами сервера. - Строки 10–11 определяют метод
add
, который добавляет новое сообщение к базе данных. Вызов методаdb
с одним параметром позволяет обернуть в транзакцию операции работы в базе. Последовательность имеющихся сообщений хранится в ключе'posts
корневого объекта базыdata/root
(доступ к данным базы осуществляется через ее корневой объект). Библиотечный методdata/new
создает новый объект в базе. - Строки 13–14 определяют метод
posts
, который считывает из базы имеющиеся сообщения и формирует последовательность HTML-элементов, представляющих эти сообщения.
Обратите внимание на то, что в определении метода posts
может обрабатываться последовательность из множества сообщений. В этом случае оператор точка "."
выполняет роль итератора, генерируя нужное количество HTML-элементов (операторы цикла в Libretto отсутствуют за ненадобностью).
Теперь добавим работу с базой в контроллер:
// url.ltt package url use com/teacode/jet use view use model def home = jet/out: view/page def publish = { fix name = this.jet/string('name) fix msg = this.jet/string('msg) model/add(name, msg) jet/append("#contents", view/post(name, msg)). jet/val(".param", "") }Здесь у нас два обновления:
- Добавлен импорт пакета
model
(с.6). - В определение
publish
добавлен вызов методаmodel/add
(с.14), сохраняющего новые сообщения в базе.
В файле view.ltt
при загрузке домашней страницы теперь публикуется текущая лента сообщений (с.31):
// view.ltt package view use com/teacode/jet use model def editor = <div> <p>Имя:</p> <div><input name="name" class="param" size="30" type="text"/></div> <p>Сообщение:</p> <textarea name="msg" class="param" cols="50" rows="3"></textarea> <button onclick=#{jet/ajax: 'publish, (), ".param"}>Добавить</button> </div> def post(name:String?, text:String?) = <div> <p style="font-size:80%; font-weight: bold; margin-top:20px;">#{name}</p> <p>#{text}</p> </div> def page = <html> <head> <title>Гостевая книга</title> <script src="/jquery/jquery.min.js"/> <script>#{jet/jetjs}</script> </head> <body> <h1>Гостевая книга</h1> <div id="contents">#{model/posts}</div> <hr/> #{editor} </body> </html>
Проведенные манипуляции с сохранением сообщений напрямую не отражаются на интерфейсе гостевой книги. Однако теперь все пользователи, вошедшие в гостевую книгу с разных компьютеров, увидят одинаковую последовательность сообщений, и смогут общаться друг с другом.
Разработка гостевой книги завершена.
Единая программная модель
Подведем итоги. Как мы уже сказали, в Libretto Jet реализуется единая программная модель. Это означает стремление к тому, чтобы максимальное количество операций кодировалось на одном языке – Libretto.
Речь вовсе не идет о полной блокировке внешних технологий. Программная среда Libretto Jet открыта всем технологиям, однако организована так, чтобы писать на одном языке было более удобно и более эффективно.
Сокращение инструментов веб-разработки
В примере с гостевой книгой Libretto заменил Javascript при организации взаимодействия клиента и сервера, и заменил SQL как язык запросов к базам данных.
Обратите внимание на то, что у нас даже несколько размылись границы между клиентом и сервером, сделав ненужными специальные языки обмена данными, типа XML или JSON. И сервер и клиент укладываются в единую и совместную модель типов данных Libretto.
Инкапсуляция Javascript
Отдельно следует сказать об использовании Javascript. Javascript в случае асинхронного взаимодействия выполняет две совершенно разные функции:
- проведение манипуляций на клиенте (например, динамические изменения на веб странице)
- организация взаимодействия клиента и сервера (AJAX)
По довольно большой уже статистике применения среды Libretto Jet, она замещает Javascript в 100% случаев применения второй функции и где-то 99% применения первой (только продвинутая обработка на стороне клиента, которая не укладывается в набор 20-30 наиболее популярных инструкций, требует явного применения Javascript).
Конечно, Javascript остается ключевым игроком на стороне клиента, но его роль в Libretto Jet инкапсулируется, прячется.
Эффективность Libretto
Libretto является компилируемым языком. Эффективность компилируемого кода примерно равна соответствующему коду на Java. Однако сам код на Libretto в среднем в 5 раз компактнее, чем его аналог на Java. В сочетании с единой программной моделью такая компактность предоставляет принципиально иные возможности не только по разработке систем, но и по их поддержке.
Итого
Перечисленные свойства языка Libretto и среды Libretto Jet позволяют охарактеризовать их как принципиально новую и очень эффективную технологию разработки веб-сервисов.
На Libretto Jet удобно писать как простые и компактные веб-проекты, так и большие, распределенные и масштабируемые веб-сервисы.