Facebook Twitter LinkedIn E-mail
magnify
Home 2014 十月

Play Framework Web开发教程(35): 使用LESS和CoffeeScript

浏览器最终处理的的HTML文档包含CSS和JavaScript,因此你的应用也必须输出这些浏览器支持的文件,但是使用CSS和JavaScript并不总是程序员最好的选择,很多程序员希望使用最新的Web技术,比如LESS和CoffeeScript。 LESS为一个stylesheet语言,通过LESS编译器可以把LESS语句翻译成CSS格式,同样CoffeeScrip通过编译也可以转换成JavaScript语言。
Play框架集成了LESS和CoffeeScript编译器,关于LESS和CoffeeScript的用法和优点不在本系列教材讨论的范围之列,本篇介绍如何在Play应用使用LESS和CoffeeScript。
如果你使用IntelliJ IDEA基础开发环境,LESS和CoffeeScript也会自动编译成对应的CSS和JavaScript,不需要你做额外的工作。
LESS
LESS(http://lesscss.org) 和CSS相比具有很多优点,比如LESS支持使用变量,minxin,嵌套,以及其它语言结构来帮助开发人员。比如下面的普通CSS,用来设置header,footer的背景色:

.header {
    background-color: #0b5c20;
}
.footer {
    background-color: #0b5c20;
}
.footer a {
    font-weight: bold;
}

这段CSS代码显示出来CSS的一些缺陷,我们必须重复定义background-color定义 ,而且需要重复定义.footer 如果需要定义footer内部的元素。 而如果使用LESS,上面代码可以改写为:

@green: #0b5c20;
.header {
  background-color: @green;
}
.footer {
  background-color: @green;
  a {
    font-weight: bold;
  }
}

我们可以定义一个变量@green,此外也可以嵌套定义。

coffeeScript
CoffeeScript(http://coffeescript.org) 和JavaScript相比在语法结构上有所增强.CoffeeScript是一套JavaScript的转译语言。受到Ruby、Python与Haskell等语言的启发,CoffeeScript增强了JavaScript的简洁性与可读性。此外,CoffeeScript也新增了更复杂的功能,例如列表内涵(List comprehension)、模式匹配(Pattern matching)等。一般来说,CoffeeScript可以在不影响执行效能的情况下,缩短约三分之一的程式码长度。
例如下面的JavaScript代码:

math = {
    root: Math.sqrt,
    square: square,
    cube: function(x) {
        return x * square(x);
    }
};

使用coffeescript改写成:

math =
  root: Math.sqrt
  square: square
  cube: (x) -> x * square x

关于LESS和CoffeeScript的中文教程,可以关注本博客,之后也会提供相关教程。

 

Play Framework Web开发教程(34): 使用implicit简化代码

关于隐式变换和隐式参数的教程可以参考

  1. Scala 专题教程-隐式变换和隐式参数(1):概述
  2. Scala 专题教程-隐式变换和隐式参数(2):使用implicits的一些规则
  3. Scala 专题教程-隐式变换和隐式参数(3):隐含类型转换
  4. Scala 专题教程-隐式变换和隐式参数(4):转换被方法调用的对象
  5. Scala 专题教程-隐式变换和隐式参数(5):隐含参数(一)
  6. Scala 专题教程-隐式变换和隐式参数(6):隐含参数(二)
  7. Scala 专题教程-隐式变换和隐式参数(7):View 限定
  8. Scala 专题教程-隐式变换和隐式参数(8):当有多个隐含转换可以选择时

 
我们还是继续前面的例子,现在我们假定我们需要支持购物车功能,在网站的每页的右上角显现当前购物车中物品的数目。如下图所示:
20141013001

由于我们需要在每页都显现购物车,因此我们可以在main.scala.html添加购物车部分的模板:

@(cart: Cart)(content: Html)
<html>
    <head>
        <title>Paper-clip web shop</title>
        <link href="@routes.Assets.at("stylesheets/main.css")"
        rel="stylesheet">
    </head>
    <body>
        <div id="header">
            <h1>Paper-clip web shop</h1>
            <div id="cartSummary">
                <p>@cart.productCount match {
                    case 0 => {
                        Your shopping cart is empty.
                    }
                    case n => {
                        You have @n items in your shopping cart.
                    }
                }</p>
            </div>
        </div>
        @navigation()
        <div id="content">
        @content
        </div>
        <div id="footer">
            <p>Copyright paperclips.example.com</p>
        </div>
    </body>
</html>

这个模板中,我们增加了一个Cart参数,它定义了一个productCount方法,我们使用模式匹配根据购物车中物品的数量显示不同的内容。
main模板中多了一个参数,因此我们在使用这个模板时,也需要多传入一个参数,这就意味着我们也要修改之前定义人catalog模板:

@(products: Seq[Product], cart: Cart)
@main(cart) {
    <h2>Catalog</h2>
    @views.html.products.tags.productlist(products)
}

定义的Acton方法也需要添加一个参数:

def catalog() = Action { request =>
    val products = ProductDAO.list
    Ok(views.html.shop.catalog(products, cart(request)))
  }
def cart(request: Request) = {
// Get cart from session
}

这里我们定义的cart方法可以从request中获取Cart的实例。
由于main模板多了一个参数,因此相关的所有Actor都需要修改来适应这个变化,随着项目的增大,工作量就越来越大,显得有些繁琐。 还好我们可以借助于Scala的隐含参数来解决这个问题。
借助于implicit,我们可以修改catalog模板如下:

@(products: Seq[Product])(implicit cart: Cart)

我们把第二个参数cart,作为单独的一个参数列表,并使用implict来修饰。因此如果当前作用域定义了隐含的Cart实例,我们可以忽略第二个参数来调用这个模板,这可以通过修改定义Controller来定义隐含参数:
20141013002

这里cart方法为一implict方法,其参数也是implicit类型。同样actor方法的参数类型也定义成implicit类型。 如果我们现在忽略Cart参数来调用catalog模板,Scala编译器会在当前作用域查找可用的Cart对象,这将找到cart方法,这个方法需要RequestHeader类型的参数,也是implicit类型,Scala编译器查找当前可以使用的Request对象,它找到了。

为了进一步实现代码可重用,我们可以把这部分代码抽取出来定义一个withCart trait:

trait WithCart {
  implicit def cart(implicit request: RequestHeader) = {
    // Get cart from session
  }
}

这里凡是需要使用cart的controller对可以通过mixin 这个trait来获取一个Cart的隐含定义。

注:如果你在Controller当前作用域定义一个implicit Request,那么在当前作用域自动获得RequestHeader,Session, Flash,和Lang的一个隐含定义,这是因为Controller类型定义这些转换的隐含转换。

 

Play Framework Web开发教程(33): 结构化页面-组合使用模板

和你编写代码类似,你编写的页面也可以由多个小的片段组合而成,这些小的片段本身也可以由更小的片段构成。这些小片段通常是可以在其它页面重复使用的:有些部分可以用在所有页面,而有些部分是某些页面特定的。本篇介绍如何使用这些可重用的小模板来构成整个页面。
Includes
到目前为止的例子,我们只显示了HTML的片段,没有实例显示整个页面。下面我们给出完整的显现产品列表的代码和模板:

def catalog() = Action {
	val products = ProductDAO.list
	Ok(views.html.shop.catalog(products))
}

对应的页面模板app/views/products/catalog.scala.html

@(products: Seq[Product])
<!DOCTYPE html>
<html>
    <head>
        <title>paperclips.example.com</title>
        <link href="@routes.Assets.at("stylesheets/main.css")"
        rel="stylesheet">
    </head>
    <body>
        <div id="header">
            <h1>Products</h1>
        </div>
        <div id="navigation">
            <ul>
                <li><a href="@routes.Application.home">Home</a></li>
                <li><a href="@routes.Shop.catalog">Products</a></li>
                <li><a href="@routes.Application.contact">Contact</a></li>
            </ul>
        </div>
        <div id="content">
            <h2>Products</h2>
            <ul class="products">
            @for(product <- products) {
                <li>
                    <h3>@product.name</h3>
                    <p class="description">@product.description</p>
                </li>
            }
            </ul>
        </div>
        <footer>
            <p>Copyright paperclips.example.com</p>
        </footer>
    </body>
</html>

这样我们就定义了一个完整的HTML页面,但是我们在其中添加了不少和显示产品列表不直接相关的标记,比如Catalog不需要知道菜单是如何显示的。页面模块化和重用性不高。
一般来说,一个action方法只应负责最终页面的内容部分。对于很多网站来说,页头,页脚,导航条在不同页面是通用的,如下图:

20141006001

在这个页面样式中,Header,Navigation,Footer通常是不变的,需要变化的部分是由Page Content指定的部分。

因此我们可以把之前产品列表页面模板中的导航条部分抽取出来单独定义一个navigation页面模板:
views/navigation.scala.html

@()
<div id="navigation">
    <ul>
        <li><a href="@routes.Application.home">Home</a></li>
        <li><a href="@routes.Shop.catalog">Catalog</a></li>
        <li><a href="@routes.Application.contact">Contact</a></li>
    </ul>
</div>

然后修改之前的catelog.scala.html

@(products: Seq[Product])
<!DOCTYPE html>
<html>
    <head>
        <title>paperclips.example.com</title>
        <link href="@routes.Assets.at("stylesheets/main.css")"
        rel="stylesheet">
    </head>
    <body>
        <div id="header">
            <h1>Products</h1>
        </div>
        @navigation()
        <div id="content">
            <h2>Products</h2>
            <ul class="products">
            @for(product <- products) {
                <li>
                    <h3>@product.name</h3>
                    <p class="description">@product.description</p>
                </li>
            }
            </ul>
        </div>
        <footer>
            <p>Copyright paperclips.example.com</p>
        </footer>
    </body>
</html>

这个修改使得我们的页面变得更好,因为Catalog页面无需再知道如何显示导航条,这种把部分页面模板抽取出来单独写成一个可重复使用页面模板的方法叫”includes”,而抽取出来的模板叫”include”。

Layouts
前面的include使得我们的页面模板变好了,但是还是有改进的余地。我们可以看到Catelog页面还是显示整个页面,比如HTML DocType,head等等,这部分不应该由Catalog来负责显示。前面页面模板只有div content部分由Catalog来显示:

<h2>Products</h2>
<ul class="products">
@for(product <- products) {
	<li>
		<h3>@product.name</h3>
		<p class="description">@product.description</p>
	</li>
}
</ul>

其它部分都应该放在Catalog 模板之外。我们也可以使用之前的include技术,但不是最理想的。如果我们使用”include”技术,那么我们需要另外两个新的模板,一个为Content前面部分的内容,另外一个模板为Content后面部分的内容。这种方法不是很好,因为这些内容应该是放在一起的。
幸运的是使用Scala的组合功能,Play支持抽出所有的内容到一个模板中,从catalog.scala.html 模板中抽出所有不应由catalog负责的部分,到一个布局模板:

<!DOCTYPE html>
<html>
    <head>
        <title>paperclips.example.com</title>
        <link href="@routes.Assets.at("stylesheets/main.css")"
        rel="stylesheet">
    </head>
    <body>
        <div id="header">
            <h1>Products</h1>
        </div>
        @navigation()
        <div id="content">
          // Content here
        </div>
        <footer>
            <p>Copyright paperclips.example.com</p>
        </footer>
    </body>
</html>

我们把这部分模板存放在app/views/main.scala.html中,要使得这个模板变成可以重用的,我们为它定义一个参数content,其类型为html,修改如下:

@(content: Html)
<!DOCTYPE html>
<html>
    <head>
        <title>paperclips.example.com</title>
        <link href="@routes.Assets.at("stylesheets/main.css")"
        rel="stylesheet">
    </head>
    <body>
        <div id="header">
            <h1>Products</h1>
        </div>
        @navigation
        <div id="content">
        @content
        </div>
        <footer>
            <p>Copyright paperclips.example.com</p>
        </footer>
    </body>
</html>

使用这个模板如同调用Scala函数类型,views.html.main(content) ,使用这个布局模板,我们修改catelog页面如下:

@(products: Seq[Product])
@main {
    <h2>Products</h2>
    <ul class="products">
    @for(product <- products) {
        <li>
            <h3>@product.name</h3>
            <p class="description">@product.description</p>
            }
    </ul>
}

如果有需要,我们可以为main布局模板添加更多的参数,比如将title也作为参数,
比如把

@(content: Html)
<html>
	<head>
	<title>Paper-clip web shop</title>

修改成

@(title: String)(content: Html)
<html>
	<head>
	<title>@title</title>

还可以给参数定义缺省值,比如:

@(title="paperclips.example.com")(content: Html)

这样修改可以进一步参数化布局模板,通用性更强。