精通 Grails: 改變 Grails 應用程序的外觀

火星人 @ 2014-03-12 , reply:0
←手機掃碼閱讀

  
在這一期的 精通 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 是一個現代的 Web 開發框架,它將熟悉的 Java 技術(比如 Spring 和 Hibernate)和最新的實踐(比如約定優於配置)結合起來。Grails 是用 Groovy 編寫的,它使您能夠與遺留的 Java 代碼無縫集成,同時又增加了腳本語言的靈活性和動態性。學習了 Grails 之後,您將對 Web 開發有新的看法。

在開始之前,必須安裝 Grails 1.1。撰寫本文時,它還是 beta 版。

安裝 Grails 1.1

Grails 在 Java 1.5 或 1.6 上運行表現最佳。通過命令提示符輸入 java -version,確保 Java 版本是比較新的。

Java 1.5 或 1.6 就緒之後,安裝 Grails 的步驟就很簡單了:

  1. 從 Grails 站點 下載 grails.zip 文件。
  2. 解壓縮 grails.zip。
  3. 創建一個 GRAILS_HOME 環境變數。
  4. 將 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 欄位提供文本區域(而不是簡單的文本欄位)。

下一篇文章:為 blog 條目添加內容

在下一篇文章中,將添加一個能夠保存 blog 條目的實際內容的 body 欄位。在本文中,我忽略了 body 欄位,因為要完整地實現它,必須先理解 Grails 如何處理用戶身份驗證和文件上傳。Blogito 允許終端用戶上傳各種類型的數據 — HTML、圖像,甚至 MP3。

記住,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 頭部和段落標記中。

CSS 101:<div> 和 <span>

當談論 CSS 和其他與圖像設計(而不是軟體工程)關係更緊密的技術時,一些程序員就顯得厭煩。但是我承認,當 CSS 變得複雜時,它可以是非常 複雜的。但反之亦然:當 CSS 比較簡單時,它可以是非常簡單的。

HTML 標記可以分為兩大類:塊和內聯。塊標記(比如 <h1>、<p> 和 <div>)通常用於包含大的、雜亂的內容塊。瀏覽器通常在每個塊元素的末尾拋出一個隱式的新行。<h1> 和 <p> 有預定義的外觀。我們通常將信息塊包含在 <div> 標記中,以便為其命名和自定義樣式。

內聯元素(比如 <a>、<strong> 和 <span>)通常用來包含一個或兩個單詞,而不是整個段落。內聯元素的末尾沒有添加隱式的新行。像塊元素一樣,像 <strong> 和 <em> 這樣的內聯元素有與之關聯的默認格式,而 <span> 則必須通過 CSS 來應用格式。

對不熟悉的人而言,命名 <span> 和 <div> 元素的方式可能會造成混亂。需要在頁面的多個元素上重用 class 屬性。需要創建 CSS 類,比如 entry 和 entry-date,以讓具有相同類的所有元素同時出現。它們在樣式表中顯示時帶有前導點:例如,.entry 和 .entry-date。

您還可能碰到帶有 id 屬性的元素。id 在 HTML 文檔中必須是惟一的。在本文的後面,將創建一個 <div id="header">。這意味著每頁只能有一個 header 元素。id 在樣式表中顯示時帶有一個前導 hash,比如在 #header 中。

要快速回顧 CSS 基礎知識,請查看 參考資料。

在瀏覽器中刷新列表視圖(見圖 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

CSS 102:em 和 px

在瀏覽 main.css 時,就會看到許多字體的大小是用像素來表示的。從技術上講,使用固定的大小(這很常見)是沒有錯誤的,但這會影響使用。對於有視覺缺陷的用戶和具有超大監視器(或超低解析度)的機器而言,固定大小的字體都是不理想的。在 web 管理員的顯示器上看起來很好的東西在其他地方顯示時往往收不到這麼好的效果 — 包括從電影屏幕到 iPhone 手機等眾多顯示屏上。

CSS 中的像素度量並不都是不好的 — 它們非常適合具有固定大小的元素,比如圖像。但是總體而言,相對度量單元(比如 em)更適用於字體大小。1em 等效於由瀏覽器或周圍父元素設置的默認字體大小的 100%。2em 是字體大小的 2 倍,依此類推。

參見 參考資料 獲得更多信息。

現在,需要使 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>  

CSS 103:padding 和 margin

用於給塊元素留出一些空間的 CSS 方框模型(box model)乍看起來有些迷惑。簡單而言,padding 增加了塊內的空間,而 margin 增加了塊外的空間。

清單 18 的頭部使用 padding 來增加文本和藍色方框邊緣之間的空間。它使用 margin 來增加藍色方框外部的 header <div> 和 nav <div> 之間的空間。

可以將一個方框的四面設置為一致的 padding: 2em; 或 margin: 2em;。要設置方框的某個邊的空間,可以使用 margin-top、margin-right、margin-bottom 或 margin-left 直接引用它。如果想要通過一行代碼為某條邊(如清單 18 所示)設置不同的 padding,TRBL(Top、Right、Bottom 和 Left 的縮寫)將幫助您記住正確的順序。這樣,記憶四條邊的順序就很容易了。

參見 參考資料 獲得更多關於 CSS 方框模型的信息。

最後,再為 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 帶來的樂趣了。 (責任編輯:A6)






[火星人 via ] 精通 Grails: 改變 Grails 應用程序的外觀已經有366次圍觀

http://www.coctec.com/docs/linux/show-post-68969.html