在這一期的 精通 Grails 中,Scott Davis 演示如何通過使用層疊樣式表(CSS)、模板、標記庫(TagLib)等技術來對 Grails 應用程序的外觀進行有趣的更改。
歡迎閱讀第二年度的 精通 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 upgrade 將其遷移到最新的版本。但如果需要處理多個版本的 Grails,應該怎麼辦呢?
如果運行的是 UNIX®-esque OS(UNIX、Linux®,或 OS X)系統,通過將 $GRAILS_HOME 環境變數指向 symlink 就可以輕鬆處理 Grails 的多個版本。在我的系統上,將 GRAILS_HOME 指向 /opt/grails。這個步驟完成之後,通過快捷的 ln -s 就可以在各個版本之間切換,如清單 1 所示:
$ 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 所示:
$ 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 中的代碼:
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:
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 所示)。但這也有不好的地方 — 用戶不應該處理這些時間戳欄位。您需要調整默認的模板來解決這個問題。
調整默認模板
您可以輸入 grails generate-views Entry 手動地從 GSP 文件中刪除 dateCreated 和 lastUpdated 欄位,但這不能從根本上解決問題。您可能希望這些欄位永遠不出現在創建和編輯表單中。最好是在 def scaffold 中更改模板。
輸入 grails install-templates。在 src/templates/scaffolding 中查找 create.gsp 和 edit.gsp。在每個文件中,將 dateCreated 和 lastUpdated 添加到 excludedProps,如清單 5 所示:
excludedProps = ['version', 'id', 'dateCreated', 'lastUpdated', Events.ONLOAD_EVENT, Events.BEFORE_DELETE_EVENT, Events.BEFORE_INSERT_EVENT, Events.BEFORE_UPDATE_EVENT] |
重啟 Grails,確保時間戳欄位不再出現(參見圖 2):
更改排序的順序
添加新條目時,默認情況下是根據 ID 對錶進行排序的。blog 通常以逆時針順序對條目進行排序 — 最新的排在前面。在以前版本的 Grails 中,要更改默認的排序順序,則必須在 EntryController.groovy 中手動編輯列表閉包。在現有的代碼行下面添加兩個排序代碼行並不困難(見清單 6)。問題是不能再從幕後動態構建這個代碼(可以查找 src/templates/scaffolding/Controller.groovy 或輸入 grails generate-controller Entry 查看默認的底層實現)。
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 操作。
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 所示:
在開發模式下創建偽記錄
每次重啟 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 每次啟動時插入新的記錄:
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 所示:
改善列表的外觀
列表視圖中的默認 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">。
<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 指令之後,它的外觀將有很大的改善。
將清單 10 中的 CSS 添加到 web-app/css/main.css 的底部:
/* Blogito customizations */ .entry { padding-bottom: 2em; } .entry-date { color: #999; } |
再次刷新瀏覽器將看到更加好看的外觀(見圖 6)。現在還沒有充分利用 CSS,但是已經擁有一個好的起點。
創建 Date TagLib
|
現在,需要使 lastUpdated 日期外觀更加友好。最好將可重用代碼片段放在自定義 TagLib 中。輸入 grails create-tag-lib Date。將清單 11 中的代碼添加到 grails-app/taglib/DateTagLib.groovy:
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 所示:
<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 所示:
創建局部模板
這個布局非常漂亮。我打算在 show.gsp 中重用它。在 grails-app/views/entry 中創建 _entry.gsp,並添加清單 13 中所示的代碼(當然,可以從 list.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:
<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 所示:
<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 是一個鏈接到主頁的超鏈接。
<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 文件:
<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 所示:
#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 導航到主頁。
在登錄之前隱藏導航欄
您還需要處理一個容易弄錯的標誌,它表示這是一個 Grails 應用程序:導航欄。儘管我們在下一篇文章中才進行身份驗證,但是現在可以為未驗證的用戶關閉導航欄。這可以通過將 <div> 包含在簡單的 <g:if> 測試來實現。這個測試查找存儲在會話範圍中的 user 變數。
像清單 19 那樣修改 list.gsp 和 show.gsp:
<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 所示。這使導航欄能夠橫跨整個屏幕,從而在屏幕的底部添加一個漂亮的可視錨。
<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> 的底部,而不是旁邊:
.paginateButtons{ clear: left; } |
最後一次刷新瀏覽器。您的屏幕應該如圖 9 所示:
設置主頁
現在,一切準備就緒了,此時應該將 EntryController 設置為默認主頁。為此,需要添加一個將 /(URL http://localhost:9090/blogito/ 中的尾部反斜杠)重新定向到 EntryController 的映射。根據清單 22 編輯 grails-app/conf/UrlMappings.groovy:
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 帶來的樂趣了。 (責任編輯:A6)
[火星人 ] 精通 Grails: 改變 Grails 應用程序的外觀已經有782次圍觀