Вебчик и Scala (via Play! Framework)
Введение
“Не консолью единой живы люди” - подумал я и решил, что надо попробовать наваять простое веб-приложение на Play! Framework. Play! Framework - это MVC фреймворк для создания веб-приложений на Java/Scala. На первый (да и на второй, если честно) взгляд кажется довольно простым и ничем особо не отличающимся от других подобных фреймворков. Однако, есть мнение, что для версии 2.x надо брать на вооружение Scala, а для Java лучше подойдет более взрослая версия 1.x.
Задача
Свой небольшой эксперимент я решил провести со Scala и Play! версии 2.1, который идет в комплекте с IDE от JetBrains IntelliJ IDEA. Удобным является то, что запустить приложение можно прямо в IDEA. Если вы еще не установили плагин для Scala - самое время это сделать. Теперь можно создать приложение из шаблона Play 2.x.
Play! Framework
При создании проекта в IDE будет создана базовая структура проекта с необходимым минимумом для приложения: по одному контроллеру и представлению. Модели в шаблон приложения не завезли. Так же для создания скелета приложения можно исспользовать activator. Что радует - активатор содержит в себе полноценную IDE выполнив команду activator ui. На самом деле, для простого приложения этого более чем достаточно. А делать ничего сильно серьезного и не планировали: просто перенесем предыдущее приложение в веб. Вкратце: приложение получает на вход поисковый запрос к сайту stackoverflow.com и возвращает список вопросов подходящих этому запросу.
Представление
Представления в Play имеют расширение *.scala.html и, по сути, являются такими же функциями и могут вызываться одно из другого:
<footer>
Powered by Play Framework
</footer>
@(title: String)(content: => Html)(implicit flash: Flash)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
</head>
<body>
<section class="content">@content</section>
@footer()
</body>
</html>
Есть возможность создать “базовое” представление (по аналогии с Master Page в
ASP.NET WebForms или Shared Layout в ASP.NET MVC), которое будет расширяться
конкретными представлениями. Например, у есть представление main.scala.html
:
@(title: String)(content: Html)
<html>
<head>
<title>@title</title>
</head>
<body>
@content
</body>
</html>
В представлении index.scala.html
мы можем написать следующий код:
@main("Result page") {
<h1>Hi there!</h1>
}
На деле всё это соберется во что-то похожее на следующий кусок:
<html>
<head>
<title>Result page</title>
</head>
<body>
<h1>Hi there!</h1>
</body>
</html>
Контроллеры
Контроллеры располагаются в модуле controllers. Каждый контроллер должен
наследоваться от класса play.api.mvc.Controller
. Конфигурирование путей
(routs) необходимо производить в файле /conf/routes
. Например, строка
GET /Search controllers.Application.search(query: String)
Значит, что при обращении к адресу http://host:port/Search?query=scala
будет
выполняться GET
запрос к action
-у search
контроллера Application
с
параметром query
равным scala
. Подробнее можно почитать
здесь.
Модели
Модель - обычный класс, который, по своей сути, являются классом, описывающими бизнес-объекты области.
Приложение
Так как мы делаем веб-приложение, то возможностей у нас больше: будет
отображаться не просто список заголовков вопросов. Заголовки будут являться
ссылками на сами вопросы на StackOverflow. Так же добавим в список вопросов
автора вопроса со ссылкой на его профиль. Приложение будет включать три
представления: index.scala.html
(“главная” страница нашего сайта),
search.scala.html
(страница с поиском и результатами поиска) и мастер-
представление main.scala.html
. В приложении будет так же один контроллер
Application.scala
, который будет содержать всю логику и модель
QuestionModel.scala
, содержащая описание вопроса. Контроллер содержит два
action
-а: index
и search
(соответсвенно используют index.scala.html
и
search.scala.html
). Представление и action
index
будет отображаться,
если к приложению обращаются по адресу http://host:port/
. Если обращаются по
адресу http://host:port/search?query=scala
, то будет вызываться action
search
с параметром из адресной строки q
. В приложении используются
библиотеки json4s для работы с JSON и
scalaj-http для выполнеия запросов к
API StackOverflow. Логика получения ответов от API StackOverflow ничем не
отличается от предыдущего
приложения:
val url = "https://api.stackexchange.com/2.2/search?order=desc&sort=activity&site=stackoverflow&intitle=" +
java.net.URLEncoder.encode(query, "UTF-8")
// response будет содержать ответ от API StackOverflow
val response: HttpResponse[String] = Http(url).asString
Для того, чтобы ответ корректно десериализовался необходимо создать несклько
вспомогательных case
-классов: Questions
, представляющий собой список всех
вопросов; Question
- один вопрос; User
- автор вопроса:
case class Questions(items: List[Question])
case class Question(answer_count: Int,
creation_date: String,
is_answered: Boolean,
last_activity_date: Long,
link: String,
owner: User,
question_id: Long,
score: Integer,
tags: List[String],
title: String,
view_count: Integer)
case class User (display_name: String,
link: String,
profile_image: String,
reputation: Integer,
user_id: Long,
user_type: String)
Данные классы написаны на основе ответа от API StackOverflow:
{"items":[{"tags":["java","scala","maven","apache-spark"],"owner":{"reputation":1019,"user_id":317027,"user_type":"registered","accept_rate":69,"profile_image":"https://www.gravatar.com/avatar/de811098d6fa6c11aa0be27262cd1796?s=128&d=identicon&r=PG","display_name":"hba","link":"http://stackoverflow.com/users/317027/hba"},"is_answered":false,"view_count":16,"answer_count":0,"score":-1,"last_activity_date":1449080010,"creation_date":1449080010,"question_id":34050028,"link":"http://stackoverflow.com/questions/34050028/scala-test-maven-plugin-multiple-spark-context-exception","title":"Scala Test Maven Plugin - Multiple Spark Context Exception"}]}
json4s
позволяет просто распарсить ответ в виде JSON-строки к case
-классу:
// Требуется для корректной сериализации JSON.
implicit val formats = Serialization.formats(NoTypeHints)
// Десериализация ответа StackOverflow к объекту Questions
val output = org.json4s.jackson.Serialization.read[Questions](response.body)
Представление search.scala.html
ожидает на вход объект класса
scala.collection.immutable.List[models.QuestionModel]
(об этом чуть позже).
Для этого преобразуем наш объект класса Questions
к
scala.collection.immutable.List[models.QuestionModel]
и вернем его в
представление search.scala.html
:
Ok(views.html.search(output.items.map(q =>
new models.QuestionModel(q.title, q.link, q.is_answered, q.owner.display_name, q.owner.link))))
Класс QuestionModel
не содержит никакой логики:
package models
class QuestionModel(t: String, l: String, a: Boolean, on: String, ol: String) {
var title: String = t
var link: String = l
var is_answered: Boolean = a
var owner_name: String = on
var owner_link: String = ol
}
Приложеие содержит три представления: - main.scala.html
- “базовое”
представление - index.scala.html
- стартовая страница приложения -
search.scala.html
- страница отображения результатов поиска На каждой
странице приложения должна быть возможность поиска вопросов. Таким образом,
форму поиска вопросов можно вынести в представление main.scala.html
. Так же
каждая страница должна иметь стандартную html разметку (теги <html>
,
<head>
и т.п.). Следовательно, их тоже можно вынести в это представление:
@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="screen" href="assets/stylesheets/main.css">
<link rel="shortcut icon" type="image/png" href="assets/images/favicon.png">
</head>
<body>
<div class="center">
<form action="Search" method="GET">
<input id="query" name="query" type="text" />
<input id="search" type="submit" value="search"/>
</form>
</div>
@content
</body>
</html>
В представлении index.scala.html
означим заголовок страницы и значение тега
title
:
@(message: String)
<h1>@message</h1>
@main("Welcome to SO searcher") { }
Строка @(message: String)
значит, что в это представление можно передать
строку, которой потом можно будет воспользоваться в представлении. Что и
делаем в строке <h1>@message</h1>
. Строка @main("Welcome to SO searcher") { }
вызывает представление main.scala.html
с первым параметром Welcome to SO searcher
и пустым вторым параметром. Т.е. в итоге эти два представления
сольются в одну страницу. Представление search.scala.html
имеет чуть более
сложную структуру. В этом представлении требуется отображать резульататы
поискового запроса. К счастью, как говорилось выше, представления в Play!
являются Scala
-функциями и позволяются использовать Scala
-код (правда с
некоторыми ограничиениями). Нам достаточно базовой функциональсти
предоставляемой шаблонизатором Play!
: проход по циклу и формирование
разметки маркированного списка с примененем соответствующего класса, если
вопрос содержит ответ:
<ul>
@for(item <- result) {
<li>
<a @if(item.is_answered) { class='answered' } href="@item.link">@item.title</a> from <a href="@item.owner_link">@item.owner_name</a>
</li>
}
</ul>
Заключение
В целом впечатление Scala
как о языке для веб-разработки осталось двоякое: с
одной стороны, довольно приятный синтаксис Scala
, но, с другой стороны,
стандартный шаблонизатор Play!
иной раз выводил из себе непонятными
сообщениями об ошибках. Возможно, всё это можно решить. Но пока, пожалуй, не
буду советовать бросать всё и начинать разрабатывать на связке Play! Framework
и Scala
. Итоговый проект можно найти на
github По мотивам: -
[Scala - Working with REST service calls and handling
JSON](http://krishnabhargav.github.io/scala,/how/to/2014/06/15/Scala-Json-
REST-Example.html) - [Working With JSON in Scala USsing The JSON4S Library
(Part Two)](https://nosqlnocry.wordpress.com/2015/06/23/working-with-json-in-
scala-using-the-json4s-library-part-two/) - Scala
Templats