Гостевая книга в Libretto Jet

29.1.2019 20:45

Строим гостевую книгу в 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.

  1. HTML и XML – выражения.

    HTML-выражение в определении метода hello является обычной структурой языка Libretto, которую можно свободно перемешивать с другим кодом.

В Libretto можно использовать XML-структуры как обычные выражения языка. HTML–выражения также записываются в строгом XML–формате.

  1. Синтаксический сахар.

В 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 из адресной строки браузера:

Рис 1. Вызов метода `home`
В Libretto Jet метод home из пакета url по умолчанию используется для определения домашней (корневой) страницы веб–приложения. Поэтому home в адресной строке может быть опущен (рис. 1а).

Рис 1a. Загрузка корневой страницы, определенной в `url/home`

Ввод сообщения

Для публикации сообщения в гостевой книге пользователь должен задать свое имя и написать текст сообщения. Организуем эту возможность, определив блок редактирования сообщений в отдельном методе 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.

В результате получаем:

Рис 2. Поля для редактирования сообщения

Управление кнопкой

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

Начнем с простейшего примера – привяжем к кнопке "Добавить" инструкцию «выбросить окошко с надписью 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 и других. Некоторыми из них мы воспользуемся ниже.

Нажмем на кнопку, чтобы проверить ее работу:

Рис 3. При нажатии кнопки выскакивает окно

Передача параметров на сервер

Теперь нам нужно научиться передавать на сервер текущие значения элементов <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.

Перегружаем домашнюю страницу, вводим имя и нажимаем на кнопку:

Рис 4. При нажатии кнопки в появляющемся окне выдается имя пользователя

Публикация сообщений

Наш следующий шаг – научить приложение публиковать ленту пришедших сообщений.

Во-первых, добавим во 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".

Проверяем:

Рис 5. Диалог

Как видим, наш сервис научился формировать ленту сообщений. Заметим, что после добавления сообщения в полях остаются неподчищенные значения от предыдущего ввода. Но это легко исправить:

// 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, который задает новые – пустые – значения полям ввода. Теперь все в порядке:

Рис 6. Поля ввода подчищаются после нажатия кнопки

Сохранение сообщений в базе данных

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

Прежде чем добавить в наш проект долговременное хранение соообщений, скажем несколько слов о том, как в 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 можно быстро развернуть прототип системы или сервиса в стиле динамического программирования (как на Ruby, Python или Perl), а затем методом "последовательных уточнений" внедрить в код полновесный статический контроль типов данных (как в Java или Scala).

Итого

Перечисленные свойства языка Libretto и среды Libretto Jet позволяют охарактеризовать их как принципиально новую и очень эффективную технологию разработки веб-сервисов.

На Libretto Jet удобно писать как простые и компактные веб-проекты, так и большие, распределенные и масштабируемые веб-сервисы.