默认情况下,Lift的菜单系统对于当前页面是没有<a/>这个tag的,也就是说假如你处在页面A上,菜单里的A是没有URL的。但是有些时候确实又需要,enable的方法很简单:
1 | <pre lang="html" colla="+"><span class="lift:Menu.builder?linkToSelf=true"></span> |
跟所有Lift的feature一样,支持REST也是非常非常得简单。
Mixin这个trait:
Boot.scala里面加上:
1 | LiftRules.dispatch.append(AdminAPI) |
如果不需要创建session(不创建著名的S对象),还可以:
1 | LiftRules.statelessDispatchTable.append(AdminAPI) |
然后就是URL匹配:
1 2 3 4 5 6 | serve { case "api" :: "add" :: Nil JsonGet _ => add case "api" :: "delete" :: Nil JsonGet _ => delete case "api" :: "edit" :: Nil JsonGet _ => edit case "api" :: "get" :: Nil JsonGet _ => get } |
以上都是GET的JSON接口。JsonGet会检查HTTP头里面的Accept以确认client是否支持JSON,这里需要注意的是“*/*”表示所有都接受(这个应该已经在2.3里面被修复掉了);当然如果Accept里面不支持JSON,JsonGet还会去查询URL是否是.json结尾。
具体解释一下上面的例子。
对于URL为 http://server/api/add.json 的请求,第一条匹配规则命中,最后那个“_”是Req,然后调用add方法,add的返回值需要是LiftResponse类型;其实LiftReponse是个啥都不干的trait,有很多实现,JsonResponse、XmlResponse等等,总之几乎不用特别在意,Lift里面能作为response返回给client的都能用。
其他的匹配类似处理。
给个add方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 | private def add = { val linkId = param(ShortenedUrl.originUrl.name).map(x => { shortenedUrl.find(ShortenedUrl.originUrl.name -> x).map(_.linkId.value) or { val tmp = (DependencyFactory.inject[NextIdGenerator].open_! !? 'id).toString shortenedUrl.createRecord.linkId(tmp).originUrl(x).shortUrl(Props.get(Site).open_! + "/" + tmp).date(new Date). ip(containerRequest.map(_.remoteAddress).toString).clickCount(0).save Full(tmp) } }) (StatusField -> linkId.map(_.map(x => SuccessStatus)).openOr(Full(FailedStatus)).openOr(FailedStatus)) ~ (ShortenedUrl.linkId.name -> linkId.openOr(Full("")).openOr("")) } |
返回一个JSON对象{status: “successful”, linkId: “1″}。
Lift利用Scala对DSL的支持把JSON整得很舒服,几乎跟js原生的用起来没区别:
1 2 3 | ("name" -> "honnix") ~ ("address" -> "somewhere") ~ ("phone" -> List("111", "222", "333")) |
等价于:
1 2 3 | {name: "honnix", address: "somewhere", phone: ["111", "222", "333"]} |
具爽吧?
基本上是转帖:http://www.assembla.com/wiki/show/liftweb/Internationalization。
但是还上要说一下,设计得太爽了。没怎么用过其他的web框架,不过Lift提供的真是好用。尤其是最后一点,template也可以直接国际化:index_en_US.html,index_zh_CN.html。不是所有都可以直接通过resource bundle来进行翻译,所以直接对template也就是页面本身来搞,真是很爽。
其实说Lift里不够准确,Lift对MapReduce没做什么封装,基本上就是直接调用mongo-java-driver的API。
举例说明,计算所有URL的点击次数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def countClicks = { MongoDB.useCollection(shortenedUrl.collectionName) { x => { val map = """function() { emit("totalClickCount", this.%s); }""" format ShortenedUrl.clickCount.name val reduce = """function(key, values) { return Array.sum(values); }""" val results = x.mapReduce(map, reduce, null, null).results /** * all numbers returned by mongodb is Double since this is how number defined by javascript */ if (results.hasNext) results.next.get("value").asInstanceOf[Number].intValue.toString else "0" } } } |
map的时候把this.clickCount的值塞给”totalClickCount”这个key,reduce把所有这些值加起来,MongoDB会生成一个临时的collection,从里面选择“value”这个字段就可以了。
需要注意的是所有返回的数值类型都是Double,虽然BSON里对各种类型都有定义,但是目前为止MongoDB只支持返回Double。对js了解的人可能会比较清楚,但我就惨了。调了很长时间,一个bit一个bit的看,甚至还去MongoDB的JIRA上问:http://jira.mongodb.org/browse/SERVER-2688。土死了……
说来惭愧,从来没写过分页的代码,知道是怎么回事儿,但没干过总归差了这么一点儿。
MongoDB和传统的数据库类似,提供了skip和limit,前者用来跳过一批记录,后者用来选择多少条。怎么用这里有很详细的解释:http://www.mongodb.org/display/DOCS/Advanced+Queries,就不多说了。重点讨论Lift里面怎么用。
分页通常有这么几个条件:排序字段、排序方式(升序还是降序)、每页显示条数、当前页面。这些参数可以通过URL传递,譬如:sort-by=linkId&sort-order=-1&perpage=3&page=2,然后HTTP GET发送。
对于这种per request的参数,当然可以用的时候直接从著名的“S”里面取,也可以用RequestVar:
1 2 3 4 5 | object page extends RequestVar[Int](S.param("page").openOr("1").toInt) object perpage extends RequestVar[Int](S.param("perpage").openOr("10").toInt) object offset extends RequestVar[Int]((page - 1) * perpage) object sortBy extends RequestVar[String](S.param("sort-by").openOr(ShortenedUrl.linkId.name)) object sortOrder extends RequestVar[Int](S.param("sort-order").openOr("-1").toInt) |
这其中offset表示当前页的起始条目,-1表示降序,默认排序字段为linkId。
只有这些还不过瘾,我们再加上搜索条件:
1 2 3 4 | object search extends RequestVar[String](S.param("search").openOr("")) object searchIn extends RequestVar[String](S.param("search-in").openOr(ShortenedUrl.originUrl.name)) object clickFilter extends RequestVar[String](S.param("click-filter").openOr("gte")) object clickLimit extends RequestVar[String](S.param("click-limit").openOr("")) |
默认搜索originUrl,点击次数大于等于某个值。
有过数据库搜索设计经验的人一般都会这么干:
1 | select * from table where 1=1 |
然后在后面拼搜索条件:
1 | and originUrl like '%google%' and clickCount >= 10 sort by linkId skip 2 limit 10 |
我们如法炮制,不过稍微改变一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | object clickObject extends RequestVar[JObject]( if (clickLimit.isEmpty) JObject(Nil) else (ShortenedUrl.clickCount.name -> (("$" + clickFilter) -> clickLimit.is.toInt)) ) object searchObject extends RequestVar[JObject]( if (search.isEmpty) JObject(Nil) else ("$where" -> ("this." + searchIn + ".indexOf('" + search + "') != -1")) ) object findOptions extends RequestVar[List[FindOption]]( List(Skip(offset), Limit(perpage)) ) shortenedUrl.findAll(clickObject.is ~ searchObject.is, (sortBy.is -> sortOrder.is), findOptions: _*) |
如果没有搜索条件,返回Nil表示空的List;否则(x -> y)构建一个JObject(这里是Scala的语法糖衣,背后发生了很多);$where那里纯粹是MongoDB特有的,根本就是js;Skip和Limit跟SQL的含义完全相同。这样对最后一个statement的解读也就很自然了。
数据的事情搞定了,剩下就是页面处理,Lift再次表示了自己的牛逼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | def generateNav: NodeSeq = { def generateHref(page: Int) = "/?search=" + search + "&sort-by=" + sortBy + "&sort-order=" + sortOrder + "&search-in=" + searchIn + "&click-filter=" + clickFilter + "&click-limit=" + clickLimit + "&perpage=" + perpage + "&page=" + page val left = if (page.is != 1) <a title="{"«" href="{generateHref(page">«</a> else Nil val pages = (1 to totalPages).toList.map { x => if (x == page.is) <strong> {"[" + x + "]"} </strong> else <a title="{"Page" href="{generateHref(x)}"> {x} </a> } val right = if (page.is != totalPages) <a title="{"Go" href="{generateHref(page.is">»</a> else Nil left ++ pages ++ right } |
越来越多的公司和组织掺和到NoSQL运动里来,我也跟风玩玩儿MongoDB。
Lift对MongoDB已经有了很好的支持:http://www.assembla.com/wiki/show/liftweb/MongoDB
下面这段例子是一个非常简单的model设计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class ShortenedUrl extends MongoRecord[ShortenedUrl] with MongoId[ShortenedUrl] { def meta = ShortenedUrl object linkId extends StringField(this, 10) object originUrl extends StringField(this, 500) object shortUrl extends StringField(this, 100) object date extends DateField(this) object ip extends StringField(this, 15) object clickCount extends IntField(this) } object ShortenedUrl extends ShortenedUrl with MongoMetaRecord[ShortenedUrl] class NextId extends MongoRecord[NextId] with MongoId[NextId] { def meta = NextId object next extends StringField(this, 10) } object NextId extends NextId with MongoMetaRecord[NextId] |
这个东西是我仿照yourls做的数据模型。很简单,两个collection,一个记录缩短域名的所有信息,一个记录下一个URL的id是什么。
查找:
1 | ShortenedUrl.find(ShortenedUrl.linkId.name -> id) |
存储:
1 2 | currentShortenedUrl.linkId(linkId).shortUrl(Props.get("site").open_! + "/" + linkId). ip(containerRequest.open_!.remoteAddress).clickCount(0).save |
根本不用描述,太简单了。
具体怎么回事儿,这里写得很清楚,就不啰嗦了:http://www.assembla.com/wiki/show/liftweb/Binding_via_CSS_Selectors。
有一点需要特别说明一下:为了彻底把页面设计和后台代码分离,我希望所有和页面布局展示的地方都用静态html实现,然后在snippet里面作binding。这时候就需要TemplateFinder了。
看下面这段:
1 2 3 4 5 6 7 8 9 10 | TemplateFinder.findAnyTemplate(List(TemplatesHidden, "edit")) map { "#real-content ^^")#> "true" andThen "#real-conent [id]" #> (EditPrefix + shortenedUrl.linkId.value) & "#edit-url [name]" #> urlId & "#edit-url [value]" #> shortenedUrl.originUrl.value & "#edit-url [id]" #>; urlId & "#save-button [onclick]" #> js.toJsCmd & "#save-button [id]" #> (EditPrefix + "submit-" + shortenedUrl.linkId.value) & "#cancel-button [onclick]" #> ("hide_edit('" + shortenedUrl.linkId.value + "')") & "#cancel-button [id]" #> (EditPrefix + "close-" + shortenedUrl.linkId.value) |
edit.html是一个完整的静态html,用来显示一个table,上面代码做的事情就是选中id为real-content的这个节点,然后对它做CSS的binding以实现动态页面。
最近在试着用Lift写一个缩URL的东西,由于这个东西相对来说比之前做的小玩意儿复杂一些,写几篇东西作为记录,省得总是要解决之前早就已经解决过的问题。
那本著名的书上也没写,但是应该是比较常用的功能:如何绑定一个attribute。
有一段html,内容如下:
1 2 3 4 5 6 | <div>
<dl id="xxx">
<dt>xxx</dt>
<dd>yyy</dd>
</dl>
</div> |
要实现的是把“dl”的“id”动态绑定到一个值,这样可以直接使用数据库中的id,并且也方便jQuery操作。lift里面可以这样实现:
首先把上面的html改造一下:
1 2 3 4 5 6 | <div>
<dl entry:id="xxx">
<dt>xxx</dt>
<dd>yyy</dd>
</dl>
</div> |
然后Scala代码这样写:
1 2 3 | bind("entry", xhtml, AttrBindParam("id", Text(article.id.is.toString), "id") ) |