2016/10/17

Scala Play 2.5


新版本的 Scala Play 又有些不同,軟體的更新帶來了新的功能,新的想法跟做法,但就得花一些時間去了解,一個沒有變化的專案除了表示穩定,也可能是維護的人太少,沒辦法推出新功能,一個常常更新的專案,一般都是希望能夠相容舊版,無痛升級,雖然對開發者來說是福音,但也可能因為相容而自我設限,造成整個專案越來越龐大,該怎麼做沒有定論,Play Framework 就曾因為升級到 2 之後,不跟 1 相容,造成專案開發者的流失,不過都已經是過去的歷史了。


書本的內容也沒辦法隨著軟體更新就馬上更新,所幸 Play Framework 官方文件很完整,跟著文件的說明測試一次,應該就能大致掌握新版的功能。


安裝 Play


  1. 安裝 Java SDK
  2. 下載 Activator 1.3.10 minimal
  3. 將 typesafe-activator-1.3.10-minimal 資料夾的 bin 目錄放到環境變數的 PATH 中
  4. 確認可以在 console 執行 activator

產生 project


在 console 產生 project 有兩種方式,command line 或是 web ui。


  • command line

使用 activator 就可以產生 project


activator new playtest1 play-scala

切換到 playtest project folder,就可以用 activator 執行project


cd playtest1
activator

  • activator ui

執行 activator ui,就可以使用 activator 內建的網頁管理介面


activator ui

連結到 http://localhost:8888 就可以看到管理介面



選擇 Lightbend 的 Play Scala Seed 專案 template 及路徑,就可以產生 project




Play 支援了 Eclipse, IDEA, NetBeans, ENSIME 這些 IDE,跟著 Setting up your preferred IDE 的說明,就可以利用 IDE 直接產生一個新的 project。


以下記錄 IDEA 的步驟:
如果要產生一個新的 Play application


  1. New Project
  2. 選擇 Scala -> Activator
  3. 選擇一個適當的 template,最簡單的就是選擇 Lightbend 提供的 Play Scala Seed
  4. Finish

如果要 import 一個已經產生的 Play application


  1. File -> New -> Project from existing sources
  2. 選擇 project folder
  3. 選擇 Import project from external model -> SBT
  4. Finish

activator command line


cd playtest1

# 進入 activator 互動介面
activator

編譯 project


[playtest1] $ compile

執行 project,在這個模式下會啟用 auto-reload 功能,也就是 Play 會自動檢查 project source 有沒有更新,並自動重新編譯


[playtest1] $ run

執行測試程式


[playtest1] $ test

進入 scala console mode,按 Ctrl-D 就可以跳出 console mode


[playtest1] $ console

在 scala console mode,可以直接使用 play 的 classes


scala> views.html.index("Hello")
res0: play.twirl.api.HtmlFormat.Appendable =

<!DOCTYPE html>
<html lang="en">
    <head>

        <title>Welcome to Play</title>

以下這段 code 在文件中提到是可以在 console mode 中啟動 application


import play.api._
val env = Environment(new java.io.File("."), this.getClass.getClassLoader, Mode.Dev)
val context = ApplicationLoader.createContext(env)
val loader = ApplicationLoader(context)
val app = loader.load(context)
Play.start(app)

如果 debug 就要在啟動 activator 加上參數,JDPA 就會運作在 Port 9999 上


activator -jvm-debug 9999

Play console 其實就是一個 sbt console,因此我們可以用以下的方式,透過 sbt 進行 triggered execution


[playtest1] ~ compile
[playtest1] ~ run
[playtest1] ~ test

Play application file layout


app                      → app 原始程式
 └ assets                → 程式資源資料夾
    └ stylesheets        → LESS CSS 原始程式
    └ javascripts        → CoffeeScript 原始程式
 └ controllers           → app controllers
 └ models                → app business layer
 └ views                 → 網頁 templates
build.sbt                → sbt build script
conf                     → 設定檔及其他不需要編譯的資源
 └ application.conf      → app 主設定檔
 └ routes                → 路由跟程式的對應設定
dist                     → 專案發布的資料夾
public                   → 對外開放的資源資料夾
 └ stylesheets           → CSS files
 └ javascripts           → Javascript files
 └ images                → Image files
project                  → sbt 設定檔
 └ build.properties      → sbt 專案的設定檔
 └ plugins.sbt           → sbt plugins
lib                      → 不在 lib 倉庫中受管理的 libraries
logs                     → logs 資料夾
 └ application.log       → 預設的 log file
target                   → 編譯後的檔案資料夾
 └ resolution-cache      → 相關 libraries 的資料
 └ scala-2.11
    └ api                → API 文件
    └ classes            → 編譯後的 class files
    └ routes             → routes 編譯後的結果
    └ twirl              → templates 編譯後的結果
 └ universal             → app 封裝
 └ web                   → 編譯後的網頁資源
test                     → 單元測試的資料夾

Task sample project


以剛剛的 test1 為基礎,修改成一個簡單的 Task 網頁


  • 修改 conf/routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Map static resources from the /public folder to the /assets URL path
GET           /assets/*file        controllers.Assets.at(path="/public", file)

# Home page
GET           /                    controllers.TaskController.index

# Tasks
GET           /tasks               controllers.TaskController.tasks
POST          /tasks               controllers.TaskController.newTask
DELETE        /tasks/:id           controllers.TaskController.deleteTask(id: Int)

  • 新增 models/Task.scala

package models

case class Task(id: Int, name: String)

object Task {

  private var taskList: List[Task] = List()

  def all: List[Task] = {
    // 回傳整個 taskList
    taskList
  }

  def add(taskName: String) = {
    // task id 由 0 開始,如果 list 為 nonEmpty,就使用 list 中最後一個元素的 id
    val lastId: Int = if (taskList.nonEmpty) taskList.last.id else 0
    taskList = taskList ++ List(Task(lastId + 1, taskName))
  }

  def delete(taskId: Int) = {
    // filter 可取得滿足條件的所有元素
    // filterNot 可取得 不滿足 條件的所有元素
    taskList = taskList.filterNot(task => task.id == taskId)
  }
}

  • 新增 app/controllers/TaskController

package controllers

import models.Task
import play.api.mvc._


class TaskController extends Controller {

  def index = Action {
    // 將 request 由 http://localhost:9000/ redirect 到 http://localhost:9000/tasks
    Redirect(routes.TaskController.tasks)
  }

  def tasks = Action {
    // Task.all 取得整個 taskList,送入 views.html.index 產生網頁
    Ok(views.html.index(Task.all))
  }

  def newTask = Action(parse.urlFormEncoded) {
    implicit request =>
      // 取得 request 裡面的 taskName, 新增到 Task 中
      Task.add(request.body.get("taskName").get.head)
      // redirect 到 http://localhost:9000/tasks
      Redirect(routes.TaskController.index)
  }

  def deleteTask(id: Int) = Action {
    // 刪除 task id
    Task.delete(id)
    Ok
  }

}

  • views/index.scala.html

@(tasks: List[Task])

@main("Task Tracker") {

    <h2>Task</h2>
    <div>
        <form action="@routes.TaskController.newTask()" method="post">
            <input type="text" name="taskName" placeholder="Add a new Task" required>
            <input type="submit" value="Add">
        </form>
    </div>
    <div>
        <ul>
        @tasks.map { task =>
            <li>
                @task.name <button onclick="deleteTask ( @task.id) ;">Remove</button>
            </li>
        }
        </ul>
    </div>
    <script>
    function deleteTask ( id ) {
        var req = new XMLHttpRequest ( ) ;
        req.open ( "delete", "/tasks/" + id ) ;
        req.onload = function ( e ) {
            if ( req.status = 200 ) {
                document.location.reload ( true ) ;
            }
        } ;
        req.send ( ) ;

    }
    </script>
}

  • views/main.scala.html

@(title: String)(content: play.twirl.api.Html)
<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
    </head>
    <body>
    @content
    </body>
</html>


Projects


Streaming examples using Play 2.5 and Akka Streams http://loicdescotte.github.io/posts/play25-akka-streams/


play-scala-akka example


Example Play Scala application showing WebSocket use with Akka actors https://www.playframework.com/documentation/2.5.x/ScalaWebSockets


Mastering Play Framework for Scala