不會 JavaScript 也能做 AJAX 的 Web 框架--Lift Web Framework.

不會寫 JavaScript 也可以做 Ajax 表單和用 jQuery 的特效,你信不信?!反正我信了……因為,對於 JavaScript 的認知只一直停留在 alert("Hello World") 的我真的做出來了啊!

廢話不多說,直接看一下這個網站吧:遠子學姐的點心箱

這個網站很簡單,你點了右上角的『隨機選題』後,系統會隨機抽出三個題目,填到標題欄位中,然後在選題的過程中右上角會出現讀取中的 Spinner 圖示。等到選好了題目,把故事內容寫完後,系統會將三題故事加上你送出的時間後淡入顯示、同時把表單淡出。

這是看起來很標準的一個簡單的 Ajax 網頁,什麼是 Ajax 呢?Asynchronous JavaScript and XML。那我寫這個網頁的時候寫了幾行 JavaScript 呢?答案:零行!

沒錯,你沒看錯,真的是零行!靠的就是 Lift Web Framework 這東西,之前有稍微看一點資料,但沒仔細研究,沒想到前兩天看了還在撰寫中的 Simply Lift,心中不自覺地想大喊:靠!這是啥鬼,Lift 你也太強大了吧。於是就稍微來試試看,是不是真的不會 JavaScript 也可以寫出 Ajax 網頁,沒想到是真的…… :o

那這個網站要幾行程式呢?答:扣掉 HTML 的部份,164 行,而這 164 行裡有 47 行是註解,還沒扣掉用來排版的空白行,全部的程式碼我放在 GitHub 上了。

下面挑一些重點(其實也就是全部的程式了)來講,首先是 src/main/bootstrap/Boot.scala 這隻程式,內容如下:

package bootstrap.liftweb

import net.liftweb.common._
import net.liftweb.http._
import net.liftweb.sitemap._
import net.liftweb.sitemap.Loc._

class Boot {
    def boot {

        // 要找相關的程式的話,到 code 這個 package 找
        LiftRules.addToPackages("code")

        // 整個網站只有 Home 一頁,這頁會顯示在左邊的 Menu 上
        val entries = List(Menu.i("Home") / "index")
        LiftRules.setSiteMap(SiteMap(entries:_*))

        // 設定 Ajax 執行時的通知圖示
        LiftRules.ajaxStart = Full(() => LiftRules.jsArtifacts.show("ajax-loader").cmd)
        LiftRules.ajaxEnd = Full(() => LiftRules.jsArtifacts.hide("ajax-loader").cmd)

        // 強制所有 Request 的編碼是 UTF-8
        // 可有可無,加上可以防止被系服器預設的 JVM 編碼擾亂
        LiftRules.early.append(_.setCharacterEncoding("UTF-8"))
    }
}

這個程式顧名思義,就是網站啟動時的初始化設定,裡頭的重點只有第 15 和 16 行的部份,這邊是說替網站加入選單,這個選單只有 Home 這一個選項,他的網址是 /index (或是只有 /)這樣。

而且只要設定 SiteMap,就等於替整個網站加入了白名單的機制,只有 SiteMap 裡的資料是可以被存取到的。你可以看一下 GitHub 上的程式碼,裡面有個 src/main/webapps/static/index.html 檔案,但如果你用 http://lifttest.brianhsu.cloudbees.net/static/index.html 來存取的話,他是會吐錯誤訊息給你,不讓你看的。

接下來……看一下我們的首頁檔案長什麼樣子:

嗯!相當標準的 XHTML 原始碼,事實上,你把他拿去 W3C 的驗證器驗證的話,一個警告都不會出現!這是個一普通的 HTML 網頁,除了一些 CSS 裡設定的 class 很奇怪而已。這就是 Lift 很厲害而且我也很喜歡的一點--你的網站的外觀和內容是完全分離的,你可以用任何一個 HTML 編輯器來編輯 Lift 的網頁模版!

舉例來說,在上面我們定義了一個 div 的區塊,來顯示現在已經 4 點了的訊息,這看起來像是靜態網頁,但實際上在伺服器上跑的時候,那個 id="timeInHour" 的 span 標籤會被取代掉,變成實際的時間。

這是怎麼做的呢?接下來就來看看一切的元兇--Lift Snippet吧!這東西在 src/main/scala/code/snippet/AjaxExample.scala 中,用簡單的話來說,就是當 Lift 看到有 class="lift:AjaxExample.currentTime" 這樣的標籤的時候,會去找 AjaxExample.currentTime 這個函式。

而這個函式是一個型態為 NodeSeq => NodeSeq 的函式,白話文來說,就是把某一串 XML 轉成另一串 XML 的函式,而 XHTML 本來就符合 XML 的規範,所以 Lift 就把你模版裡的子節點丟給他,再把 currentTime 處理過後的 XHTML 顯示出來。

那這個函式要怎麼寫呢?很簡單……就像下面一樣,三行解決掉他,而重點只有一行--把 id=timeInHour 的 HTML 元素用實際上算出的數值來取代掉。這也是為什麼模版上有 span 標籤,但如果你去看上面的網頁卻發現這標籤不見了的原因,因為直接被取代掉了!

object SimpleSnippet
{
    /**
     *  處理模版裡 
的部份 */ def currentTime = { val dateFormatter = new SimpleDateFormat("h") val dateTime = Calendar.getInstance(TimeZone.getTimeZone("Asia/Taipei")).getTime val timeInHour = dateFormatter.format(dateTime) // 把模版裡 id=timeInHour 的 XHTML 元素用 timeInHour 變數取代掉 "id=timeInHour" #> timeInHour } }

我只能說……Lift 想出的這個 CSS Selector 實再太範規了啊!

剩下的,就是 Ajax 的部份,其實和上面差不多,只是我們在模版的第 14 行的 form 標籤裡加入了奇怪的 class="lift:form.ajax" 屬性而已,而加了這東西後,Lift 會自動幫我們把表單變成 Ajax 表單。

接下來我們要做的,就是定義表單送出後要做什麼動做而已:

package code.snippet


import net.liftweb.http._                       // For SHtml
import net.liftweb.util.Helpers._               // For #> Operator
import net.liftweb.http.js.JsCmds._             // For SetValById, Replace
import net.liftweb.http.js.jquery.JqJsCmds._    // For FadeIn / FadeOut

import net.liftweb.http.js.JsCmd
import net.liftweb.util.Helpers.TimeSpan

import scala.util.Random
import java.text.SimpleDateFormat
import java.util.{Date, Calendar, TimeZone}

/**
 *  三題故事題庫
 */
object Subject
{
    val subjects = List(
        "眼球", "櫻花", "文學少女", "游泳池", 
        "開學典禮", "熊貓", "竹葉", "計算機", 
        "窗戶", "點心", "袋鼠"
    )

    // 隨機選三個(把整個 List 洗牌後挑前三個=隨機選不重覆的三個題目)
    def randomTitle = Random.shuffle(subjects).take(3)
}

/**
 *  重頭戲……沒有 JavaScript 的 AJAX 表單和 JQuery 特效!!
 */
object AjaxExample {

    private var title0: String = _  // 第一個題目
    private var title1: String = _  // 第二個題目
    private var title2: String = _  // 第三個題目
    private var content: String = _ // 故事內容

    /**
     *  處理模版裡 
的部份 */ def randomTitle = { /** * 『隨機選題』按下去後會做的事情 * * 註:隨機洗牌挑題目這件事是在 Server 端發生的!! */ def chooseRandomTitle(): JsCmd= { val titles = Subject.randomTitle SetValById("title0", titles(0)) & // 把 HTML 裡 id=title01 的框框設成 title01 的值 SetValById("title1", titles(1)) & // 依此類推 SetValById("title2", titles(2)) // 總共設三個 } // 超神奇的 CSS Selector // // - "id=title0" #> .... // 這個是找到網頁上 id=title 的元件,然後用後面的取代掉的意思 // - SHtml.onSubmit(action) // 是說維持原來的元件,但當表單送出後,做 action 的動作 // - 上面合起來就是當表單送出後,把上面的 title0、title1、title2 設成這三個框框裡的值 // - SHtml.ajaxSubmit // 重頭戲:把網頁 id=randomTitle 的元件取成成按下去會 // 做 chooseRandomTitle 的 AJAX 按鈕 "id=title0" #> SHtml.onSubmit(title0 = _) & "id=title1" #> SHtml.onSubmit(title1 = _) & "id=title2" #> SHtml.onSubmit(title2 = _) & "id=randomTitle" #> SHtml.ajaxSubmit("隨機選題", chooseRandomTitle) } /** * 處理模版裡
的部份 */ def sendStory = { /** * 『點心寫好囉』按下去後會發生的事 * * 註:取得日期的動作也是在 Server 端發生的! */ def process(): JsCmd = { // 檢查是不是三個題目都有 val titleOK = title0.trim.length > 0 && title1.trim.length > 0 && title2.trim.length > 0 if (!titleOK) { return Alert("心葉……三題故事一定要有三個題目才行喲!") } // 檢查故事內文是不是空的 if (content.trim.length <= 0) { return Alert("心葉……故事太短囉!") } val dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") val dateTime = Calendar.getInstance(TimeZone.getTimeZone("Asia/Taipei")).getTime val timestamp = dateFormatter.format(dateTime) // 將網頁中 ID 是 fullTitle, fullStory, createTimestamp 的用後面的東西取代 // 並且把表單用 JQuery 做淡出隱藏、結果用 JQuery 淡入顯示 Replace("fullTitle", {title0 + "、" + title1 + "、" + title2}) & Replace("fullStory", {content}) & Replace("createTimestamp", {timestamp}) & FadeIn("test", TimeSpan(0), TimeSpan(2000)) & FadeOut("form", TimeSpan(0), TimeSpan(2000)) } // 在上面 randomTitle() 說明過了,一樣的東西 "name=content" #> SHtml.onSubmit(content = _) & "id=sendStory" #> SHtml.ajaxSubmit("點心寫好囉", process) } }

很簡單吧,randomTitle 裡定義了『隨機選題』按下去後的動作,sendStory 裡定義了『點心寫好囉』按下去後的動作,而這些動作都是 Scala 程式碼,包括使用 jQuery 的 FadeIn/FadeOut 功能,從頭到尾,我沒有寫過任何一行 JavaScript,但卻把 Ajax 表單做出來了。

到這邊我真的無言了……Lift 這個框架真的是神奇到無以復加,唯一可惜的是,文件很少,少得可憐,不然這個框架真的超有趣的!唯一的前題是腦袋要從物件導向和 MVC 跳開,走向 Functional Programming 以『轉換、轉換、再轉換』的思考方式,才會比較容易理解那些 Snuppet / CSS Selector 是啥東西。

回響