Head first Play framework
Bài đăng này đã không được cập nhật trong 3 năm
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:
-
Download JDK từ địa chỉ http://www.oracle.com/technetwork/java/javase/downloads/index.html, vì mình sử dụng Linux nên sẽ download file tar.gz
-
Giải nén thư mục file vừa download
-
Thêm thư mục bin của JDK vào PATH Sau đó kiểm tra bạn đã cài đặt thành công hay chưa bằng lệnh
$ 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:
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ề HTML.@() 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:
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à:
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