光哥 2015-01-13
Scala在业界已日益成为主流的编程语言和开发工具,与Java一样在Web开发领域的发展尤其令开发者关注,因此本文选取Scala语言中当前两个主要Web框架(Play、Lift)中的一个较易上手的Play框架,结合作者的实践以简洁易懂快速上手为目标,使开发者快速进行以Scala语言为基础的Web应用和网站开发。
Scala语言简介Scala语言编译后的代码直接运行在Java虚拟机之上,可调用所有的Java代码库,Scala设计目标是成为比Java更好的语言。Scala同时具备和整合了面向对象及函数式的编程特性。
Play框架简介PlayFramework是一个开源的Web应用框架,使用Scala和Java语言混合编写。Play遵循传统的MVC(Model-View-Controller:模型、视图和控制器)模式,这一点Lift与其有所不同。
本文主要对PlayFramework最新第二版(Play2.0)进行讲解。
安装和配置Scala和PlayFramework开发环境安装JDKScala作为Java虚拟机语言(JVMLanguage),与Java一样需要Java虚拟机才能编译和运行,因此开发者须首先安装Java开发工具包(JDK),无论为个人学习还是商业开发,目前均推荐使用JDK6(JavaSE6)而非7,亦可使用同版本的OpenJDK。
安装完JDK后,一定要检查环境变量是否设置正确,检验方法:
在命令行程序中任意路径位置,分别输入java-version和javac-version若输出正确的java和javac版本信息,即表示安装和环境变量设置正确;若输出的是"commandnotfound"或"不是内部或外部命令,也不是可运行的程序"等错误提示,即表示没有安装成功或环境变量设置有误。
下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk6u37-downloads-1859587.html
$sudoapt-getinstallopenjdk-6-jre
下载Scala、Play工程项目下载地址:http://../go/scala-play-prj.zip
Play2.0开始使用Scala开发生态中的SBT(SimpleBuildTool)作为编译、运行、测试、部署和配置管理系统,类似Java的Maven。
SBT通常以一个sbt-launch.jar文件通过Java指令启动,根据项目的SBT配置文件,自动下载该项目所需的全部工具和类库。SBT可由开发者自行安装在计算机中,作为各种项目的统一工具,也可放入工程项目中以便快速部署。
本文所提供的用于快速入门的工程项目内置了SBT,开发者下载解包后,根据不同的操作系统运行下列命令即可初始化和启动开发环境:
*Window:
cdweb_appsbt
*Unix(Linux/MacOSX...)
$cdweb_app$./sbt
WindowsXP的开发者请注意,由于PlayFramework发布版本的问题(Bug),目前无法在该操作系统上正常开发和运行,Windows7不受影响。
使用Scala和Play开发环境初始化Scala、Play开发环境首次使用SBT,启动和执行run命令时,SBT会自动下载所需的工具和类库,如:Scala编译器、Scala基础库、Play框架等相应开发和运行所必需的类库,这个过程根据网速快慢可能需要数十分钟至数个小时,开发者此时可以继续阅读本文下面的内容。
运行ScalaPlayWeb应用Scala和Play的开发和测试均在SBT命令行状态下进行。进入项目所在路径启动SBT。
当出现[web_app]$时,表示开发环境准备就绪,可以进行开发和测试工作。
输入指令run:
[web_app]$run
即可运行本文提供的工程项目Web应用,指令执行后输出以下信息时证明启动成功:
---(RunningtheapplicationfromSBT,auto-reloadingisenabled)---
[info]play-ListeningforHTTPonport9000...
(Serverstarted,useCtrl+Dtostopandgobacktotheconsole...)
"Serverstarted"表示Web应用已经运行成功,"HTTPonport9000"指的是Web应用使用了本机9000端口作为Web服务器监听端口。
此时在浏览器中输入:http://localhost:9000,即可浏览该Web应用的效果。
首次运行需要等待数秒,用于SBT编译项目中的Scala/Java代码,此时会看到以下信息:
[info]Compiling6Scalasourcesand1Javasourceto/../web_app/target/scala-2.9.1/classes...
Play是由Scala和Java混合编写的框架,因此在用Scala开发Play应用的过程中虽并不需z编写Java代码,但Play会自动生成一个Java代码文件,因此在编译的时候会提示一个Javasource,稍后会讲解为何生成该Java代码。
此时SBT命令行处于锁定状态,若想输入新的SBT命令,必须按下Ctrl+D,重新回到命令行待命状态:[web_app]$
文件保存、浏览器刷新与自动编译在输入run指令后,修改任何文件只需要进行保存,然后通过浏览器刷新(F5)网页,SBT将会自动编译修改后的文件,并将结果(或错误信息)输出至浏览器。
清理编译后的文件有时需要清空所有编译后的文件如各种.class文件以恢复全新的状态,在待命状态下输入:
[web_app]$clean
可以尝试执行该清理命令再重新编译来解决开发过程中遇到一些难以理解的问题。
只编译(不运行)有时在进行程序编码为了校验语法等目的,不需要运行程序,以提高开发效率只需要执行编译指令compile:
[web_app]$compile
退出SBT在待命状态下输入:
[web_app]$exit
PlayFramework下的Web开发Play工程目录结构web_app根目录
|sbtSBTUnix批处理脚本用于启动sbt-launch.jar
|sbt.batSBTWindows批处理脚本用于启动sbt-launch.jar
|sbt-launch.jarSBT启动的Java可执行类库
|
+---appPlayWeb应用全部代码所在目录
||
|+---models模型代码所在目录
||Message.scala留言板例程模型代码
||
|+---controllers控制器代码所在目录
||Application.scala默认控制器代码
||
|\---views视图(PlayScalaHTML模板)代码所在目录
|main.scala.html主模板文件
|index.scala.html首页模板文件
|msgboard.scala.html留言板例程模板文件
|
+---confPlay配置文件所在目录
|application.conf应用配置文件
|routes应用入口路由文件,所有的HTTP请求将通过该文件转发到指定的Scala对象处理
|
+---logs日志目录
|application.log应用运行日志
|
+---projectSBT工程文件
|build.properties保存所需的SBT版本信息,通常无需更改
|Build.scala主要的工程配置文件
|plugins.sbt告知SBT本工程所需要的插件以及下载位置
|
+---public存储一切直接发送给浏览器的资源文件
||
|+---images图像文件,如JPEG、PNG、GIF等
||
|+---javascriptsJavaScript脚本文件
||
|\---stylesheetsCSS样式表文件
|
\---target存放编译后的可执行代码和编译时的中间代码
一切从routes开始Play的应用入口为conf/routes文件,该文件定义了全部Play应用中URL对应的动作(Action),如当浏览器请求访问http://localhost:9000/,Play应用将会返回一个页面,此时routes文件应定义成如下形式:
GET/controllers.Application.index
该定义告知Play收到HTTPGET类型请求且路径为/时调用controllers包中Application类的index方法,对应的代码即为:web_app/app/controllers/Application.scala
此时若尝试访问http://localhost:9000/index.html,会出现“Actionnotfound”错误提示,因为此时没有在routes文件中定义针对GET/index.html的对应动作,开发者可尝试修改routes文件,添加下列行:
GET/index.htmlcontrollers.Application.index
当再次访问http://localhost:9000/index.html会就发现浏览器可以正常显示与http://localhost:9000/一样的内容。
理解PlayFramework控制代码(Controller)上文的routes定义/和/index.html对应了Application.scala代码中的index方法来显示网页内容:
//所有的控制代码按Play规范均归入controllers包
packagecontrollers
//导入Play应用开发所需的类库
importplay.api._
importplay.api.mvc._
importplay.api.templates._
//Application全局对象实例化,因此使用Object来声明Application并继承Play的Controller类
objectApplicationextendsController{
//定义index方法,任何routes文件中指定调用的方法,必须返回Action对象来处理HTTP请求
defindex=Action{
//任何Action对象必须获得反返回的Result对象
//Ok继承于Result对象,所以返回Ok表示其包含的内容为HTTP200OK状态
//在Scala最后一行代码可以无需使用return来返回结果
//因此下面最后一行代码等同于returnOk(views.html.index("首页","首个PlayWeb应用!"))
Ok(views.html.index("首页","第一个PlayWeb应用!"))
}
}
Ok表示所包括的内容的状态为HTTP200OK,现在尝试修改该段代码为:
Ok("<p>修改后的<strong>代码!</strong></p>")
保存并刷新浏览器,会发现浏览器会将上面的字符串连同HTML标签一起显示出来,因为此时传入Ok中对象类型为String,Ok将其Content-type作为text/plain输出,所以连同HTML标签一起显示。
因此需要返回并告知Ok这是段Html格式内容而非纯文本,因此套用Html类,再次修改代码为:
Ok(Html("<p>修改后的<strong>代码!</strong></p>"))
保存并刷新浏览器,HTML标签已不存在,文字也按照规定的格式正常显示。
由此可知PlayWeb应用的调用顺序和关系为:
浏览器(http://localhost:9000/)->Play框架(conf/routes)->对应的Controller代码(app/controllers/Application.scala)->对应的返回Action(defindex=Action{...})的方法->对应的可返回Result的代码(OK(...))->要返回的正文内容("..."纯文本或Html("...)HTML格式)。
PlayFramework模板(View)Play的模板在HTML基础上直接基于Scala语言,模板文件通常存放在/app/views目录下,文件须以“.scala.html”双扩展名命名。Play的每个模板文件其实都是一个Scala代码,均需要通过Scala编译器检查其类型与语法,并编译成.class可执行的JVM二进制文件。
编译时Play首相会将.scala.html的Play模板文件自动生成为.scala的源代码文件,如/app/views/index.scala.html的模板文件将会生成/target/scala-2.9.1/src_managed/main/views/html/index.template.scala文件,该文件将会继而被Scala编译器编译成index.class。
首先看一下index.scala.html文件:
@(title:String,message:String)
@*模板入参,两个参数均为String类型,分别命名为title和message*@
@*调用main.scala.html模板,传入参数title*@
@main(title){
@*输出参数message*@
<p>@message</p>
<ahref="#">Scala语言与Play框架入门教程</a>
}
在Play的模板中@符号代表跟随其后的代码为Scala语言代码,由于最终任何.scala.html文件都将转换成Scala语言代码,在编写Play模板时也可以理解为编写Scala语言代码,一个模板文件可以理解为一个对象,因此需要传入参数时就必须在模板文件的第一行定义都需要哪些入参以及这些入参的名字和类型。服务器端的代码注释写法为:@*注释内容*@
比如index.scala.html定义需要两个入参,两个参数均为String类型,分别命名为title和message,就对应了Application.scala代码:
Ok(views.html.index("首页","第一个PlayWeb应用!"))
Play将会生成一个包名为views.html,名为index的实例化对象,并接受两个String类型的入参,此时index.scala.html文件中的title="首页"、message="第一个PlayWeb应用!",大括号中此时的内容应为:
<p>第一个PlayWeb应用!</p>
<ahref="#">Scala语言与Play框架入门教程</a>
接下来@main(title)调用了main.scala.html模板,在Scala代码层面可以理解为views.html.main(title)。
再看main.scala.html文件:
@(title:String)(body:Html)
<!DOCTYPEhtml>
<html>
<head>
<title>@title</title>
</head>
<body>
@body
</body>
</html>
main.scala.html声明需要两个分别名为title的String类型和名为body的Html类型入参,index.scala.html将title直接传给了main,因此此时main的title的值同样也是"首页",其后index将大括号中的全部的内容传递给了main的body。
根据上述代码当我们访问http://localhost:9000/时,输出的全部内容为:
<!DOCTYPEhtml>
<html>
<head>
<title>首页</title>
</head>
<body>
<p>第一个PlayWeb应用!</p>
<ahref="#">Scala语言与Play框架入门教程</a>
</body>
</html>
Play业务和数据模型(Model)当Web应用需要对数据模型进行处理时,则需要在app/models目录下编写相应的scala代码,这些模型可以与内存、文件、关系型数据库或NoSQL非关系型数据库等数据存储方式进行绑定,也可以是对业务逻辑或表单的定义。
下文将会对Play模型进行讲解。
例程学习:留言板设计一个留言板,通过表单提交留言,存储在内存中,并显示出全部留言。
定义留言板表单和数据模型在app/models目录下创建Message.scala文件,并写入如下Scala代码:
packagemodels
importplay.api._
importplay.api.data._
importplay.api.data.Forms._
//每条留言的数据模型:包括两个字符串,分别存储名字和内容
caseclassMessage(name:String,content:String)
//全部留言的数据操作
objectMessage{
//用于存储全部留言的列表
varlist:List[Message]=Nil
//将留言追加在用于存储全部留言的列表前面
defpost(name:String,content:String){
list:::=List(Message(name,content))
}
//定义表单及其校验要求,nonEmptyText表示该项内容不得为空
valform=Form(tuple(
"name"->nonEmptyText,
"content"->nonEmptyText
))
}
设计视图模板(View)在app/views目录下创建msgboard.scala.html文件,并写入如下Play模板代码:
@(msgs:List[Message],msgForm:Form[(String,String)])
@*
模板入参:
第一个名为msgs的List[Message]类型参数;
第二个名为msgForm的Form[(String,String)]类型参数
*@
@*导入helper包下的类和对象,因为需要其中的form来生成表单*@
@importhelper._
@*调用main.scala.html模板,并将标题改为"留言板"*@
@main("留言板"){
<h2>留言<h2>
<ul>
@*提取msgs入参中的全部数据*@
@msgs.map{message=>
<li>
@*提取名字*@
<p><strong>@message.name</strong></p>
@*提取内容*@
<p>@message.content</p>
</li>
}
</ul>
<h2>发言</h2>
@*创建表单,告知表单提交时发送POST给routes.Application.postMsg来处理*@
@form(routes.Application.postMsg){
@*生成名字的输入框*@
@inputText(msgForm("name"),'_label->"名字")
@*生成内容的输入框*@
@inputText(msgForm("content"),'_label->"内容")
@*生成用于提交的按钮*@
<inputtype="submit"value="发送">
}
}
编写控制器(Controller)在app/controllers/Application.scala文件中加入如下代码:
//显示留言列表和发言表单
defm=Action{
Ok(views.html.msgboard(Message.list,Message.form))
}
//处理发言
defpostMsg=Action{implicitrequest=>
Message.form.bindFromRequest.fold(
//处理错误
errors=>BadRequest(views.html.msgboard(Message.list,errors)),
{
case(name,content)=>
//发言
Message.post(name,content)
//重新定向到显示留言列表和发言表单页面
Redirect(routes.Application.m)
}
)
定义routes在conf/routes文件中写入如下行:
#显示留言列表和发言表单
GET/mcontrollers.Application.m
#使用POST方式提交留言
POST/postMsgcontrollers.Application.postMsg
运行和测试留言板此时在浏览器中输入:http://localhost:9000/m,即可测试留言板。