Play framework + Scala — from zero to hero
В наше время набирают популярность приложения, которые выполняются в браузере, или на мобильных платформах. Надо признать, что выбор у современных программистов огромен, даже если рассматривать только лишь программирование сайтов или под телефоны. Что из этого перспективнее — тема далеко не одной статьи, поэтому не будем разводить холивар. Сегодня мы поговорим о выборе серверной технологии для своего сайта. Если вы не боитесь изучать новое — добро пожаловать под кат.
Итак. Вы собрались создать сайт. Сайт, которым будут пользоваться миллионы. У Вас есть потрясающая, оригинальная идея. Конечно же, Вы уже знакомы с веб программированием, возможно даже очень хорошо знакомы. Но Вы же знаете, что от технологий зависит успешность Вашего проекта, и Вы очень боитесь прогадать. Уже второй день вы сидите возле монитора, сравнивая технологии, читая комментарии, смотря презентации. Вы не можете ни есть, ни спать. Именно для Вас я написал эту статью. Что же я порекомендую вам? А вот что: Play framework 2.1.* + Scala.
История
Итак, давайте же посмотрим, что из себя представляет play.
Начало
Прошу простить меня пользователей windows, но так как у меня debian, я буду предоставлять решения для пользователей unix. Надеюсь, вы разберетесь.
У нас все готово, можно приступать к созданию приложения. В этом уроке мы с вами создадим простенькое todo-приложение.
$ mkdir playapps && cd playapps $ play new todolist
Теперь пару слов о среде разработки. Вы можете пользоваться любой IDE, или любимым текстовым редактором. Я буду пользоваться intellij idea.
$ cd todolist $ play [todolist] $ run
Мы запустили наше приложение в режиме разработки (development mode). Перейдя по адресу localhost:9000. Если вы все правильно сделали, то должны увидеть вот это:
Введение в Play и в Scala (частично)
Пора бы посмотреть, что было сгенерировано командой «play new todolist»
Если вы используете idea, то остановите сервер (Ctrl + D), и введите
Теперь, когда все нужные модули для idea созданы, мы можем открыть наш проект в IDE (этот шаг только для пользователей Intellij Idea).
Мы видим структуру проекта:
Самый важные для нас папки, это app (где лежат все компоненты MVC), и conf (с конфигом приложения и routes). Если вы уже знакомы с MVC, то можете немножко поэкспериментировать. Но для начала разберемся, почему, когда мы заходим на localhost:9000, то видим не пустую страницу.
Зайдем в conf/routes, видим там:
# Home page GET / controllers.Application.index
package controllers import play.api._ import play.api.mvc._ object Application extends Controller < def index = Action < Ok(views.html.index("Your new application is ready.")) >>
Что же это за магическая строчка в def index, которая позволяет нам увидеть страницу, полную информации. Давайте розбираться. Но для начала хочу объяснить (для тех, кто не знаком с Scala). Запись типа
означает, что функция возвращает Action (это часть фреймворка, не будем углубляться; просто скажу, что Action обрабатывает запрос, и отправляет ответ в браузер). Да, и еще. Как вы уже успели заметить, в Scala точки с запятой необязательны (только если вы записываете несколько выражений в одну строку, например:
if(a>b) var c =5; var d=15 else var f = 10
Ok(views.html.index("Your new application is ready."))
Первое: каждое выражение в скале имеет свое значение. Так функция index возвращает значение этой единственной строки.
Второе: разберем саму строку. Функция «Ok» создает «200 OK» ответ, заполненный html контентом, из view`а под названием index.scala.html (дефолтные views в play на Scala template language; если вы хотите — можете использовать haml, или еще что-то). Давайте откроем app/views/index.scala.html:
@(message: String) @main("Welcome to Play 2.1")
Первая строка здесь — сигнатура функции. А далее можно писать html код, с вставками scala (которые начинаются с символа @).
Теперь вы можете снова запустить сервер в режиме разработки и немного поэксперементировать, но перед тем как продолжить отмените все изменения.
Развитие событий
А теперь мы начнем создавать todo приложение.
Шаг первый: Routes & Controller
# Routes # This file defines all application routes (Higher priority routes first) # ~~~~ # Home page GET / controllers.Application.index # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file) # Tasks GET /tasks controllers.Application.tasks POST /tasks controllers.Application.newTask POST /tasks/:id/delete controllers.Application.deleteTask(id: Long) POST /tasks/:id/complete controllers.Application.completeTask(id: Long)
Кроме стандартных, мы добавили еще 4 rout’a. Они позволят нам просматривать задания (первый), создавать задания (второй), удалять задания (третий), и делать задание завершенным (четвертый).
Теперь для каждого пути надо создать действия в app/controllers/Application.scala:
def tasks = TODO def newTask = TODO def deleteTask(id: Long) = TODO def completeTask(id: Long) = TODO
Мы пока еще не знаем, что будут делать эти функции, поэтому мы используем встроенную функцию TODO, которая вернет HTTP ответ «501 Not Implemented». Да, и еще: Scala — строго типизированный язык, но объявление метода переменных может быть непривычно для Java/C++ программистов.
Java код:
int a = 15; String s = "Hello, Habr!";
val a = 15 // Или val a: Int = 15 val s = "Hello, Habr!" // Или val s: String = "Hello, Habr!"
Давайте теперь попробуем запустить сервер, и посмотрим, что вышло.
Заходим на localhost:9000/tasks, и видим:
Маленькое отступление: мы не хотим, чтобы пользователи нашего приложения видели страницу, которая генерируется функцией index контроллера Action, поэтому внесем в контроллер (app/controllers/Application.scala) маленькие изменения:
Функция Redirect перенаправит нас на
GET /tasks controllers.Application.tasks
Шаг второй: Model
Как вы уже заметили, папки controllers и views сгенерировались, а папку models нам придется создать вручную. В корне приложения из консоли:
Теперь создадим в ней файл Task.scala. Он и станет нашей моделью. Итак, app/models/Task.scala:
package models case class Task(id: Long, label: String, who: String, mytime: String, ready: Short) object Task < def all(): List[Task] = Nil def create(label: String, who: String, time: String) <>def delete(id: Long) <> def complete(id: Long) <> >
case class Task(id: Long, label: String, who: String, mytime: String, ready: Int)
public class Task < private long id; private String label; private String who; private Int ready; public Task(long id, String label, String who, Int ready)< this.id = id; this.label = label; this.who = who; this.ready = ready; >public long getId() < return id; >public void setId(long id) < this.id = id; >//и еще 3 геттера и сеттера
А теперь давайте немного отвлечемся к модели, и перейдем к
Третий шаг: Views
Изменим содержимое файла app/views/index.scala.html на:
@(tasks: List[Task], taskForm: Form[(String, String)]) @import helper._ @main("Todo list") < @tasks.size idea(s)
Idea Who When Status Complete? @tasks.map < task =>@if(task.ready == 0) < >else < > @task.label @task.who @task.mytime @if(task.ready==0) < unfinished >else @form(routes.Application.deleteTask(task.id)) < > @if(task.ready==0)< @form(routes.Application.completeTask(task.id)) < > >
>
Add a new idea
@form(routes.Application.newTask) < @inputText(taskForm("label")) @inputText(taskForm("who")) > >
В первой строчке мы добавили в сигнатуру 2 параметра: список заданий, и «какой-то странный» taskForm. О нем мы поговорим чуть позже.
Далее идет импорт. Надо отметить, что запись в scala «import helper._» аналогична записи на java «import helper.*». Этот импорт предоставляет нам функцию form, которая создает тег с атрибутами action и method, а также функцию inputText, который создает несколько полей: input для ввода, label объясняет, что вводить, и еще label`ы, которые сообщают об ошибках.
Все остальные записи после @, на мой взгляд, интуитивно понятны.
Перейдем к «неведомой» taskForm. Внесем некоторые модификации в файл app/controllers/Application.scala:
- Добавим пару импортов для роботы с формами:
import play.api.data._ import play.api.data.Forms._
def completeTask(id: Long) = TODO val taskForm = Form( tuple ( "label" -> nonEmptyText, "who" -> nonEmptyText ) )
Четвертый шаг: все вместе
Наступило время продолжить работу над главной страницей (где выводится список всех дел, а также форма для добавления дел).
- добавим импорт модели в контроллер
То, что мы передаем в views.html.index() — мы передаем в функцию, сигнатура которой находится на первой строке файла index.scala.html. Сейчас Вы можете проверить результаты нашей незаконченной работы. Откройте в браузере localhost:9000/tasks, и, если вы все делали правильно, вы увидите:
Вы можете попробовать нажать кнопку «Create», но мы еще не написали def newTask, да и базы данных для хранения информации у нас еще нет.
Для начала закончим def newTask, которая будет обрабатывать данные из формы taskForm:
- Для начала добавим пару импортов (для даты)
import java.util.Calendar import java.text.SimpleDateFormat
def newTask = Action < implicit request =>taskForm.bindFromRequest.fold( errors => BadRequest(views.html.index(Task.all(), errors)), x=>x match < case(label,who) => < // Получаем текущее время val today = Calendar.getInstance().getTime() val timeFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss") val time = timeFormat.format(today) //---------------------------- Task.create(label, who, time) Redirect(routes.Application.tasks) >> ) >
Для человека, не знакомого с Scala, синтаксис данного кода будет понятно трудно, поэтому я просто объясню, что он делает. Из запроса мы получаем данные формы, которые мы обрабатываем (функцией bindFromRequest.fold()). Если есть ошибки в запросе, то выполняется функция BadRequest, которая останавливает процесс, возвращает нас на главную страницу и выводит все ошибки. Далее нам надо обработать еще и само задание (label), и того, кто создал идею (who). А также мы находим время создания (заметьте, оно не отправляется с формой, просто записывается время создания задания), и вызываем функцию Task.create(). Как и функцию Task.all, мы скоро допишем в файле app/models/Task.scala. Ну и теперь, когда все создано, мы переправляем пользователя на главную страницу.
Наступило время обратиться к базе данных.
Что нам осталось сделать? Подключиться-таки к базе данных, дописать методы модели и контроллера. Займемся базой данных.
- В файле conf/application.conf раскомментируйте или добавьте строки
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play"
# Tasks schema # --- !Ups CREATE SEQUENCE task_id_seq; CREATE TABLE task ( id integer NOT NULL DEFAULT nextval('task_id_seq'), label varchar(2000), who varchar(40), mytime varchar(100), ready integer ); # --- !Downs DROP TABLE task; DROP SEQUENCE task_id_seq;
Сейчас настало самое время открыть страницу localhost:9000/tasks в браузере.
Вы должны увидеть такую картинку:
import anorm._ import anorm.SqlParser._
val task = < get[Long]("id") ~ get[String]("label") ~ get[String]("who") ~ get[String]("mytime") ~ get[Int]("ready") map < case id~label~who~mytime~ready =>Task(id, label, who, mytime, ready) > >
import play.api.db._ import play.api.Play.current
А теперь заканчиваем функции all, create, delete и complete:
def all(): List[Task] = DB.withConnection < implicit c =>SQL("select * from task").as(task *) > def create(label: String, who: String, mytime: String) < DB.withConnection < implicit c =>SQL("insert into task (label,who,mytime,ready) values (,,, 0)").on( 'label -> label, 'who -> who, 'mytime -> mytime ).executeUpdate() > > def delete(id: Long) < DB.withConnection < implicit c =>SQL("delete from task where 'id -> id ).executeUpdate() > > def complete(id: Long) < DB.withConnection < implicit c =>SQL("update task set ready=1 where 'id -> id ).executeUpdate() > >
Как мы видим, написанный парсер мы используем только в функции all. Обратите внимания, что для непосредственного соединения с базой данных используется DB.withConnection, а для создания запроса мы используем функцию Anorm SQL.
Теперь, когда мы «разделались» с моделью, можно заканчивать контроллер. Нам остались две функции: delete и complete. Про удаление — без комментариев, а про complete: у каждого дела в БД есть поле «ready». Если оно 0 — то дело незаконченно. В функции complete мы будем менять его на 1. Итак, приступим:
def deleteTask(id: Long) = Action < Task.delete(id) Redirect(routes.Application.tasks) >def completeTask(id: Long) = Action
Осталось пару штрихов. Немного доделаем фал app/views/index.scala.html.
Вот этот код:
@inputText(taskForm("label")) @inputText(taskForm("who"))
@inputText(taskForm("label"), args = 'size -> 55, 'placeholder -> "Idea") @inputText(taskForm("who"), args = 'placeholder -> "Your name")
Поздравляю! Если вы честно следовали по статье, то сейчас у вас есть полностью работающее приложение. Вместе с делами оно может выглядеть вот так:
Теперь перейдем к развертыванию приложения. Я буду использовать git и heroku. Но для начала надо создать Procfile в корне приложения (в папке todolist):
web: target/start -Dhttp.port=$ -DapplyEvolutions.default=true -DapplyDownEvolutions.default=true -Ddb.default.url=$ -Ddb.default.driver=org.postgresql.Driver
import sbt._ import Keys._ import play.Project._ object ApplicationBuild extends Build < val appName = "todolist" val appVersion = "1.0-SNAPSHOT" val appDependencies = Seq( // Add your project dependencies here, jdbc, anorm, "postgresql" % "postgresql" % "8.4-702.jdbc4" ) val main = play.Project(appName, appVersion, appDependencies).settings( // Add your own project settings here ) >
Это надо сделать потому, что мы использовали базу данных H2, а на heroku база данных — PostgreSQL. Наконец-то мы можем перейти к непосредственному развертыванию (у вас должно быть установлено heroku, и вы должны быть зарегестрированы на сайте heroku.com), если нет:
$ wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh $ heroku login
$ git init $ git add . $ git commit -m "init" $ heroku create --stack cedar $ git push heroku master $ heroku open
Итоги
Подведем итоги. Сегодня мы познакомились с языком программирования Scala (не все, естественно), с фреймворком Play. Пользоваться им или нет — решать вам. Я на простом примере показал всю простоту их использования. Мне бы хотелось, чтобы каждый кто прочел эту статью начал пользоваться приобретенными знаниями и развивать их. Пишите на scala с play!