1i7 (1i7) wrote,
1i7
1i7

Разработка веб-приложений на Scala: пишем приложение с Unfiltered

Среду разработки Netbeans для работы со Scala настроили, пишем и запускаем веб-приложение с фреймворком Unfiltered.


Веб-приложение на Scala с Unfiltered

Весь код нашего простого, но функционального веб-приложения находится в одном файле.

Начало традиционное - пэкэдж, импорты используемых классов, главный класс для текущего файла (object в Scala реализует паттерн "Класс-одиночка", который в Яве принято делать из обычного класса вручную).

ScalaUnfilteredBasicDemo/src/main/scala/edu/nntu/scalaunfiltereddemo/UnfilteredBasicDemo.scala

package edu.nntu.scalaunfiltereddemo

import unfiltered.request.Path
import unfiltered.request.Seg
import unfiltered.response.Ok
import unfiltered.response.ContentType
import unfiltered.response.CssContent
import unfiltered.response.HtmlContent
import unfiltered.response.PlainTextContent
import unfiltered.response.ResponseBytes
import unfiltered.response.ResponseString
import unfiltered.response.Html

/**
 * Базовый пример Http-сервера с Unfiltered
 * http://unfiltered.databinder.net/Try+Unfiltered.html
 *
 * с нормально поддержкой UTF-8 в строке запроса и в теле ответа.
 *
 */
object UnfilteredBasicDemo {


Определим вспомогательный класс для нормальной работы с юникодными символами в адресной строке (да, в 2014 году для этого всё еще приходится использовать специальные костыли, и это касается не только Unfiltered или Scala, послезавтра в 2015 скорее всего тоже ничего не изменится). Заметим, как начинают появляться маленькие фишечки Скалы: импорты можно помещать не только в начале класса, но и внутри вообще любого блока.


/**
   * Перекодировщик URL для нормальной работы с UTF-8
   * Отсюда: http://stackoverflow.com/questions/18083311/url-decoding-with-unfiltered
   */
  object Decode {
    import java.net.URLDecoder
    import java.nio.charset.Charset

    trait Extract {
      def charset: Charset
      def unapply(raw: String) =
        scala.util.Try(URLDecoder.decode(raw, charset.name())).toOption
    }

    object utf8 extends Extract {
      val charset = Charset.forName("utf8")
    }
  }


Главная часть веб-приложения с фреймфорком Unfiltered - фильтр, работающий с сегментами строки URL, которую ввели адресную строку браузера или отправили программно через JavaScript при доступе к веб-приложению. Для разных сегментов веб-приложение возвращает разный результат (простой текст, HTML-страницу, CSS-файл, двоичный контент или что-то еще), собственном в этом и заключается вся его работа: пришел запрос - отправили ответ.

Каждый блок case соответствует одному или нескольким (для регулярных выражений) сегментам URL. В этом примере рассмотрены сегменты (это и есть структура веб-приложения):
- Nil: для корневого сегмента "/" (показываем простой текст с приветствием и инструкциями).
- /plain_text: пример простого текста
- /inline_html: пример кода HTML, встроенного прямо в код Scala
- /static_html: пример статического файла HTML, заргужаемого из ресурсов внутри jar-файла
- /site.css: файл CSS, загружается из ресурсов внутри jar-файла
- /lasto4ka.png и /buggy.png: картинки PNG, загружаются из ресурсов внутри jar-файла

последний блок case принимает любой сегмент и просто возвращает его обратно.

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

Замечание 1: Подобные HTTP-серверы обычно используют для того, чтобы возвращать обычный текст (или текст в формате JSON) для обслуживая запросов JavaScript/Ajax, но, как видим, для раздачи любого другого веб-контента он вполне подходит, поэтому мы сможем сделать на нем полноценное веб-приложение целиком.

Замечание 2: Читать картинки по байтам из ресурсов архива jar - не самый лучший способ работы с графикой в веб-приложении, в первую очередь из-за производительности. Поэтому для работы с картинками в реальной инсталляции приложения, которая будет подразумевать хоть какую-то нагрузку, стоит использовать более подходящий для этого способ (работа с внешними ресурсами в Jetty или Netty, использовать внешний http-сервер для статического контента или что-то еще). Но мне нравится иметь теоретическую и практическую возможность хранить любые типы ресурсов в одном jar-файле, для персональных экспериментов производительности будет более чем достаточно.


  // Для контейнера веб-приложений jetty
  val handlePath = unfiltered.filter.Planify {
  // Для контейнера веб-приложений netty
  //  val handlePath = unfiltered.netty.cycle.Planify {

    // список определений доступных XXXContent:
    // https://github.com/unfiltered/unfiltered/blob/master/library/src/main/scala/response/types.scala

    // начальная страница (корень сайта)
    case Path(Seg(Nil)) =>
      Ok ~> PlainTextContent ~> ResponseString("Демонстрация работы фреймворка "
        + "Unfiltered для языка Scala, попробуйте сегменты в строке адреса: "
        + "plain_text, inline_html, static_html, site.css, lasto4ka.png, buggy.png")

    // простой текст
    case Path(Seg("plain_text" :: Nil)) =>
      // PlainTextContent добавит заголовок Content-Type с UTF-8:
      Ok ~> PlainTextContent ~> ResponseString("Это простой текст от сервера")
    // Можно и так, но тогда браузер не распознает UTF-8 строки:
    // Ok ~> ResponseString("Это простой текст от сервера" )

    // демо html-страницы, встроенной в код scala
    case Path(Seg("inline_html" :: Nil)) =>
      Ok ~> HtmlContent ~> Html(
        <html>
          <head>
            <title>Scala Unfiltered framework basic demo</title>
            <meta charset="UTF-8"/>
          </head>
          <body>
            <div>
              Демонстрация работы фреймворка Unfiltered для языка Scala:
            это HTML-страница, встроенная в код файла scala.
            </div>
          </body>
        </html>)

    // статическая html-страница, загружается из ресурсов
    case Path(Seg("static_html" :: Nil)) =>
      Ok ~> HtmlContent ~> ResponseString(
        scala.io.Source.fromInputStream(getClass.getResourceAsStream("/html/static_html.html"),
            "UTF-8").mkString)

    /**************************/
    // Ниже для интереса попробуем отдавать вручную разные ресурсы (css, картинки),
    // но лучше для них использовать возможности контейнеров Jetty/Netty или
    // внешний веб-сервер для раздачи статического контента типа nginx
    
    // css-файл со стилем страницы, загружается из ресурсов
    case Path(Seg("site.css" :: Nil)) =>
      Ok ~> CssContent ~> ResponseString(
        scala.io.Source.fromInputStream(
          getClass.getResourceAsStream("/public/site.css"), "UTF-8").mkString)

    // картинка, загружается из ресурсов
    case Path(Seg("lasto4ka.png" :: Nil)) =>
      val in = getClass.getResourceAsStream("/public/lasto4ka.png")

      // так в некоторых случаях может не сработать - за один раз будет считан 
      // не весь файл, а только часть, поэтому придется через ByteArrayOutputStream
      // (подробности: https://groups.google.com/forum/#!topic/unfiltered-scala/czRtn5Vnoug)
      //      val bytes = new Array[Byte](in.available)
      //      in.read(bytes)     

      val buffer = new java.io.ByteArrayOutputStream()
      val data = new Array[Byte](16384)
      var nRead = in.read(data, 0, data.length)
      while (nRead != -1) {
        buffer.write(data, 0, nRead)
        nRead = in.read(data, 0, data.length)
      }
      buffer.flush()
      val bytes = buffer.toByteArray();

      // Предопределенных классов Content для картинок в стандартной 
      // поставке не нашлось, добавим свои:
      object JpegImageContent extends ContentType("image/jpeg")
      object PngImageContent extends ContentType("image/png")

      Ok ~> PngImageContent ~> ResponseBytes(bytes)

    // другая картинка, загружается из ресурсов
    case Path(Seg("buggy.png" :: Nil)) =>
      val in = getClass.getResourceAsStream("/public/buggy.png")

      // так в некоторых случаях может не сработать - за один раз будет считан 
      // не весь файл, а только часть, поэтому придется через ByteArrayOutputStream
      // (подробности: https://groups.google.com/forum/#!topic/unfiltered-scala/czRtn5Vnoug)
      //      val bytes = new Array[Byte](in.available)
      //      in.read(bytes)     

      val buffer = new java.io.ByteArrayOutputStream()
      val data = new Array[Byte](16384)
      var nRead = in.read(data, 0, data.length)
      while (nRead != -1) {
        buffer.write(data, 0, nRead)
        nRead = in.read(data, 0, data.length)
      }
      buffer.flush()
      val bytes = buffer.toByteArray();

      // Предопределенных классов Content для картинок в стандартной 
      // поставке не нашлось, добавим свои:
      object JpegImageContent extends ContentType("image/jpeg")
      object PngImageContent extends ContentType("image/png")

      Ok ~> PngImageContent ~> ResponseBytes(bytes)

    /**************************/
    // работа с сегментами напрямую (убрать при работе с внешними ресурсами,
    // подробности: https://groups.google.com/forum/#!topic/unfiltered-scala/czRtn5Vnoug)
    case Path(Decode.utf8(Seg(path :: Nil))) =>
      Ok ~> PlainTextContent ~> ResponseString("Ваш сегмент: " + path)
  }


Запуск приложения - метод main, запускает встроенный HTTP-сервер Jetty на порте 8080 и развертывает внутри него веб-приложение

Замечание: Unfiltered умеет работать с двумя встроенными HTTP-серверами: Jetty и Netty. Для простых приложений не важно, какой из них использовать, различия могут проявиться под нагрузкой или при возникновении потребности в каких-то специфических возможностях.


  def main(args: Array[String]) {
    // Запустить контейнер веб-приложенией Jetty
    println("Starting Scala Unfiltered web framework demo on Jetty http server...")
    unfiltered.jetty.Http.apply(8080).plan(handlePath).run()
    //    println("Resources dir: " + getClass.getResource("/public"))
    //    unfiltered.jetty.Http.apply(8080).resources(getClass.getResource("/public")).plan(handlePath).run()

    // Запустить контейнер веб-приложенией Netty
    //    println("Starting Scala Unfiltered web framework demo on Netty http server...")
    //    unfiltered.netty.Http(8080).plan(handlePath).run()
  }



Запускаем, проверяем

1) Компилируем приложение (правой кнопкой на проекте, меню "Собрать" или "Очистить и собрать")
2) Запускаем приложение (правой кнопкой внутри открытого UnfilteredBasicDemo.scala, меню "Отладка файла")
3) Следим за сообщениями в окне "Отладка": видим, что HTTP-сервер запущен на порте 8080

scala-netbeans-15.png

4) Открываем любимый браузер и вводим адрес http://localhost:8080/ : видим сообщение для корневого сегмента (блок Nil).

scala-netbeans-10.png

Пробуем другие сегменты, http://localhost:8080/static_html : грузит HTML из ресурсов из файла static_html.html (здесь же грузятся картинки buggy.png и lasto4ka.png и CSS site.css)

scala-netbeans-12.png

http://localhost:8080/plain_text : обычный текст

scala-netbeans-13.png

Всё работает.

Попробуем немного JavaScript и Аякса

Основа веб-интерфейсов AJAX - это фоновый обмен данными между страницей HTML и HTTP-сервером через JavaScript. Посмотрим, как это работает.

Скачаем рядом другой проект: snippets/scala-web/ScalaUnfilteredAjaxDemo

Структура веб-приложения еще проще: одна статическая страница HTML ajax_demo и два текстовых вызова call1 и call2.

ScalaUnfilteredAjaxDemo/src/main/scala/edu/nntu/scalaunfiltereddemo/UnfilteredAjaxDemo.scala

package edu.nntu.scalaunfiltereddemo

import unfiltered.request.Path
import unfiltered.request.Seg
import unfiltered.response.Ok
import unfiltered.response.HtmlContent
import unfiltered.response.PlainTextContent
import unfiltered.response.ResponseString
import unfiltered.response.Html

/**
 * Базовый пример Http-сервера с Unfiltered
 * http://unfiltered.databinder.net/Try+Unfiltered.html
 *
 * с нормально поддержкой UTF-8 в строке запроса и в теле ответа.
 *
 */
object UnfilteredAjaxDemo {
 
  val handlePath = unfiltered.filter.Planify {
    // статическая html-страница со вставками Ajax, загружается из ресурсов
    case Path(Seg("ajax_demo" :: Nil)) =>
      Ok ~> HtmlContent ~> ResponseString(
        scala.io.Source.fromInputStream(getClass.getResourceAsStream("/html/ajax_demo.html"), "UTF-8").mkString)
     
    // rest-вызов 1: возвращает простой текст
    case Path(Seg("call1" :: Nil)) =>
      Ok ~> PlainTextContent ~> ResponseString("Это текст от сервера - результат вызова 1")
    // rest-вызов 2: возвращает простой текст
    case Path(Seg("call2" :: Nil)) =>
      Ok ~> PlainTextContent ~> ResponseString("Это текст от сервера - результат вызова 2")
  }

  def main(args: Array[String]) {
    println("Starting jetty http server demo...")

    // Запустить веб-сервер
    unfiltered.jetty.Http.apply(8080).filter(handlePath).run()
  }
}


В страницу HTML встроен простой код JavaScript, который при нажатии ссылок "Прочитать с сервера значение 1" и "Прочитать с сервера значение 2" делает фоновые HTTP-запросы call1 и call2, результат прописывает в элемент "call_res"

ScalaUnfilteredAjaxDemo/src/main/resources/html/ajax_demo.html

<!DOCTYPE html>
<html>
    <head>
        <title>Scala Unfiltered framework basic demo</title>
        <meta charset="UTF-8"/>
        <script type="text/javascript">
            function readServerString(elemRenderer, url) {
                var req = new XMLHttpRequest();
                req.onreadystatechange = function() {
                    if (req.readyState === 4) { // only if req is "loaded"
                        if (req.status === 200) { // only if "OK"
                            document.getElementById(elemRenderer).innerHTML = req.responseText;
                        } else {
                            // error
                        }
                    }
                };
                // can't use GET method here as it would quickly 
                // exceede max length limitation
                req.open("POST", url, true);

                //Send the proper header information along with the request
                req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                req.send();
            }

            function call1() {
                readServerString("call_res", "/call1");
            }

            function call2() {
                readServerString("call_res", "/call2");
            }

        </script>
    </head>
    <body>
        <p> Демонстрация работы фреймворка Unfiltered для языка Scala:
            это HTML-страница с демонстрацией Ajax.
        </p>
        <p>
            <a href="javascript:call1()">Прочитать с сервера значение 1</a>,
            <a href="javascript:call2()">прочитать с сервера значение 2</a>
        </p>
        <p>
            Результат: <span id="call_res" style="font-style: italic"></span>
        </p>
    </body>
</html>


Компилируем, запускаем, открываем в браузере http://localhost:8080/ajax_demo, кликаем на ссылки,

scala-netbeans-21.png

видим меняющийся результат.

scala-netbeans-22.png



Третья часть о том, как развернуть веб-приложение в облаке.

исходники занятия, подсветка синтаксиса.
Tags: облако, сервер роботов, типовые задачи
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment