歡迎閱讀第二年度的 精通 Grails。正如我在 2008 年的最後一篇文章中許諾的一樣,在新的一年將 使用新的應用程序。再見了,Trip Planner!讓我們歡迎 blog 發布系統(blog publishing system)!
我已經將這個應用程序命名為 Blogito。在西班牙語中,它表示 “little blog”,或者是對笛卡兒 的 Cogito ergo sum(“我思故我在”)表示敬意。可從 blogito.org 下載這個完整的應用程序。在接 下來的幾篇文章中,您將一步步構建核心的功能。
這篇文章的重點是顯著地更改 Grails 應用程序的外觀。去年的 Trip Planner 的外觀很怪異,恐怕 只有開發人員才會喜歡(說句公道話,與外觀相比,我對核心功能更感興趣)。在本文中,通過使用一些 CSS 和局部模板進行調整,將得到一個外觀新穎的 Grails 應用程序。在這個過程中,您還可以簡單溫習 一下 Grails 特性,比如 scaffold、自動時間戳、修改默認模板、創建自定義 TagLib,以及調整關鍵配 置文件(比如 Bootstrap.groovy 和 URLMapper.groovy)。
在開始之前,必須安裝 Grails 1.1。撰寫本文時,它還是 beta 版。
安裝 Grails 1.1
Grails 在 Java 1.5 或 1.6 上運行表現最佳。通過命令提示符輸入 java -version,確保 Java 版 本是比較新的。
Java 1.5 或 1.6 就緒之後,安裝 Grails 的步驟就很簡單了:
從 Grails 站點 下載 grails.zip 文件。
解壓縮 grails.zip。
創建一個 GRAILS_HOME 環境變量。
將 GRAILS_HOME/bin 添加到 PATH。
如果您使用的應用程序是使用上一版本的 Grails 編寫的,則可以輸入 grails upgrade 將其遷移到 最新的版本。但如果需要處理多個版本的 Grails,應該怎麼辦呢?
如果運行的是 UNIX®-esque OS(UNIX、Linux®,或 OS X)系統,通過將 $GRAILS_HOME 環 境變量指向 symlink 就可以輕松處理 Grails 的多個版本。在我的系統上,將 GRAILS_HOME 指向 /opt/grails。這個步驟完成之後,通過快捷的 ln -s 就可以在各個版本之間切換,如清單 1 所示:
清單 1. 為 UNIX、Linux 或 Mac OS X 系統上的 $GRAILS_HOME 創建一個 symlink
$ ln -s /opt/grails-1.1-beta1 grails
$ ls -l | grep "grails"
lrwxr-xr-x 1 sdavis admin 17 Dec 5 11:12 grails - > grails-1.1-beta1/
drwxr-xr-x 14 sdavis admin 476 Nov 10 2006 grails- 0.3.1
drwxr-xr-x 16 sdavis admin 544 Feb 9 2007 grails-0.4.1
drwxr-xr-x 17 sdavis admin 578 Apr 6 2007 grails-0.4.2
drwxr-xr-x 17 sdavis admin 578 Jun 15 2007 grails-0.5
drwxr-xr-x 19 sdavis admin 646 Jul 30 2007 grails-0.5.6
drwxr-xr-x 18 sdavis admin 612 Sep 18 2007 grails-0.6
drwxr -xr-x 19 sdavis admin 646 Feb 19 2008 grails-1.0
drwxr-xr-x 18 sdavis admin 612 Apr 5 2008 grails-1.0.2
drwxr-xr-x 18 sdavis admin 612 Oct 9 21:46 grails-1.0.3
drwxr-xr-x 18 sdavis admin 612 Nov 24 20:43 grails-1.0.4
drwxr-xr-x 18 sdavis admin 612 Dec 5 11:13 grails-1.1-beta1
在 Windows® 系統上,最好是直接更改 %GRAILS_HOME% 變量。在變更之後,不要忘記重新啟動現有的命 令提示符。
輸入 grails -version 以確保使用了最新的版本,並且正確設置了 GRAILS_HOME 變量。現在,輸入 應該如清單 2 所示:
清單 2. grails -version 的輸出結果
$ grails -version
Welcome to Grails 1.1-beta2 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails
現在 Grails 1.1 已經安裝完成,可以創建新的應用程序了。
創建應用程序
輸入 grails create-app blogito 以生成初始的目錄結構。轉到新的 blogito 目錄並輸入 grails create-domain-class Entry,以創建表示 blog 條目的類。在 grails-app/domain 找到 Entry.groovy ,並添加清單 3 中的代碼:
清單 3. 創建 Entry 類
class Entry {
static constraints = {
title()
summary(maxSize:1000)
dateCreated()
lastUpdated()
}
String title
String summary
Date dateCreated
Date lastUpdated
}
每個 Entry 有一個 title 和 summary 字段。將 maxSize 限制范圍設置為 1,000 個字符,這會導致 動態地構造 HTML 表單,從而為 summary 字段提供文本區域(而不是簡單的文本字段)。
記住,dateCreated 和 lastUpdated 是 Grails 中比較神奇的字段名。這些時間戳字段非常適合 blog 應用程序 — 它們允許在列表的頂部保留最新的 Entry。
在域類准備就緒之後,下一步就是創建一個控制器。輸入 grails create-controller Entry。將清單 4 中的代碼添加到 grails-app/controllers/EntryController.groovy:
清單 4. 創建 EntryController
class EntryController {
def scaffold = Entry
}
表面上看起來很簡單的 def scaffold = Entry 行指示 Grails 為 Entry 類構造其余的支持。您隨後 將獲得一個條目表,其中 Entry 類中的每個字段都有一個列(以及一個主鍵 ID 字段和一個樂觀鎖定的 版本字段)。您還獲得完整的 Groovy 服務器頁面(Groovy Server Pages,GSP),它們提供很普通但至 關重要的 Create/Retrieve/Update/Delete (CRUD) 功能。
輸入 grails run-app 並通過 Web 浏覽器訪問 http://localhost:8080/blogito。單擊 EntryController,然後單擊 New Entry。這樣做的好處是所有 Entry 字段都出現在創建表單中(如圖 1 所示)。但這也有不好的地方 — 用戶不應該處理這些時間戳字段。您需要調整默認的模板來解決這個問 題。
圖 1. Create Entry 表單中可編輯的時間戳字段
調整默認模板
您可以輸入 grails generate-views Entry 手動地從 GSP 文件中刪除 dateCreated 和 lastUpdated 字段,但這不能從根本上解決問題。您可能希望這些字段永遠不出現在創建和編輯表單中。最好是在 def scaffold 中更改模板。
輸入 grails install-templates。在 src/templates/scaffolding 中查找 create.gsp 和 edit.gsp 。在每個文件中,將 dateCreated 和 lastUpdated 添加到 excludedProps,如清單 5 所示:
清單 5. 從 list.gsp 和 show.gsp 模板中刪除時間戳字段
excludedProps = ['version',
'id',
'dateCreated',
'lastUpdated',
Events.ONLOAD_EVENT,
Events.BEFORE_DELETE_EVENT,
Events.BEFORE_INSERT_EVENT,
Events.BEFORE_UPDATE_EVENT]
重啟 Grails,確保時間戳字段不再出現(參見圖 2):
圖 2. 不包含時間戳字段的表單
更改排序的順序
添加新條目時,默認情況下是根據 ID 對表進行排序的。blog 通常以逆時針順序對條目進行排序 — 最新的排在前面。在以前版本的 Grails 中,要更改默認的排序順序,則必須在 EntryController.groovy 中手動編輯列表閉包。在現有的代碼行下面添加兩個排序代碼行並不困難(見 清單 6)。問題是不能再從幕後動態構建這個代碼(可以查找 src/templates/scaffolding/Controller.groovy 或輸入 grails generate-controller Entry 查看默認 的底層實現)。
清單 6. Grails 1.0.x 中的排序
def list = {
if(!params.max) params.max = 10
if(!params.sort) params.sort = "lastUpdated"
if(!params.order) params.order = "desc"
[ entryList: Entry.list( params ) ]
}
Grails 1.1 將一個很簡單但極為有用的特性添加到靜態映射塊,即 sort。將清單 7 中的映射塊添加 到 Entry.groovy。通過在域類中處理排序,您可以繼續對控制器執行 def scaffold 操作。
清單 7. 將 sort 添加到 static mapping 塊
class Entry {
static constraints = {
title()
summary(maxSize:1000)
dateCreated()
lastUpdated()
}
static mapping = {
sort "lastUpdated":"desc"
}
String title
String summary
Date dateCreated
Date lastUpdated
}
重啟 Grails,確保編輯後的條目移動到列表的頂端,如圖 3 所示:
圖 3. 驗證新的排序順序
在開發模式下創建偽記錄
每次重啟 Grails 時將丟失現有的條目,您注意到了嗎?記住,這是一個特性,而不是 bug。在每次 啟動 Grails 時將創建條目表,並且在關閉 Grails 時刪除它們。打開 grails- app/conf/DataSource.groovy 驗證這個特性。很明顯,開發模式中的 db-create 值設置為 create-drop 。
可以將該值更改為 update,但這也不是很理想。在開發過程的前期,模式是很不穩定的 — 您可以隨 時添加或刪除字段,或修改限制條件等等。在所有東西穩定下來之前,我覺得最好將 db-create 設置為 create-drop。
在開發模式中經常要重新輸入樣例數據,為了使這個操作沒那麼繁瑣,可以為 grails- app/conf/BootStrap.groovy 添加一些邏輯。清單 8 中的代碼在 Grails 每次啟動時插入新的記錄:
清單 8. 在開發模式中添加偽記錄
import grails.util.GrailsUtil
class BootStrap {
def init = { servletContext ->
switch(GrailsUtil.environment){
case "development":
new Entry(
title:"Grails 1.1 beta is out", summary:"Check out the new features").save()
new Entry(
title:"Just Released - Groovy 1.6 beta 2", summary:"It is looking good.").save ()
break
case "production":
break
}
}
def destroy = {
}
}
再次重啟 Grails。這一次,條目表中將出現現有的記錄,如圖 4 所示:
圖 4. 在引導時出現的偽記錄
改善列表的外觀
列表視圖中的默認 HTML 表對入門人員已經足夠好,但對 Blogito 而言,這明顯不是長期解決辦法。 blog 頁面通常垂直地顯示 date、title 和 summary 字段,而不是橫向地顯示(每次顯示一個字段)。
為進行這種更改,輸入 grails generate-views Entry。前面動態構造的 GSP 文件現在應該出現在 grails-app/views/entry 中。在文本編輯器中打開 list.gsp。在頭部將標題從 Entry List 更改為 Blogito。刪除 <h1> 和 <g:if> 塊,然後用清單 9 中的代碼代替現有的 <div class="list">。
清單 9. 更改 list.gsp 視圖
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<div class="entry">
<span class="entry-date">${entryInstance.lastUpdated}</span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title} </g:link></h2>
<p>${entryInstance.summary}</p>
</div>
</g:each>
</div>
注意,這些代碼是經過大大簡化的。可以刪除 <fieldValue> 標記 — 它們幫助將域類綁定到 HTML 表單字段,但在這裡沒有實用價值。每個 Entry 都包含在一個指定的 <div> 中,而 lastUpdated 字段則包含在指定的 <span> 中。這些類屬性連接到隨後將構建的 CSS 格式中。 title 和 summary 字段包含在普通的 HTML 頭部和段落標記中。
在浏覽器中刷新列表視圖(見圖 5)。這還不算是進步。但是添加一些新的 CSS 指令之後,它的外觀 將有很大的改善。
圖 5. 沒有使用 CSS 的新列表
將清單 10 中的 CSS 添加到 web-app/css/main.css 的底部:
清單 10. list.gsp 視圖的 CSS 自定義
/* Blogito customizations */
.entry {
padding-bottom: 2em;
}
.entry-date {
color: #999;
}
再次刷新浏覽器將看到更加好看的外觀(見圖 6)。現在還沒有充分利用 CSS,但是已經擁有一個好 的起點。
圖 6. 帶有 CSS 的新列表
創建 Date TagLib
現在,需要使 lastUpdated 日期外觀更加友好。最好將可重用代碼片段放在自定義 TagLib 中。輸入 grails create-tag-lib Date。將清單 11 中的代碼添加到 grails-app/taglib/DateTagLib.groovy:
清單 11. 針對 DateTagLib 的代碼
import java.text.SimpleDateFormat
class DateTagLib {
def longDate = {attrs, body ->
//parse the incoming date
def b = attrs.body ?: body()
def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
//if no format attribute is supplied, use this
def pattern = attrs["format"] ?: "EEEE, MMM d, yyyy"
out << new SimpleDateFormat(pattern).format(d)
}
}
現在,將 lastUpdated 字段包含在 grails-app/views/entry/list.gsp 中剛才創建的 <g:longDate> 標記中,如清單 12 所示:
清單 12. 在 list.gsp 中使用 <g:longDate>
<div class="entry">
<span class="entry-date"><g:longDate>${entryInstance.lastUpdated} </g:longDate></span>
<h2>${entryInstance.title}</h2>
<p>${entryInstance.summary}</p>
</div>
重啟 Grails 並刷新 Web 浏覽器。您將看到日期的新格式,如圖 7 所示:
圖 7. 使用自定義 <g:longDate> 標記創建的新日期格式
創建局部模板
這個布局非常漂亮。我打算在 show.gsp 中重用它。在 grails-app/views/entry 中創建 _entry.gsp ,並添加清單 13 中所示的代碼(當然,可以從 list.gsp 剪切粘貼過來)。
清單 13. 針對 _entry.gsp 的代碼
<div class="entry">
<span class="entry-date"><g:longDate>${entryInstance.lastUpdated} </g:longDate></span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title} </g:link></h2>
<p>${entryInstance.summary}</p>
</div>
為了使用剛才創建的局部模板,需要像清單 14 那樣調整 list.gsp:
清單 14. 在 list.gsp 中使用 _entry.gsp 局部模板
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
</g:each>
</div>
現在還可以在 list.gsp 中重用局部模板,如清單 15 所示:
清單 15. 在 show.gsp 中使用 _entry.gsp 局部模板
<div class="body">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
<div class="buttons">
<!-- snip -->
</div>
</div>
在浏覽器中刷新列表視圖。它將和前面完全一樣。現在單擊條目的標題,確保它也適用於這個視圖。
自定義頭部
各個部分將協調地顯示。現在需要用自己的標志來代替 Grails 標志。
我沒有看到在 list.gsp 或 show.gsp 的其他地方引用了 Grails 徽標。記住,Grails 使用 SiteMesh 將最終頁面的不同部分結合起來。查看 grails-app/views/layouts/main.gsp 就會看到包含 grails_logo.jpg 文件的位置。
在 grails-app/views/layouts 中創建另一個名為 _header.gsp 的局部模板。添加清單 16 中的代碼 。注意,Blogito 是一個鏈接到主頁的超鏈接。
清單 16. 針對 _header.gsp 局部模板的代碼
<div id="header">
<p><g:link class="header-main" controller="entry">Blogito</g:link></p>
<p class="header-sub">A tiny little blog</p>
</div>
現在像清單 17 那樣編輯 main.gsp,以包含 _header.gsp 文件:
清單 17. 使用新 _header.gsp 局部模板的 Main.gsp
<body>
<div id="spinner" class="spinner" style="display:none;">
<img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
</div>
<g:render template="/layouts/header"/>
<g:layoutBody />
</body>
最後,再為 web-app/css/main.css 添加一些代碼,如清單 18 所示:
清單 18. _header.gsp 局部模板的 CSS 格式
#header {
background: #67c;
padding: 2em 1em 2em 1em;
margin-bottom: 1em;
}
a.header-main:link, a.header-main:visited {
color: #fff;
font-size: 3em;
font-weight: bold;
}
.header-sub {
color: #fff;
font-size: 1.25em;
font-style: italic;
}
刷新浏覽器查看發生了什麼變化(見圖 8)。單擊條目的標題,然後在頭部單擊 Blogito 導航到主頁 。
圖 8. 展示新的頭部
在登錄之前隱藏導航欄
您還需要處理一個容易弄錯的標志,它表示這是一個 Grails 應用程序:導航欄。盡管我們在下一篇 文章中才進行身份驗證,但是現在可以為未驗證的用戶關閉導航欄。這可以通過將 <div> 包含在 簡單的 <g:if> 測試來實現。這個測試查找存儲在會話范圍中的 user 變量。
像清單 19 那樣修改 list.gsp 和 show.gsp:
清單 19. 在登錄之前隱藏導航欄
<g:if test="${session.user}">
<div class="nav">
<span class="menuButton">
<a class="home" href="${createLinkTo(dir:'')}">Home</a>
</span>
<span class="menuButton">
<g:link class="create" action="create">New Entry</g:link>
</span>
</div>
</g:if>
在 show.gsp 中,在按鈕 <div> 的周圍添加相同的測試(您最不願意看到的事情就是用戶編輯 未經驗證或刪除 blog 條目,不是嗎?)。
最後,對 list.gsp 的外觀進行調整。將 paginateButtons <div> 從 body <div> 移出 ,如清單 20 所示。這使導航欄能夠橫跨整個屏幕,從而在屏幕的底部添加一個漂亮的可視錨。
清單 20. 將 paginateButtons <div> 從 body <div> 移出,改善外觀
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="layout" content="main" />
<title>Blogito</title>
</head>
<body>
<g:if test="${session.user}">
<div class="nav">
<span class="menuButton">
<a class="home" href="${createLinkTo(dir:'')}">Home</a>
</span>
<span class="menuButton">
<g:link class="create" action="create">New Entry</g:link>
</span>
</div>
</g:if>
<div class="body">
<div class="list">
<g:each in="${entryInstanceList}" status="i" var="entryInstance">
<g:render template="entry" bean="${entryInstance}" var="entryInstance" />
</g:each>
</div>
</div>
<div class="paginateButtons">
<g:paginate total="${Entry.count()}" />
</div>
</body>
</html>
再添加一些 CSS,如清單 21 所示,確保 paginateButtons <div> 出現在 body <div> 的底部,而不是旁邊:
清單 21. 確保 paginateButtons <div> 出現在屏幕底部的 CSS
.paginateButtons{
clear: left;
}
最後一次刷新浏覽器。您的屏幕應該如圖 9 所示:
圖 9. 隱藏導航欄
設置主頁
現在,一切准備就緒了,此時應該將 EntryController 設置為默認主頁。為此,需要添加一個將 /( URL http://localhost:9090/blogito/ 中的尾部反斜槓)重新定向到 EntryController 的映射。根據清 單 22 編輯 grails-app/conf/UrlMappings.groovy:
清單 22. 將 EntryController 設置為默認主頁
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(controller:"entry")
"500"(view:'/error')
}
}
結束語
本文的目標是顯示如何改變 Grails 應用程序的外觀。僅需幾行 CSS 就可以改變顏色、字體和塊元素 周圍的空間。通過局部模板和 TagLibs 可以創建一些可重用的代碼片段。最後,您還可以利用 Grails 框架的所有優點,並且獲得一個擁有獨特外觀的應用程序。
下一期文章繼續探討 Blogito 應用程序。您將添加一個 User 域類,從而讓多個人添加 blog 條目。 此外,您還將研究 Grails 編解碼器,並且進一步了解自定義 URL 映射。不要忘記可以通過 http://blogito.org 下載完整的應用程序。到那時,就可以享受精通 Grails 帶來的樂趣了。