使用Scala开发现代应用程序:使用Play框架的Web应用程序

本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。

在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

1.简介

自从Web成为用于各种不同应用程序(网站,Web门户网站或Web API)的占主导地位的,可普遍访问的全球平台以来,已经有很长时间了。 从一组简单的静态HTML页面开始,Web应用程序就突飞猛进,赶上了桌面应用程序,并转变为我们以前称为富Internet应用程序 (或简称RIA )的新类。 但是,如果没有网络浏览器的发展(在某些情况下甚至是革命),那么大多数这样的进步是不可能的。

在本教程的这一部分中,我们将讨论使用Scala编程语言和生态系统开发丰富的网站和门户(或简单地说,是现代Web应用程序)。

2. MVC和模式的力量

人们可以采用多种方法来设计和开发Web应用程序。 尽管如此,还是有一些模式浮出水面,并在软件开发社区中得到了广泛采用。

Model-View-Controller (或MVC )是用于开发可维护的基于UI的应用程序的最广泛应用的体系结构模式之一。 它非常简单,但是提供了足够程度的关注点和责任分离。

MVC模式及其合作者

MVC模式及其合作者

本质上, MVC很好地概述了协作者及其角色。 该View使用Model来呈现桌面或Web UI表示的UserUserView进行交互,这可能会通过使用Controller导致模型更新(或检索)。 反过来, Controller'sModel Controller's操作也可能导致View被刷新。 在某些情况下, User可能会完全绕过View ,而直接与Controller交互。

如今,用于Web应用程序开发的许多框架都是围绕MVC模式或其一种(或多种)派生方式设计的。 在Scala生态系统中, Play框架无疑是目前最好的选择,这是我们在本教程的这一部分中要讨论的内容。

3.游戏框架:愉快而富有成效

Play框架是用Scala编写的现代化,可立即投入生产的高速,成熟的Web框架( 也提供 Java友好的API)。 它的架构是完全异步的,轻量级的和无状态的,并且是在Akka Toolkit的基础上构建的,我们已经在本教程的上一部分中进行了详细讨论。 在撰写本文时, Play Framework的最新发行版本是2.5.9

尽管Play Framework不仅限于仅支持Web应用程序的开发,但我们将主要关注这一方面,在下一节中继续专门讨论Web API的讨论。

4.控制器,动作和路线

Play Framework完全包含MVC模型,并且从一开始就引入了控制器的概念。 遵循职责, 控制器可能会生成一些操作 ,并返回一些结果,例如:

@Singleton
class HealthController extends Controller {
  def check() = Action {
    Ok("OK")
  }
}

请注意,按照惯例,控制器存储在controllers软件包下。 控制器方法可以使用HTTP协议语义直接公开为HTTP端点。 在Play Framework中,这样的映射称为route ,所有路由定义都放置在conf/routes文件中,例如:

GET  /health        controllers.HealthController.check
GET  /assets/*file  controllers.Assets.versioned(path="/public", file: Asset)

不要让这个示例的简单性欺骗您, Play Framework路由定义可能包含任意数量的非常复杂的参数和URI模式,这将在本节的后面部分看到。

Play Framework中的 控制器扩展了控制器特性,并且可以包含任何(合理)数量的返回Action实例的方法。 所有动作都以异步,非阻塞的方式执行,记住这一点非常重要。 控制器应尽可能避免执行阻塞操作,并且Action伴侣对象提供了方便的异步方法系列,可与异步执行流无缝集成,例如:

@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合作者,即model view 。 让我们谈论一下。

5.视图和模板

Play Framework中的视图通常基于常规HTML标记,但由Twirl (基于Scala的功能非常强大的模板引擎)支持。 在后台, 模板随同对象一起转换为Scala类。 它们可以具有参数,并被编译为标准Scala代码。

让我们举一个简单的示例,介绍如何通过引入User case类在Play Framework视图模板中传递和呈现model

case class User(id: Option[Int], email: String, 
  firstName: Option[String], lastName: Option[String])

如果看起来很熟悉,那么您不会误会:这与我们在本教程的“ 使用Slick进行数据库访问”部分中看到的案例类完全相同。 因此,让我们创建一个users.scala.html模板,以HTML表格的形式打印用户列表,并按惯例存储在views文件夹中(该文件夹也用作包名):

@(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的表达式呈现表的行时:

@for(user <- users) {
  <tr>
    <td>@user.id</td>
    <td>@user.firstName</td>
    <td>@user.lastName</td>
    <td>@user.email</td>
  </tr>
}

就是这样! 并且由于所有模板都被编译为字节码,因此与不存在的属性或不适当的表达式用法相关的任何错误都将在编译时捕获! 借助编译器的这种帮助,任何种类的重构都变得更加容易和安全。

用户表

用户表

为了结束循环,在“ 控制器,动作和路由”部分中,我们已经看到了如何使用控制器动作实例化模板并将其发送到浏览器:

def getUsers = Action.async {
  service.findAll().map { users =>
    Ok(views.html.users(users))
  }
}

除了简单的组件外, 模板还可以包含HTML 表单 ,这些表单可以由控制器方法直接支持。 作为示例,让我们实现新用户功能的添加,例如将接线视图,控制器和模型一起添加。 首先,在控制器端,我们必须添加Form定义,包括所有验证约束:

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)
)

其次,我们将在控制器中创建一个专用端点,以作为表单提交的结果添加新用户:

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复制到其HTML演示文稿中:

@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框架代替。 这是此表单在浏览器中的外观的简要介绍。

用户表格

添加用户表格

作为最后一步,应该更新路由表以同时列出此新端点。 幸运的是,它只是一个班轮:

POST  /  controllers.UserController.addUser

请注意, Play Framework脚手架负责所有验证部分,并将其报告给视图,例如,将不接受使用无效电子邮件地址提交表单:

电子邮件无效

指定了错误的电子邮件地址时出错

当然, Play Framework提供了足够的灵活性来报告其他类型的错误,这些错误不一定与验证相关。 例如,在控制器中,我们使用了userForm.withGlobalError方法来表示重复的电子邮件地址。

6.动作组成和过滤器

通常需要在控制器的方法调用之前或之后执行某些操作,甚至可能更改响应。 例如,日志记录,安全性验证或对跨域资源共享 (CORS)的支持。

开箱即用的Play框架提供了几种劫持到处理管道中的方法:使用过滤器动作组合

7.访问数据库

任何或多或少的实际Web应用程序都需要管理一些数据,在许多情况下,众所周知的关系数据存储是最佳选择。 Play Framework与两个基于JDBC的库提供了卓越的集成 ,但是我们已经学到了很多有关Slick的很棒的知识,并且可以肯定的是, Play Framework 与Slick无缝集成。

为了使事情变得更加简单和熟悉,我们将重复使用在带有Slick部分的Database Access中构建的相同数据模型,并且几乎不做任何修改。 我们需要做的微小更改仅会影响UserRepository :将DatabaseConfigProvider注入默认数据库配置,并使用其provider.get[JdbcProfile]方法获取相应的JdbcProfile实例。

@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文件对其进行配置。 为了方便起见,单个数据库Web应用程序可以使用经过特殊处理的default应用程序。

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框架立足于Akka Toolkit基金会,因此演员是那里的第一批公民。 任何Play Framework网络应用程序都具有在应用程序启动时立即创建的专用actor系统 (并在应用程序重新启动时自动重新启动)。

Play框架从一开始就完全采用反应式范例,是Akka Streams实施的最早采用者之一。 更进一步, Play Framework提供了许多有用的实用程序类,以将Akka Streams与Web应用程序特定技术桥接在一起,我们将要讨论这些技术。

9. WebSockets

可以说, WebSockets是当今最有趣,传播最Swift的通信协议之一。 本质上, WebSockets将Web客户端/ Web服务器的交互提升到了一个新的水平,提供了通过HTTP协议建立的全双工通信通道。

为了使WebSockets真正的应用程序的角度来看,让我们实现此功能,以在每次添加新用户时在Web应用程序中显示通知。 在内部,此事实由UserAdded事件表示。

case class UserAdded(user: User)

使用我们在本教程的上一部分中讨论的Akka 事件流 ,我们可以通过创建一个专用的actor来订阅此事件,我们将其UsersWebSocketActor

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
  }
}

它看起来非常简单,但神秘的out演员的参考。 让我们看看它的来源。 Play Framework一直都对WebSockets提供了出色的支持,但是与Akka Streams的紧密集成使它变得更好了 。 这是WebSockets端点,用于将有关新用户的通知广播到客户端。

def usersWs = WebSocket.accept[User, User] { request =>
  ActorFlow.actorRef(out => Props(new UsersWebSocketActor(out)))
}

如此复杂的功能仅需几行代码,真是太神奇了! 请注意, out actor引用实质上代表了Web客户端,并且由Play Framework开箱即用地提供。 路由表中的更改也很小。

GET  /notifications/users  controllers.NotificationController.usersWs

在浏览器方面,这是标准JavaScript代码段,可以将其直接插入到视图模板中。

<script type="text/javascript">
  var socket = new WebSocket(
    "@routes.NotificationController.usersWs().webSocketURL()")
  
  socket.onmessage = function(event) {
    var user = jQuery.parseJSON(event.data);
    ...
  }
</script>

如前所述, WebSockets是双向通信通道:不仅Web服务器可以向Web客户端发送数据,而且Web客户端也可以启动一些消息。 我们没有在此处的示例中涵盖此部分,但Play Framework文档对其进行了详细讨论。

10.服务器发送的事件

WebSockets非常强大,但通常可以通过更简单的实现来支持Web应用程序的需求。 如果不需要全双工通道,Web服务器可以依靠服务器发送的事件 (或SSE )以单向方式将数据发送到Web客户端。 在Play框架 (以及许多其他框架)中,通过使用特殊内容类型text/event-stream支持分块(或流式)响应来实现。

def usersSse = Action {
  Ok.chunked(
    Source.actorPublisher(Props[UsersSseActor]) via EventSource.flow[User]
  ).as(ContentTypes.EVENT_STREAM)
}

在这种情况下,服务器可以使用成熟的Akka Streams数据处理管道,并使用EventSource支架将数据传递到客户端。 为了说明但另一个有趣的功能阿卡流 ,我们使用UsersSseActor演员,功能上类似于UsersWebSocketActor ,作为流源。

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
  }
}

由于我们必须遵循Akka Streams约定和API才能拥有行为良好的发布者,所以它稍微复杂了一点,但是从本质上讲,它还使用事件流来订阅通知。 同样,在视图方面,仅使用裸露JavaScript:

<script type="text/javascript">
var event = new EventSource(
  "@routes.NotificationController.usersSse().absoluteURL()");
         
event.addEventListener('message', function(event) {
  var user = jQuery.parseJSON(event.data);
  ...
});
</script>

并且不要忘记在路由表中添加另一个条目:

GET  /notifications/sse  controllers.NotificationController.usersSse

11.运行Play应用程序

有多种方法可以运行我们的Play Framework应用程序,但最简单的方法可能是使用我们已经非常熟悉的sbt工具:

sbt run

但是,更好的方法仍然使用sbt ,那就是以连续的edit-compile-(re)deploy周期运行应用程序,以(主要)即时反映源文件中的修改:

sbt ~run

默认情况下,每个Play应用程序都在HTTP端口9000上运行,因此,请随意将浏览器导航至http:// localhost:9000以与用户一起播放,或导航至http:// localhost:9000 / notifications以查看WebSockets服务器发送活动中的事件

12.安全HTTP(HTTPS)

在当今的现代网站和门户网站中,在生产中使用安全HTTPHTTPS )是必不可少的规则。 但是在开发过程中,通常也需要在HTTPS支持下运行Play框架应用程序。 通常,这是通过生成自签名证书并将其导入Java Key Store来完成的 ,只需执行以下命令即可:

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密钥存储区可用于将Play Framework应用程序配置为在HTTPS支持下运行,例如:

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.测试

在Web应用程序世界中,测试具有多种形式和形式,但是Play Framework通过提供必要的支架来简化它们,确实起到了很好的作用。 而且,同样支持ScalaTestspecs2框架。

Play Framework中进行测试的最简单,最快的方法可能是使用单元测试。 例如,让我们看一下这个用于测试UserController方法的specs2套件。

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(
        User(Some(1), "a@b.com", Some("Tom"), Some("Tommyknocker"))))
      val result = controller.getUsers()(FakeRequest())

      status(result) must equalTo(OK)
      contentAsString(result) must contain("a@b.com")
    }
  }
}

用于specs2集成的有用的Play Framework脚手架使编写单元测试像呼吸一样容易。 在测试金字塔上上一个台阶,我们可能需要考虑编写集成测试,在这种情况下,由于开箱即用的专用附加功能, Play Framework最好与ScalaTest一起使用。

class UserControllerSpec extends PlaySpec with OneAppPerTest with ScalaFutures {
  "UserController" should {
    "render the users page" in {
      val userService = app.injector.instanceOf[UserService]
     
      whenReady(userService.insert(User(None, "a@b.com", 
          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")
      contentAsString(users) must include("a@b.com")
        .and(include ("Tommyknocker"))
    }
  }
}

在这种情况下,将创建一个功能完善的Play Framework应用程序,其中包括配置了所有扩展功能的数据库实例。 但是,如果您想尽可能地接近实际部署,则可以考虑添加Web UI(带有或不带有浏览器)测试用例,同样, ScalaTest集成提供了必要的部分。

class UserControllerBrowserSpec extends PlaySpec 
    with OneServerPerSuite with OneBrowserPerSuite 
      with HtmlUnitFactory {
  "Users page" must {
    "should show emtpy users" in {
      go to s"http://localhost:$port/"
      pageTitle mustBe "Manage Users"

      textField("email").value = "a@b.com"
      submit()
      
      eventually { pageTitle mustBe "Manage Users" }
      find(xpath(".//*[@class='table']/tbody/tr[1]/td[4]")) map {
        _.text mustBe ("a@b.com") 
      }
    }
  }
}

此著名的测试策略基于Selenium Web浏览器自动化 ,该浏览器与无浏览器的HtmlUnitFactory很好地包装在OneBrowserPerSuite中

14.结论

毫无疑问, Play Framework带回了在JVM平台上进行Web应用程序开发的乐趣。 以ScalaAkka为核心,基于反应式编程范式构建的现代,功能极其丰富且高效的产品,所有这些使Play Framework成为您永远不会后悔的选择。 别忘了,前端和后端之间的出色分隔使您可以将两个世界的最佳部分联系在一起,从而创建漂亮且可维护的Web应用程序。

15.下一步是什么

在本教程下一部分中,我们将讨论使用Akka HTTP模块开发REST(ful)Web API

完整的源代码可供下载。

翻译自: https://www.javacodegeeks.com/2016/10/developing-modern-applications-scala-web-applications-play-framework.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值