Head first Play framework

Nếu như Rails là một framework nổi tiếng và mạnh mẽ của Ruby thì Play là một framework rất nổi tiếng của Scala.Nó được ca ngợi là xây dựng trên nền tảng nhẹ nhàng, thân thiện, dễ dùng, tối ưu hóa tài nguyên phần cứng.Trong những năm gần đây Play đã được đưa vào ứng dụng trong các trường đại học để sinh viên làm quen thực hành.Sau đây tôi xin giới thiệu 1 vài ví dụ nho nhỏ để chúng ta cùng nhau tìm hiểu sự tiện lợi và thân thiện của Play framework.

I.Setting up your environment

1.Installing Java Development Kit

Scala biên dịch thành Java bytecode, vì vậy chúng ta cần cài đặt Java Development Kit (JDK) để chạy Scala.Việc cài đặt JDK được tiến hành qua các bước sau:

$ java -version

2.Typesafe Activator

Để bước đầu tiếp cận Play chúng ta nên sử dụng Typesafe Activator chúng ta có thể tải về để cài đặt tại địa chỉ: https://www.playframework.com/download, chúng ta có thể sử dụng lệnh sau để kiểm tra

$ activator --version

II.The Play sample application

1.The Play sample application

Để không phải đợi lâu chúng ta bắt ta vào thử thôi nào.. 😃) Activator cung cấp các tùy chọn mới để tạo mới một project, chúng ta có thể tạo mới project bằng cách đơn giản sau:

$ activator new scala-web-project play-scala

Khi đó chúng ta có một project mới là scala-web-project.Để chạy thử chúng ta sử dụng:

$ cd scala-web-project
$ activator
[scala-web-project] $ run

Lưu ý là app của chúng ta sẽ chạy trên cổng 9000, nên chúng ta sẽ chạy app theo đường dẫn

localhost:9000

và kết quả là thế này:

Untitled.png

Cấu trúc thư mục của project mà Play tạo cho chúng ta khá ấn tượng, tuy nhiên bảng dưới đây liệt kê những gì chúng ta cần làm với chúng trước khi bắt đầu sự thay đổi:

build.sbt                        	     Để thêm pipelineStages := Seq(digest)
project/plugins.sbt	                     Remove tất cả các web plugins trừ “sbt-digest”, chúng ta sẽ sử dụng Webpack để handling frontend assets.
conf/application.conf   	             Remove các comments chỉ là cácdòng bao gồm play.crypto.secret và play.i18n.langs
views/index.scala.html 	                 Xóa file này
app/Filters.scala		                 Xóa file này
app/Module.scala		                 Xóa file này
app/controllers/AsyncController.scala	 Xóa file này
app/controllers/CountController.scala	 Xóa file này
app/controllers/HomeController.scala     Giải thích ở phần bên dưới
app/filters					             Xóa file này
app/services					         Xóa file này
public/javascripts				         Xóa file này
public/stylesheets				         Xóa file này
test						             Xóa file này

Trong file index.scala.html chúng ta cần remove template's parameters và frontend assets để build lại theo cách dưới đây:

@()
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
    <link rel="shortcut icon" type="image/png"
      href="@routes.Assets.versioned("images/favicon.png")">
  </head>
  <body>
    <h1>Hello Play</h1>
  </body>
</html>

Trong file conf/routes thay thế bằng đoạn code dưới đây:

GET  /			    controllers.Application.index
GET  /assets/*file	controllers.Assets.versioned(path="/public", file: Asset)

Cuối cùng đổi tên HomeController.scala thành Application.scala và thay thế bằng đoạn code sau:

package controllers

import play.api._
import play.api.mvc._
class Application extends Controller {
  def index = Action {
    Ok(views.html.index())
  }
}

2.Application structure

Bước tiếp theo chúng ta sẽ tìm hiểu sâu hơn về cấu trúc của ứng dụng:

build.sbt				        Bao gồm các library dependencies, enabled plugins, version..
activator files				    Cho phép các dev khác có thể làm việc trên ứng dụng mà không cần cài đặt Typesafe Activator
public					        Chứa static assets như images, fonts hoặc compiled frontend assets như minified scripts và stylesheets
project/build.properties		Quy định sbt version
project/plugins.sbt			    Quy định về plugins cần thiết cho Play
conf/application.conf			Bao gồm các config cho ứng dụng, database connection properties
conf/logback.xml			    Chứa logging configuration
routes					        Quy định cụ thể các routes như HTTP endpoints và map chúng với Scala controllers methods
app/views				        Chứa Play templates
app/*.scala				        Phần backend của ứng dụng

Chúng ta hoàn toàn có thể viết thêm vào cấu trúc ở trên, tuy nhiên như thế đã là đủ để tạo cơ sở cho ứng dụng của chúng ta.Nếu bạn nhìn vào build.sbt file, bạn có thể tìm thấy các định nghĩa ở trong đó,tuy nhiên việc sử dụng 2 sbt operators chưa được thảo luận, vì vậy hãy xem xem libraryDependencies được khai báo và hoạt động thế nào:

name := """scala-web-project"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
scalaVersion := "2.11.7"
pipelineStages := Seq(digest)
libraryDependencies ++= Seq(
  jdbc,
  cache,
  ws,
  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.0-RC1" % Test
)
resolvers += "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases"

Toán tử += thêm giá trị mới vào list setting. Toán tử ++= cũng gần tương tự như thế, nhưng thay vì thêm 1 giá trị, nó thêm vào tất cả các giá trị từ provided sequence.Vì vậy trong ví dụ dưới đây, 4 thư viện đã được thêm vào danh sách các dependencies và một resolver mới được thêm vào danh sách các resolvers(Resolvers xác định nơi sbt tìm kiếm thư viện, theo mặc định sbt đã bao gồm Maven repository, bao gồm thư viện Java hoặc Scala bạn sẽ cần tới.Nếu không sử dụng tới ScalaZ trong project, chúng ta có thể xóa dòng dưới đây một cách an toàn).

GET 	/ 		controllers.Application.index

Hãy phân tích dòng trên một chút nhé, phần đầu tiên định nghĩa HTTP-method thường là GET hoặc POST, thành phần thứ 2 là URL tương đối so với root của ứng dụng, thành phần thứ 3 trỏ tới controllers action.Vì vậy trong ví dụ dưới đây, khi gõ địa chỉ http://localhost:9000/, index method của Application controller sẽ phục vụ request trên.

def index = Action {
  Ok(views.html.index())
}

Ngay sau đó index method gọi tới Play templates và gửi tới caller(ở trường hợp của chúng ta là browser).

3.Play templates.

Chúng ta có thể thấy Play templates(còn được gọi là Twirl templates ) như HTML files với fragments và Scala code.Dựa vào trình biên dịch Play chuyển chúng thành Scala method và trả về [email protected]() directive ở đầu forms là dấu hiệu của method.

@import scala.concurrent.Future

Kí tự @ là một dấu hiệu của Scala, kết quả của đoạn code dưới đây là render một random number trong div tags.

<body>
  <h1>Hello Play</h1>
  <div>@{Math.random}</div>
</body>

routes.Assets.versioned method thường thêm các dấu hiệu nhận biết cho static assets.Tuy nhiên nếu bạn check tổng quan HTML nhận được từ browser bạn thấy nó không hoạt động:

<link rel="shortcut icon" type="image/png" href="/assets/images/favicon.png">

Đầu tiên chúng ta cần chắc chắn rằng digest Play module được thêm vào assets pipeline trong build.sbt file:

pipelineStages := Seq(digest)

Thứ 2, chúng ta cần kiểm tra trạng thái của Play plugins được khai báo trong project/plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.0")

Cuối cùng chúng ta cần thay đổi play.crypto.secret từ application.conf.Để tạo secret mới có thể làm tuần tự như sau:

$ $ activator playGenerateSecret
[info] Generated new secret: _AC9P4rt66K7^cB=[g9`MoI9pGu;q2wB`cpl9cBY0FN0x:864`M\
lOFR_i=t^D2YL
[success] Total time: 0 s, completed Mar 5, 2016 5:26:44 PM

Sau đó trong application.conf, thay thế “changeme” bằng string mới tạo ra và start ứng dụng trên production:

$ activator testProd
(Starting server. Type Ctrl+D to exit logs, the server will remain in background)
[info] - play.api.Play - Application started (Prod)
[info] - play.core.server.NettyServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9\
000

Sau khi các URLs được tạo ra bởi routes.Assets.versioned, đường dẫn ở trên thay đổi thành

<link rel="shortcut icon" type="image/png"
  href="/assets/images/84a01dc6c53f0d2a58a2f7ff9e17a294-favicon.png">

4.Using template parameters

Khi Play template thực thi Scala method, chúng ta có thể khai báo parameters và arguments.Ví dụ như việc render current time với format đơn giản là string trong div tag trên trang home page của chúng ta. Đầu tiên chúng ta cần thêm mới parameters trong index.scala.html:

@(timeStr: String)
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
    <link rel="shortcut icon" type="image/png"
      href="@routes.Assets.versioned("images/favicon.png")">
  </head>
  <body>
    <h1>Hello Play</h1>
    <div>Current time: @timeStr</div>
  </body>
</html>

Lưu ý rằng chúng ta khai báo parameter như String.Scala là một kiểu statically language, passing arguments của các kiểu khác sẽ bị lỗi ở thời điểm biên dịch.Vì thế nếu chúng ta refresh trang lúc này chúng ta sẽ nhìn thấy lỗi xuất hiện bởi vì index method của Application controller đã gọi tới template method mà không passing arguments:

Untitled.png

Việc chúng ta cần làm là tạo một java.util.Date instance.Một khi chúng ta đã có object đó chúng ta có thể format chúng sử dụng SimpleDateFormatter và pass chúng vào template:

def index = Action {
  import java.util.Date
  import java.text.SimpleDateFormat
  val date = new Date()
  val dateStr = new SimpleDateFormat().format(date)
  Ok(views.html.index(dateStr))
}

Sau đó tất nhiên mọi thứ sẽ như chúng ta mong muốn, kết quả là:

Untitled.png

5.Serving asynchronous results Thay vì việc tạo Date instance một cách đơn giản như trên, nào cùng thử get những thông tin từ remote server.Trong Play, chúng ta cần sử dụng tới built-in HTTP client được gọi là WS, nó dựa trên Async Http Client for Java. Việc query từ remote servers có thể bị chậm, WS client trả về một Future, và việc sử dụng Future là một thử thách.Làm sao chúng ta có thể trả về một kết quả đúng với kiểu play.api.mvc.Result nếu chúng ta không biết khi nào Future object sẽ được trả về? Từ khi Play cung cấp class đầu tiên support cho các chương trình bất đồng bộ, một controller action có thể trả về 1 Future[Result] và có thể được handled bởi chính framewok.Sẽ có một thứ cần thay đổi đó là sử dụng Action.async thay vì Action.apply

def index = Action.async {
  import java.util.Date
  import java.text.SimpleDateFormat
  import scala.concurrent.Future
  val date = new Date()
  val dateStr = new SimpleDateFormat().format(date)
  Future.successful { (views.html.index(dateStr)) }
}

6.Adding sunrise and sunset times

Để làm màu thêm cho ứng dụng (rofl2) chúng ta sẽ show ra thông tin về thời gian mặt trời mọc, mặt trời lặn.Trong ví dụ dưới đây chúng ta sẽ lấy ví dụ với thành phố Sydney, có thể lấy thông tin tọa độ của thành phố này từ trang TravelMath.com sẽ được kết quả là vĩ độ: -33.8830, kinh độ: 151.2167. Để get các thông tin về thời tiết cho các khu vực trên thế giới bạn có thể thông qua một service với đường link sau:

http://api.sunrise-sunset.org/json?lat=-33.8830&lng=151.2167&formatted=0

Tất nhiên là chúng ta đang get API với location là Sydney rồi, khi đó kết quả trả về có format như sau:

{"results":{"sunrise":"2016-02-03T19:19:22+00:00","sunset":"2016-02-04T08:58:37+\
00:00","solar_noon":"2016-02-04T02:08:59+00:00","day_length":49155,"civil_twilig\
ht_begin":"2016-02-03T18:52:34+00:00","civil_twilight_end":"2016-02-04T09:25:24+\
00:00","nautical_twilight_begin":"2016-02-03T18:20:12+00:00","nautical_twilight_\
end":"2016-02-04T09:57:46+00:00","astronomical_twilight_begin":"2016-02-03T17:46\
:05+00:00","astronomical_twilight_end":"2016-02-04T10:31:54+00:00"},"status":"OK\
"}

Kết quả nhìn có vẻ phức tạp nhưng nếu format lại dưới dạng JSON thì sẽ rất dễ để sử dụng mà thôi.

{
  "results": {
    "sunrise": "2016-02-03T19:19:22+00:00",
    "sunset": "2016-02-04T08:58:37+00:00",
    "solar_noon": "2016-02-04T02:08:59+00:00",
    "day_length": 49155,
    "civil_twilight_begin": "2016-02-03T18:52:34+00:00",
    "civil_twilight_end": "2016-02-04T09:25:24+00:00",
    "nautical_twilight_begin": "2016-02-03T18:20:12+00:00",
    "nautical_twilight_end": "2016-02-04T09:57:46+00:00",
    "astronomical_twilight_begin": "2016-02-03T17:46:05+00:00",
    "astronomical_twilight_end": "2016-02-04T10:31:54+00:00"
  },
  "status": "OK"
}

Sử dụng WS object chúng ta có thể nhận được response như trên bằng cách sau:

val responseF = WS.url("http://api.sunrise-sunset.org/json?" +
  "lat=-33.8830&lng=151.2167&formatted=0").get()

Sau đó để lấy thông tin của 1 trường cụ thể có thể dùng method \ như đoạn code dưới đây:

val json = response.json
val sunriseTimeStr = (json \ "results" \ "sunrise").as[String]
val sunsetTimeStr = (json \ "results" \ "sunset").as[String]

Để code được clean chúng ta sẽ tạo mới một class trong model là SunInfo.Nó thường được dùng để lưu trữ thông tin về thời gian mặt trời mọc, mặt trời lặn

case class SunInfo(sunrise: String, sunset: String) Chúng ta sẽ lấy thông tin của SunInfo objects trong controller và pass chúng vào template như sau:

def index = Action.async {
  val responseF = WS.url("http://api.sunrise-sunset.org/json?" +
    "lat=-33.8830&lng=151.2167&formatted=0").get()
  responseF.map { response =>
    val json = response.json
    val sunriseTimeStr = (json \ "results" \ "sunrise").as[String]
    val sunsetTimeStr = (json \ "results" \ "sunset").as[String]
    val sunInfo = SunInfo(sunriseTimeStr, sunsetTimeStr)
    Ok(views.html.index(sunInfo))
  }
}

Lưu ý để method trên làm việc cần thêm 2 dòng quan trọng sau:

import play.api.Play.current
import scala.concurrent.ExecutionContext.Implicits.global

Khi đó chúng ta sẽ sửa lại view thành thế này:

@(sunInfo: model.SunInfo)
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Home</title>
    <link rel="shortcut icon" type="image/png"
      href="@routes.Assets.versioned("images/favicon.png")">
  </head>
  <body>
    <h1>Hello Play</h1>
    <div>Sunrise time: @sunInfo.sunrise</div>
    <div>Sunset time: @sunInfo.sunset</div>
  </body>
</html>

Kết quả cho chúng ta để biết khi nào là thời gian tuyệt vời nhất để ngắm bình minh và hoàng hôn ở Sydney là:

Sunrise time: 2016-02-03T19:19:22+00:00
Sunset time: 2016-02-04T08:58:37+00:00

III.Kết Luận

Tạm thời thế đã nhỉ, trong bài viết tiếp theo tôi sẽ giới thiệu ví dụ kinh điển và kinh khủng hơn thế này ví dụ như là xây dựng một trang login logout trên Play Framwork (yaoming)


All Rights Reserved