Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью Scala» .
В этом курсе мы предоставляем среду и набор инструментов, чтобы вы могли разрабатывать современные приложения Scala. Мы охватываем широкий спектр тем: от сборки SBT и реактивных приложений до тестирования и доступа к базе данных. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !
1. Введение
Прошло много времени с тех пор, как Интернет стал доминирующей, универсально и глобально доступной платформой для множества различных приложений: веб-сайтов, веб-порталов или веб-API. Начинаясь как простой набор статических HTML-страниц, веб-приложения стремительно догоняли своих настольных аналогов, превращаясь в новый класс того, что мы привыкли называть многофункциональными интернет-приложениями (или просто RIA ). Однако большинство таких достижений было бы невозможно без эволюции (а в некоторых случаях и революции) веб-браузеров.
В этом разделе руководства мы поговорим о разработке богатых веб-сайтов и порталов (или, проще говоря, современных веб-приложений) с использованием языка программирования и экосистемы Scala .
Содержание
- 1. Введение
- 2. MVC и сила паттернов
- 3. Play Framework: приятный и продуктивный
- 4. Контроллеры, действия и маршруты
- 5. Представления и шаблоны
- 6. Композиции действий и фильтры
- 7. Доступ к базе данных
- 8. Использование Akka
- 9. WebSockets
- 10. Отправленные сервером события
- 11. Запуск приложений Play
- 12. Безопасный HTTP (HTTPS)
- 13. Тестирование
- 14. Выводы
- 15. Что дальше
2. MVC и сила паттернов
Существует множество способов приблизиться к проектированию и разработке веб-приложений. Тем не менее, пара шаблонов появилась из толпы и получила широкое распространение в сообществе разработчиков программного обеспечения.
Model-View-Controller (или MVC ) является одним из наиболее широко применяемых архитектурных шаблонов, используемых для разработки поддерживаемых приложений на основе пользовательского интерфейса. Это очень просто, но обеспечивает вполне достаточный уровень разделения интересов и обязанностей.
По сути, MVC очень хорошо описывает сотрудников и их роли. View
использует Model
для визуализации представления рабочего стола или веб-интерфейса для User
. User
взаимодействует с View
, что может привести к обновлению (или поиску) модели посредством использования Controller
. В свою очередь, действия Controller's
над Model
могут привести к обновлению представления. В некоторых случаях User
может взаимодействовать с Controller
напрямую, полностью обходя View
.
Многие фреймворки, используемые в наши дни для разработки веб-приложений, разработаны на основе шаблона MVC или одного (или нескольких) его производных. В экосистеме Scala , Play Framework, несомненно, является наилучшим из доступных вариантов, и именно об этом мы и поговорим в этом разделе руководства.
3. Play Framework: приятный и продуктивный
Play Framework — это современный, готовый к работе, высокоскоростной, полнофункциональный веб-фреймворк, написанный на Scala (с доступным для Java API). Он спроектирован так, чтобы быть полностью асинхронным, легким и не сохраняющим состояние, и построен на основе Akka Toolkit , который мы подробно обсудили в предыдущем разделе учебника. Последняя выпущенная версия Play Framework на момент написания этой статьи — 2.5.9
.
Хотя Play Framework не ограничивается поддержкой только разработки веб-приложений, мы сосредоточимся в основном на этой стороне вещей, продолжая обсуждение веб-API в следующем разделе, посвященном именно этому.
4. Контроллеры, действия и маршруты
Play Framework полностью охватывает модель MVC и с самого начала представляет концепцию контроллеров . Следуя своим обязанностям, контроллеры могут генерировать некоторые действия , возвращая некоторые результаты, например:
1
2
3
4
5
6
|
@ Singleton class HealthController extends Controller { def check() = Action { Ok( "OK" ) } } |
Обратите внимание, что по соглашению контроллеры хранятся в пакете controllers
. Методы контроллера могут быть представлены непосредственно как конечные точки HTTP , используя семантику протокола HTTP . В Play Framework такое отображение называется route
и все определения маршрутов помещаются в файл conf/routes
route, например:
1
2
|
GET /health controllers.HealthController.check GET /assets/*file controllers.Assets.versioned(path = "/public" , file : Asset) |
Пусть простота этого примера не обманет вас, определения маршрута Play Framework могут включать произвольное количество довольно сложных параметров и шаблонов URI , как мы увидим позже в этом разделе.
Контроллеры в Play Framework расширяют черту Controller и могут содержать любое (разумное) количество методов, которые возвращают экземпляры Action . Все действия выполняются асинхронно, неблокирующим образом, и это очень важно помнить. Контроллеры должны по возможности избегать выполнения операций блокировки, а объект-компаньон Action предлагает удобное семейство асинхронных методов для бесшовной интеграции с потоками асинхронного выполнения, например:
01
02
03
04
05
06
07
08
09
10
|
@ Singleton class UserController @ Inject() ( val service : UserService) extends Controller { import play.api.libs.concurrent.Execution.Implicits.defaultContext def getUsers = Action.async { service.findAll().map { users = > Ok(views.html.users(users)) } } } |
Помимо асинхронности, этот короткий фрагмент кода также показывает, как контроллеры представляют еще одного сотрудника MVC , view
model
. Давайте поговорим об этом на минуту.
5. Представления и шаблоны
Представления в Play Framework обычно основаны на обычной HTML- разметке, но поддерживаются Twirl , чрезвычайно мощным механизмом шаблонов на основе Scala . Под капотом шаблоны преобразуются в классы Scala вместе с сопутствующими объектами. Они могут иметь аргументы и скомпилированы как стандартный код Scala .
Давайте кратко рассмотрим, как можно передать и отобразить model
в шаблоне представления Play Framework , введя класс case User
:
1
2
|
case class User(id : Option[Int], email : String, firstName : Option[String], lastName : Option[String]) |
Если это выглядит знакомо, вы не ошибаетесь: это точно такой же класс наблюдений, который мы видели в разделе « Доступ к базе данных с помощью Slick » этого руководства. Итак, давайте создадим шаблон users.scala.html
для распечатки списка пользователей в виде HTML- таблицы, которая по соглашению хранится в папке представлений (которая также служит именем пакета):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@ (users : Seq[model.User]) <!DOCTYPE html> <html lang = "en" > <head> <title>Manage Users</title> </head> <body> <div class = "container" > <div class = "panel panel-default" > <div class = "panel-heading" >Users</div> <table class = "table" > <thead> <tr> <th>Id</th> <th>First Name</th> <th>Last Name</th> <th>Email</th> </tr> </thead> @ for (user <- users) { <tr> <td> @ user.id</td> <td> @ user.firstName</td> <td> @ user.lastName</td> <td> @ user.email</td> </tr> } </table> </div> </div> </body> </html> |
По большому счету, это просто необработанная HTML- разметка, которая так близка сердцу любого фронтенд-разработчика. Первая строка объявляет аргументы шаблона, @(users: Seq[model.User])
который является просто списком пользователей. Единственное место, где мы используем этот аргумент, это когда вы визуализируете строки таблицы с помощью выражений, подобных Scala :
1
2
3
4
5
6
7
8
|
@ for (user <- users) { <tr> <td> @ user.id</td> <td> @ user.firstName</td> <td> @ user.lastName</td> <td> @ user.email</td> </tr> } |
Вот и все! А поскольку все шаблоны скомпилированы в байт-код, любые ошибки, связанные, например, с несуществующими свойствами или использованием неуместных выражений, будут обнаружены во время компиляции! С такой помощью компилятора любой рефакторинг становится намного проще и безопаснее.
Чтобы закрыть цикл, в разделе « Контроллеры, действия и маршруты » мы уже видели, как создавать экземпляры шаблонов и отправлять их в браузер с помощью действия контроллера:
1
2
3
4
5
|
def getUsers = Action.async { service.findAll().map { users = > Ok(views.html.users(users)) } } |
Помимо простых компонентов, шаблоны могут включать в себя HTML- формы, которые могут поддерживаться непосредственно методами контроллера. В качестве примера, давайте реализуем добавление новых пользовательских функций, таких как вид проводки, контроллер и модель вместе. Во-первых, на стороне контроллера мы должны добавить определение Form
, включая все ограничения проверки:
1
2
3
4
5
6
7
8
|
val userForm = Form( mapping( "id" -> ignored[Option[Int]](None), "email" -> email.verifying( "Maximum length is 512" , _ .size < = 512 ), "firstName" -> optional(text(maxLength = 64 )), "lastName" -> optional(text(maxLength = 64 )) )(User.apply)(User.unapply) ) |
Во-вторых, мы собираемся создать выделенную конечную точку в контроллере, чтобы добавить нового пользователя в результате отправки формы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
def addUser = Action.async { implicit request = > userForm.bindFromRequest.fold( formWithErrors = > { service.findAll().map { users = > BadRequest(views.html.users(users)(formWithErrors)) } }, user = > { service.insert(user).map { user = > Redirect(routes.UserController.getUsers) } recoverWith { case _ = > service.findAll().map { users = > BadRequest(views.html.users(users)(userForm .withGlobalError(s "Duplicate email address: ${user.email}" ))) } } } ) } |
Обратите внимание, как userForm.bindFromRequest.fold
за один раз выполняет userForm.bindFromRequest.fold
параметров формы из запроса вместе с выполнением всех проверок валидации. Следующее, мы должны скопировать userForm
в его HTML- презентацию, используя для этого шаблон представления:
1
2
3
4
5
6
|
@ helper.form(action = routes.UserController.addUser) { @ helper.inputText(userForm( "email" ), '_label -> "Email Address") @helper.inputText(userForm("firstName"), ' _ label -> "First Name" ) @ helper.inputText(userForm( "lastName" ), ' _ label -> "Last Name" ) <button class = "btn btn-default" type = "submit" >Add User</button> } |
Использование @helper
упрощает построение многих форм, так как все соответствующие типы и ограничения проверки будут взяты из определения Form
и намекаются пользователю. Но, как и везде в Play Framework , вы не обязаны использовать этот подход: вместо этого можно использовать любой JavaScript / CSS-фреймворк по вашему выбору. Вот краткий обзор того, как эта форма выглядит в браузере.
И в качестве последнего шага, таблица маршрутизации должна быть обновлена, чтобы также была указана эта новая конечная точка. К счастью, это всего лишь один лайнер:
1
|
POST / controllers.UserController.addUser |
Обратите внимание, что леса Play Framework позаботятся обо всей проверочной части и сообщат о ней обратно, например, отправка формы с неверным адресом электронной почты не будет принята:
Конечно, Play Framework предоставляет достаточно гибкости, чтобы сообщать о других типах ошибок, не обязательно связанных с проверкой. Например, в контроллере мы использовали метод userForm.withGlobalError
для сигнализации дублированного адреса электронной почты.
6. Композиции действий и фильтры
Часто возникает необходимость выполнить некоторые действия до или после вызова метода контроллера, возможно, даже изменить ответ. Примерами этого могут быть регистрация, проверки безопасности или поддержка совместного использования ресурсов между источниками (CORS).
В стандартной комплектации Play Framework предоставляет несколько способов проникновения в конвейер обработки: использование фильтров и композиций действий .
7. Доступ к базе данных
Любое более или менее реальное веб-приложение должно было бы управлять некоторыми данными, и во многих случаях известные хранилища данных отношений являются идеальным выбором. Play Framework предлагает превосходную интеграцию с парой библиотек на основе JDBC, но мы уже узнали много хорошего о Slick и, конечно, Play Framework легко интегрируется с Slick .
Чтобы сделать вещи еще проще и привычнее, мы собираемся повторно использовать ту же модель данных, которую мы создали в компоненте Database Access с Slick , без каких-либо изменений. Незначительное изменение, которое нам нужно сделать, UserRepository
только UserRepository
: внедрение DatabaseConfigProvider
для конфигурации базы DatabaseConfigProvider
по умолчанию и использование его метода provider.get[JdbcProfile]
для получения соответствующего экземпляра JdbcProfile
.
01
02
03
04
05
06
07
08
09
10
|
@ Singleton class UserRepository @ Inject() ( val provider : DatabaseConfigProvider) extends HasDatabaseConfig[JdbcProfile] with UsersTable { val dbConfig = provider.get[JdbcProfile] import dbConfig.driver.api. _ import scala.concurrent.ExecutionContext.Implicits.global ... } |
И мы сделали. Play Framework позволяет управлять несколькими именованными экземплярами базы данных и настраивать их через файл application.conf
. Для удобства веб-приложения с одной базой данных могут использовать специально обработанное приложение по default
.
01
02
03
04
05
06
07
08
09
10
11
|
slick { dbs { default { driver= "slick.driver.H2Driver$" db { driver= "org.h2.Driver" url= "jdbc:h2:mem:users;DB_CLOSE_DELAY=-1" } } } } |
Одной из наиболее распространенных проблем, с которыми каждый раз сталкиваются разработчики приложений при работе с реляционными базами данных, является управление схемами. Модель данных со временем эволюционирует, как и база данных: часто добавляются новые таблицы, столбцы и индексы, удаляются неиспользуемые. Развитие базы данных — еще одна потрясающая функция, которую Play Framework предоставляет из коробки.
8. Использование Akka
Play Framework находится на базе Akka Toolkit, и в качестве таких актеров они являются первоклассными гражданами. Любое веб-приложение Play Framework имеет выделенную систему актеров, созданную непосредственно при запуске приложения (и автоматически перезапускается при перезапуске приложения).
Платформа Play Framework полностью охватывает реактивную парадигму с самого начала и является одним из первых разработчиков внедрения Akka Streams . Более того, Play Framework предоставляет довольно много полезных служебных классов для объединения Akka Streams с технологиями, специфичными для веб-приложений, о которых мы поговорим.
9. WebSockets
Возможно, WebSockets являются одним из самых интересных и быстро распространяющихся коммуникационных протоколов в наши дни. По сути, WebSockets выводит взаимодействия веб-клиент / веб-сервер на следующий уровень, обеспечивая полнодуплексный канал связи, установленный по протоколу HTTP.
Чтобы представить WebSockets в перспективе реальных приложений, давайте реализуем функцию отображения уведомлений в нашем веб-приложении при каждом добавлении нового пользователя. Внутри этот факт представлен событием UserAdded
.
1
|
case class UserAdded(user : User) |
Используя поток событий Akka , о котором мы говорили в предыдущей части урока, мы можем подписаться на это событие, создав отдельного актера, назовем его UsersWebSocketActor
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class UsersWebSocketActor( val out : ActorRef) extends Actor with ActorLogging { override def preStart() = { context.system.eventStream.subscribe(self, classOf[UserAdded]) } override def postStop() = { context.system.eventStream.unsubscribe(self) } def receive() = { case UserAdded(user) = > out ! user } } |
Это выглядит исключительно просто, но таинственное упоминание актера. Давайте посмотрим, откуда это. Play Framework всегда имел превосходную поддержку WebSockets , однако более тесная интеграция с Akka Streams сделала его намного лучше . Вот конечная точка WebSockets для передачи клиенту уведомлений о новых пользователях.
1
2
3
|
def usersWs = WebSocket.accept[User, User] { request = > ActorFlow.actorRef(out = > Props( new UsersWebSocketActor(out))) } |
Это всего лишь несколько строк кода для такой сложной функции, действительно удивительно! Обратите внимание, что ссылка out
actor по существу представляет собой сторону веб-клиента и предоставляется Play Framework из коробки. Изменения в таблице маршрутизации также минимальны.
1
|
GET /notifications/users controllers.NotificationController.usersWs |
На стороне браузера это стандартный фрагмент кода JavaScript, который можно вставить прямо в шаблон представления.
1
2
3
4
5
6
7
8
9
|
<script type= "text/javascript" > var socket = new WebSocket( "@routes.NotificationController.usersWs().webSocketURL()" ) socket.onmessage = function (event) { var user = jQuery.parseJSON(event.data); ... } </script> |
Как уже упоминалось, WebSockets являются двунаправленным каналом связи: не только веб-сервер может отправлять данные веб-клиенту, но и веб-клиент может инициировать некоторые сообщения. Мы не рассмотрели эту часть в приведенном здесь примере, но в документации Play Framework это подробно обсуждается.
10. Отправленные сервером события
WebSockets являются чрезвычайно мощными, но часто потребности веб-приложений могут быть подкреплены гораздо более простой реализацией. В случае, если полнодуплексный канал не требуется, веб-сервер может полагаться на отправленные сервером события (или SSE ), чтобы просто отправлять данные веб-клиенту односторонним способом. В Play Framework (и во многих других платформах) он реализован путем поддержки фрагментированных (или потоковых) ответов со специальным типом контента text/event-stream
.
1
2
3
4
5
|
def usersSse = Action { Ok.chunked( Source.actorPublisher(Props[UsersSseActor]) via EventSource.flow[User] ).as(ContentTypes.EVENT _ STREAM) } |
В этом случае сервер может использовать полноценные конвейеры обработки данных Akka Streams и доставлять данные клиенту, используя скаффолдинг EventSource . Чтобы проиллюстрировать еще одну интересную особенность Akka Streams , мы используем в качестве источника потока актер UsersSseActor
, функционально аналогичный UsersWebSocketActor
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class UsersSseActor extends ActorPublisher[User] with ActorLogging { var buffer = Vector.empty[User] override def preStart() = { context.system.eventStream.subscribe(self, classOf[UserAdded]) } override def postStop() = { context.system.eventStream.unsubscribe(self) } def receive = { case UserAdded(user) if buffer.size < 100 = > { buffer : + = user send() } case Request( _ ) = > send() case Cancel = > context.stop(self) } private [ this ] def send() : Unit = if (totalDemand > 0 ) { val (use, keep) = buffer.splitAt(totalDemand.toInt) buffer = keep use foreach onNext } } |
Это немного сложнее из-за того, что мы должны следовать соглашению и API Akka Streams, чтобы иметь хорошего издателя, но по сути он также использует поток событий для подписки на уведомления. Опять же, со стороны, просто голый JavaScript:
1
2
3
4
5
6
7
8
9
|
<script type= "text/javascript" > var event = new EventSource( "@routes.NotificationController.usersSse().absoluteURL()" ); event.addEventListener( 'message' , function (event) { var user = jQuery.parseJSON(event.data); ... }); </script> |
И не забыть добавить еще одну запись в таблицу маршрутизации:
1
|
GET /notifications/sse controllers.NotificationController.usersSse |
11. Запуск приложений Play
Есть несколько способов запустить наше приложение Play Framework , но, вероятно, самый простой — использовать инструмент sbt , с которым мы уже хорошо знакомы:
1
|
sbt run |
Однако лучший способ, все еще используя sbt , — запустить приложение в непрерывном цикле edit-compile- (re) deploy, чтобы (в основном) мгновенно отразить изменения в исходных файлах:
1
|
sbt ~run |
По умолчанию каждое приложение Play работает на HTTP-порту 9000 , поэтому вы можете свободно переходить в браузере по адресу http: // localhost: 9000, чтобы играть с пользователями, или по адресу http: // localhost: 9000 /, чтобы просматривать WebSockets и отправленные сервером. события в действии.
12. Безопасный HTTP (HTTPS)
Использование безопасного HTTP ( HTTPS ) в рабочей среде является обязательным правилом для современных веб-сайтов и порталов в наши дни. Но очень часто во время разработки необходимо запускать приложение Play Framework с поддержкой HTTPS . Обычно это делается путем создания самозаверяющих сертификатов и их импорта в хранилище ключей Java , которое находится всего в одной команде:
01
02
03
04
05
06
07
08
09
10
11
|
keytool -genkeypair - v - alias localhost -dname "CN=localhost" -keystore conf /play-webapp .jks -keypass changeme -storepass changeme -keyalg RSA -keysize 4096 -ext KeyUsage:critical= "keyCertSign" -ext BasicConstraints:critical= "ca:true" -validity 365 |
conf/play-webapp.jks
ключей conf/play-webapp.jks
можно использовать для настройки приложения Play Framework для работы с поддержкой HTTPS , например:
1
|
sbt run -Dhttps.port=9443 -Dplay.server.https.keyStore.path=conf /play-webapp .jks -Dplay.server.https.keyStore.password=changeme |
Теперь мы можем перейти к https: // localhost: 9443 /, чтобы получить список пользователей (того же, который мы увидим, используя http: // localhost: 9000 / ). Очень просто и легко, не правда ли?
13. Тестирование
В мире веб-приложений у тестирования есть много форм и граней, но Play Framework делает действительно хорошую работу, предоставляя необходимые леса для их упрощения. Более того, обе платформы ScalaTest и specs2 поддерживаются одинаково.
Вероятно, самый простой и быстрый способ приблизиться к тестированию в Play Framework — использовать модульные тесты. Например, давайте посмотрим на этот набор specs2 для тестирования методов UserController
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class UserControllerUnitSpec extends PlaySpecification with Mockito { "UserController" should { "render the users page" in { val userService = mock[UserService] val controller = new UserController(userService) userService.findAll() returns Future.successful(Seq( val result = controller.getUsers()(FakeRequest()) status(result) must equalTo(OK) } } } |
Полезные леса Play Framework для интеграции specs2 делают написание модульных тестов так же легко, как дышать. Поднявшись на один уровень в тестировании пирамиды , нам, возможно, придется подумать о написании интеграционных тестов, и в этом случае Play Framework лучше использовать со ScalaTest из-за выделенных дополнительных функций, предоставляемых из коробки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class UserControllerSpec extends PlaySpec with OneAppPerTest with ScalaFutures { "UserController" should { "render the users page" in { val userService = app.injector.instanceOf[UserService] Some( "Tom" ), Some( "Tommyknocker" )))) { user = > user.id mustBe defined } val users = route(app, FakeRequest(GET, "/" )).get status(users) mustBe OK contentType(users) mustBe Some( "text/html" ) .and(include ( "Tommyknocker" )) } } } |
В этом случае создается полноценное приложение Play Framework , включая экземпляр базы данных, настроенный со всеми примененными эволюциями. Однако, если вы хотите максимально приблизиться к реальному развертыванию, вы можете рассмотреть возможность добавления тестовых случаев веб-интерфейса (с браузером или без него), и опять же, интеграция ScalaTest предлагает необходимые компоненты .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class UserControllerBrowserSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite with HtmlUnitFactory { "Users page" must { "should show emtpy users" in { pageTitle mustBe "Manage Users" submit() eventually { pageTitle mustBe "Manage Users" } find(xpath( ".//*[@class='table']/tbody/tr[1]/td[4]" )) map { } } } } |
Эта хорошо известная стратегия тестирования основана на автоматизации веб-браузера Selenium, которая красиво встроена в OneBrowserPerSuite вместе с HtmlUnitFactory без браузера.
14. Выводы
Без сомнения, Play Framework возвращает удовольствие и радость от разработки веб-приложений на платформе JVM. С Scala и Akka в своей основе, современным, чрезвычайно многофункциональным и продуктивным, построенным на основе парадигмы реактивного программирования , все это делает Play Framework выбором, о котором вы никогда не пожалеете. Не забывайте, что отличное разделение между внешним интерфейсом и внутренним интерфейсом позволяет объединить лучшие части двух миров, что приводит к созданию красивых и поддерживаемых веб-приложений.
15. Что дальше
В следующем разделе руководства мы поговорим о разработке веб-API REST (ful) с использованием HTTP- модуля Akka .
Полный исходный код доступен для скачивания.