Гостевая книга в 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.
Начнем представление 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
из адресной строки браузера:
В Libretto Jet метод 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>
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 организована работа с базами данных. Вот три важных особенности:
map
– это некоторый объект базы данных, то map!key
– это значение объекта map
на ключе key
.
m.string(key)
берет значение ключа key
в мэпе m
и интерпретирует его как строчку. Метод m.maps(key)
берет значения ключа key
в мэпе m
и интерпретирует их как последовательность мэпов. Метод m.addl(key, val)
добавляет еще одно значение val
ключу key
в качестве последнего.
Вернемся к нашему проекту. Организуем работу с базой данных в пакете 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))
Прокомментируем:
com/teacode/data
.
db(epxr)
, организующий транзакции в базе с именем "gbase"
. Библиотечный метод jet/trand(basename, expr)
создает транзакцию в базе с именем basename
и выполняет в рамках этой транзакции выражение expr
. Если базы с именем basename
не существует, то Libretto Jet создает эту базу, пользуясь ресурсами сервера.
add
, который добавляет новое сообщение к базе данных. Вызов метода db
с одним параметром позволяет обернуть в транзакцию операции работы в базе. Последовательность имеющихся сообщений хранится в ключе 'posts
корневого объекта базы data/root
(доступ к данным базы осуществляется через ее корневой объект). Библиотечный метод data/new
создает новый объект в базе.
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 в случае асинхронного взаимодействия выполняет две совершенно разные функции:
По довольно большой уже статистике применения среды Libretto Jet, она замещает Javascript в 100% случаев применения второй функции и где-то 99% применения первой (только продвинутая обработка на стороне клиента, которая не укладывается в набор 20-30 наиболее популярных инструкций, требует явного применения Javascript).
Конечно, Javascript остается ключевым игроком на стороне клиента, но его роль в Libretto Jet инкапсулируется, прячется.
Libretto является компилируемым языком. Эффективность компилируемого кода примерно равна соответствующему коду на Java. Однако сам код на Libretto в среднем в 5 раз компактнее, чем его аналог на Java. В сочетании с единой программной моделью такая компактность предоставляет принципиально иные возможности не только по разработке систем, но и по их поддержке.
Наконец, в языке сочетаются стили статического и динамического программирования. Например, на Libretto можно быстро развернуть прототип системы или сервиса в стиле динамического программирования (как на Ruby, Python или Perl), а затем методом "последовательных уточнений" внедрить в код полновесный статический контроль типов данных (как в Java или Scala).
Перечисленные свойства языка Libretto и среды Libretto Jet позволяют охарактеризовать их как принципиально новую и очень эффективную технологию разработки веб-сервисов.
На Libretto Jet удобно писать как простые и компактные веб-проекты, так и большие, распределенные и масштабируемые веб-сервисы.