程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 精通Grails: 用定制URI和codec優化Grails中的URI

精通Grails: 用定制URI和codec優化Grails中的URI

編輯:關於JAVA

在 “改變 Grails 應用程序的外觀” 一文中,我們看到了如何使用層疊樣式表(CSS)對一個 Grails 應用程序 — Blogito blog 站點 — 進行外觀更改。這次,我將向您展示如何影響 Web 應用程 序的命脈: 用於導航的 URI。這對於像 Blogito 這樣的 weblog 極其重要。指向單個條目的那些永久鏈 接(permalink)被像名片一樣在 Internet 上傳遞;描述性越好,就越有效。

要獲得描述性更好的 URI,需要定制控制器代碼以支持個性化的 URI。還需要處理 UrlMappings.groovy 文件來創建新的路徑。最後,您將創建一個定制的 codec 來更為輕松地生成定制 URI。

了解 URI

URI 中的 U 在正式的場合下代表的是 Uniform,但是也可以表示 Unique。如果 URI http://www.ibm.com/developerworks 不能確切標識您目前所處於的 Web 站點,它就沒什麼用處了。它 還能使資源 的標識符 更容易讓人記住。通過鍵入 http://129.42.56.216 雖然可以進入該站點,但是很 少有人願意去記憶這個 Web 站點的用圓點分隔的數字形式的 IP 地址。

所以,URI 至少必須是惟一的。理想情況下,它還應該容易被人記住。Grails 絕對能夠滿足第一個要 求。它綜合使用了控制器名稱、閉包名稱以及數據庫記錄的主鍵以確保 URI 的惟一性。比如,如果想要 向用戶顯示數據庫內的第一個 Entry,就讓他們在其浏覽器內鍵入 http://localhost:9090/blogito/entry/show/1。

雖然在 URI 內包含主鍵的默認設置十分合理,但我認為它還是在兩個方面違背了美學標准。首先,實 現牽涉的內容較多。這個附帶的數據庫工件貫穿了整個 Web 站點。Google、Amazon 和 eBay 都在後台使 用了數據庫,但是很難在它們的 URI 內找到任何數據庫的跡象。其次,從 URI 刪除主鍵是出於語義的要 求。Jane Smith 的 blog 的讀者更願意用 jsmith 作為她的標識,而不是一個數字 12。同樣地,按標題 而不是主鍵列出 blog 條目更能滿足可記憶 URI 的要求。

創建 User 類

Blogito 雖然已經支持條目,但它尚不支持用戶。因此,必須先創建一個新的 User 類。

圍繞模糊 URI 的爭論

所有人都同意一個 URI 必須能夠惟一識別一種資源,但是,圍繞它是否應該為了可讀性而提供額外元 數據的爭論仍然十分激烈。有些人認為加重 URI 的負擔,使其既具有惟一性又具描述性十分危險。他們 認為描述性好的 URI 太長且太脆弱,而且還不必要地將資源標識符與底層技術連接起來。

上述這些擔心都很合理,但是我卻對 URI 不透明性的公認不敢苟同。我認為可讀的 URI 對用戶更為 友好,而且利遠遠大於弊。清晰的 URI 容易記,若遇到問題,也容易調試,而且如果它們遵循了透明約 定,還能使 Web 站點的自描述更好而且更易於被發現。

Grails 爭取透明性的第一步是在 URI 內公布對象名和控制器方法。在本文中,我將通過用更為友好 的文本標識符代替主鍵來繼續這場有關其合理性的爭論。但是為了證明我能看到問題的兩個方面的優點, 在需要簡明 URI 而不是描述性更好的 URI 時,我衷心贊同使用類似 tinyurl.com 這樣的 Web 站點。

首先,在命令行提示符鍵入 grails create-domain-class User。接下來,將清單 1 內的代碼添加到 grails-app/domain/User.groovy:

清單 1. User 類

class User {
  static constraints = {
   login(unique:true)
   password(password:true)
   name()
  }

  static hasMany = [entries:Entry]

  String login
  String password
  String name

  String toString(){
   name
  }
}

login 和 password 字段的作用不言自明;它們用來處理身份驗證。name 字段用於顯示的目的。比如 ,如果用 jsmith 登錄,將會顯示 “Jane Smith”。正如您所見,User 和 Entry 之間存在著一對多的 關系。

將 static belongsTo 字段添加到 grails-app/domain/Entry.groovy,以完成一對多的關系,如清單 2 所示:

清單 2. 向 Entry 類添加一對多的關系

class Entry {
  static belongsTo = [author:User]
  //snip
}

我們注意到,在定義關系時,可以很容易地重命名此字段。User 類具有一個名為 entries 的字段。 Entry 類現在具有一個名為 author 的字段。

通常,在此時,都會創建一個相關的 UserController 以提供一個完整的 UI 來管理 Users。我卻沒 有打算這麼做。我只是想用幾個無存根的 Users 作為占位符。在下一篇 精通 Grails 的文章中,您將更 為全面地了解用戶身份驗證和授權的相關內容。因此,我們走 “剛剛好” 的路線,通過使用 grails- app/conf/BootStrap.groovy 添加幾個新用戶,如清單 3 所示:

清單 3. 在 BootStrap.groovy 中使用無存根 Users

import  grails.util.GrailsUtil

class BootStrap {

  def init = { servletContext ->
   switch(GrailsUtil.environment){
    case "development":
     def jdoe = new User(login:"jdoe", password:"password", name:"John Doe")
     def e1 = new Entry(title:"Grails 1.1 beta is out",
       summary:"Check out the new features")
     def e2 = new Entry(title:"Just Released - Groovy 1.6 beta 2",
       summary:"It is looking good.")
     jdoe.addToEntries(e1)
     jdoe.addToEntries(e2)
     jdoe.save()

     def jsmith = new User(login:"jsmith", password:"wordpass", name:"Jane  Smith")
     def e3 = new Entry(title:"Codecs in Grails", summary:"See Mastering  Grails")
     def e4 = new Entry(title:"Testing with Groovy", summary:"See Practically  Groovy")
     jsmith.addToEntries(e3)
     jsmith.addToEntries(e4)
     jsmith.save()
    break

    case "production":
    break
   }

  }
  def destroy = {
  }
}

請注意,我是如何將條目分配給一個 User 的。您無需擔心處理主鍵或外鍵的麻煩。Grails Object Relational Mapping (GORM) API 讓您能從對象的角度而不是關系數據庫理論來進行思考。

接下來,對在 上一篇 文章中所創建的 grails-app/views/entry/_entry.gsp 局部模板稍作處理。在 Entry.lastUpdated 字段的旁邊顯示作者,如清單 4 所示:

清單 4. 向 to _entry.gsp 添加作者

<div class="entry">
  <span class="entry-date">
   <g:longDate>${entryInstance.lastUpdated}</g:longDate> :  ${entryInstance.author}
  </span>
  <h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}
   </g:link></h2>
  <p>${entryInstance.summary}</p>
</div>

${entryInstance.author} 在 User 類上調用 toString() 方法。也可以使用 ${entryInstance.author.name} 來顯示您所選擇的字段。還可以使用此語法隨心所欲地遍歷這些類的嵌 套結構。

現在,我們就可以看看所做的這些變更的實際效果了。鍵入 grails run-app 並在 Web 浏覽器內訪問 http://localhost:9090/blogito/。屏幕應該類似於圖 1:

圖 1. 顯示了新添加的作者的條目

現在 Blogito 可以支持多個用戶,下一步是讓讀者能按作者來查看這些條目。

按作者顯示條目

我們的最終目的就是支持像 http://localhost:9090/blogito/entry/list/jdoe 這樣的 URI。注意到 ,User.login 出現在此 URI 內,而不是主鍵。在這個過程中,還需要對分頁(pagination)做稍許調整 。

EntryController.list 的搭建(scaffolded)行為不允許按 User 過濾。清單 5 顯示了 list 閉包 的默認實現:

清單 5. 默認的 list 實現

def list = {
   if(!params.max) params.max = 10
   [ entryInstanceList: Entry.list( params ) ]
}

若要支持在路徑的末尾允許出現一個可選的用戶名,還需要對之進行擴展。編輯 grails- app/controllers/EntryController.groovy 並添加一個新的 list 閉包,如清單 6 所示:

清單 6. 按作者限制此列表

class EntryController {
  def scaffold = Entry 

  def list = {
    if(!params.max) params.max = 10
    flash.id = params.id
    if(!params.id) params.id = "No User Supplied"

    def entryList
    def entryCount
    def author = User.findByLogin(params.id)
    if(author){
     def query = { eq('author', author) }
     entryList = Entry.createCriteria().list(params, query)
     entryCount = Entry.createCriteria().count(query)
    }else{
     entryList = Entry.list( params )
     entryCount = Entry.count()
    }

    [ entryInstanceList:entryList, entryCount:entryCount ]
  }
}

您應該注意到的第一件事情是,若終端用戶沒有提供 params.max 和 params.id 二者的值,就用默認 值填充。現在,先不要擔心 flash.id — 我稍後在探討有關分頁問題的時候還會對之進行詳細討論。

params.id 值通常是一個整型 — 確切的說是主鍵。我們一般習慣於 /entry/show/1 和 entry/edit/2 這樣的 URI。我本可以在 grails-app/conf/UrlMappings.groovy 內設置一個映射以便返 回一個描述性更好的名稱,比如 params.name 或 params.login,但現有的映射已經獲取了操作名稱後的 路徑元素並將其存儲在 params.id 內。我只是充分利用了現有的行為。在 URLMapper.groovy 內,如清 單 7 所示,可以看到返回 params.id 的默認映射:

清單 7. UrlMappings.groovy 內的默認映射

class UrlMappings {
   static mappings = {
    "/$controller/$action?/$id?"{
     constraints {}
   }
   //snip
  }
}

由於這不是 User 的主鍵,所以不能像往常那樣使用 User.get(params.id)。相反,必須使用 User.findByLogin(params.id)。

如果找到了一個匹配的 User,就需要創建一個查詢塊。這就需要用到 Hibernate Criteria Builder 。在本例中,我們限制了列表只包含匹配某特定作者的那些條目。同樣地,我們注意到 GORM 也允許您從 對象而不是主鍵或外鍵的角度來思考。

如果沒有匹配 params.id 的作者,就會返回全部條目的完整列表: entryList = Entry.list( params )。

注意,entryCount 值是被顯式計算出來的。Scaffolded GroovyServer Pages (GSP) 代碼通常會在 <g:paginate> 標記內調用 Entry.count()。由於會傳遞回一個過濾了的列表,所以需要在此控制 器的一個變量內處理這一點。

在 flash.id 內存儲 params.id 值將允許應用程序將此查詢條件傳遞回 <g:paginate> 標記。 調整 grails-app/views/entry/list.gsp 內的 <g:paginate> 以便利用新的 entryCount 變量以 及存儲在 flash 范圍內的參數,如清單 8 所示:

清單 8. 針對定制分頁調整 list.gsp 頁面

<div class="paginateButtons">
  <g:paginate total="${entryCount}" params="${flash}"/>
</div>

重啟 Grails 並在 Web 浏覽器內訪問 http://localhost:9090/blogito/entry/list/jsmith。屏幕應 該類似圖 2:

圖 2. 按作者列出條目

為了確保分頁仍能工作,鍵入 http://localhost:9090/blogito/entry/list/jsmith?max=1。單擊 Previous 和 Next 按鈕以確保只有 Jane 的 blog 條目才會出現,如圖 3 所示:

圖 3. 測試定制分頁

按作者過濾的功能就緒後,就可以更進一步,創建一個更為友好的定制 URI。

創建一個定制 URI

UrlMappings.groovy 文件為創建新的 URI 提供了額外的靈活性。雖然 http://localhost:9090/blogito/entry/list/jsmith 已經可以發揮作用,但是假設,最新出現的用戶請 求要求支持 http://localhost:9090/blogito/blog/jsmith 這樣的 URI,又該如何呢?沒問題!如清單 9 所示那樣向 UrlMappings.groovy 添加一個新的映射:

清單 9. 向 UrlMappings.groovy 添加一個新的定制映射

class UrlMappings {
   static mappings = {
    "/$controller/$action?/$id?"{
     constraints {
   // apply constraints here
   }
   }
   "/"(controller:"entry")
   "/blog/$id"(controller:"entry", action="list")
   "500"(view:'/error')
  }
}

現在,以 /blog 開頭的那些 URI 都將會被重新定向到條目控制器和列表動作。雖然 $user 或 $login 的描述性可能更好,但是讓 $id 與 Grails 約定保持一致就意味著 "/$controller/$action?/ $id?" 和 "/blog/$id"(controller:"entry", action="list") 二者能夠指向同一個端點。

在 Web 浏覽器內鍵入 http://localhost:9090/blogito/blog/jsmith 以驗證此映射能夠工作。

處理好 Users 之後,就可以集中精力為 Entries 創建更友好的 URI。

創建一個定制 codec

在使用 User.login 而非 User.id 時,URI 很簡單,因為它不包含空白。不錯,目前尚沒有任何的驗 證規則強制這種 “無空白” 的要求,但我們可以很輕松地添加一個這樣的規則來強制 URI 遵從這一要 求。

但是,若在 URI 內用 Entry.title 代替 Entry.id 又如何呢?標題幾乎都要包含空白。一種解決方 法是向 Entry 類內添加另一個字段並讓終端用戶重新輸入沒有空白的標題。這種做法不是很理想,因為 它要求用戶做更多的工作,而且還要求必須要編寫另一個驗證規則來確保用戶能正確輸入。更好的方法是 讓 Grails 根據使用 Entry.title 的位置自動將空白轉變為下劃線。要實現此目的,需要創建一個定制 codec(即 編碼-解碼器 的簡寫)。

創建 grails-app/utils/UnderscoreCodec 並添加清單 10 所示代碼:

清單 10. 一個定制 codec

class UnderscoreCodec {
  static encode = {target->
   target.replaceAll(" ", "_")
  }

  static decode = {target->
   target.replaceAll("_", " ")
  }
}

Grails 提供了幾個開箱即用的內置 codec:HtmlCodec、UrlCodec、Base64Codec 和 JavaScriptCodec。HtmlCodec 是所生成的 GSP 文件內的 encodeAsHtml() 和 decodeHtml() 方法的源代 碼。

您也可以向其中添加您自己的 codec。Grails 使用 grails-app/utils 目錄內任何一個具有 Codec 後綴的類來將 encodeAs() 和 decode() 方法添加到 String。在本例中,Blogito 內的所有 String 都 魔法般地具有了兩個新方法:encodeAsUnderscore() 和 decodeUnderscore()。

通過在 test/integration 內創建 UnderscoreCodecTests.groovy 可以驗證這一點,如清單 11 所示 :

清單 11. 測試一個定制 codec

class UnderscoreCodecTests extends GroovyTestCase  {
  void testEncode() {
   String test = "this is a test"
   assertEquals "this_is_a_test", test.encodeAsUnderscore()
  }

  void testDecode() {
   String test = "this_is_a_test"
   assertEquals "this is a test", test.decodeUnderscore()
  }
}

在命令行提示符鍵入 grails test-app 運行測試。所看到的結果應該類似清單 12:

清單 12. 測試成功運行後的輸出

$ grails test-app
-------------------------------------------------------
Running 2 Integration Tests...
Running test UnderscoreCodecTests...
           testEncode...SUCCESS
           testDecode...SUCCESS
Integration Tests Completed in 157ms
-------------------------------------------------------

運行中的 Codec

UnderscoreCodec 也就緒後,您就可以支持在 URI 中包括用戶和條目標題 — 比如, http://localhost:9090/blogito/blog/jsmith/this_is_my_latest_entry。

首先,調整 UrlMappings.groovy 內的 /blog 映射以支持一個可選的 $title,如清單 13 所示。還 記得麼,在 Groovy 內,尾部加個問號代表這是可選的。

清單 13. 在 URI 映射內允許可選標題

class UrlMappings {
   static mappings = {
    "/$controller/$action?/$id?"{
     constraints {
   // apply constraints here
   }
   }
   "/"(controller:"entry")
   "/blog/$id/$title?"(controller:"entry", action="list")
   "/entry/$action?/$id?/$title?"(controller:"entry")
   "500"(view:'/error')
  }
}

接下來,調整 EntryController.list 來說明新的 params.title 值,如清單 14 所示:

清單 14. 處理控制器內的 params.title

class EntryController {
  def scaffold = Entry 

  def list = {
    if(!params.max) params.max = 10
    flash.id = params.id
    if(!params.id) params.id = "No User Supplied"
    flash.title = params.title
    if(!params.title) params.title = ""

    def author = User.findByLogin(params.id)
    def entryList
    def entryCount
    if(author){
     def query = {
      and{
       eq('author', author)
       like("title", params.title.decodeUnderscore() + '%')
      }
     }
     entryList = Entry.createCriteria().list(params, query)
     entryCount = Entry.createCriteria().count(query)
    }else{
     entryList = Entry.list( params )
     entryCount = Entry.count()
    }

    [ entryInstanceList:entryList, entryCount:entryCount ]
  }
}

我已經在此查詢內使用了 like 以讓此 URI 更為靈活。例如,用戶可以鍵入 /blog/jsmith/mastering_grails 來返回所有以 mastering_grails 開頭的標題。如果您願意更為嚴格一 些,可以使用此查詢內的 eq 方法來要求一個確切的匹配。

在 Web 浏覽器內鍵入 http://localhost:9090/blogito/jsmith/Codecs_in_Grails 來觀察運行中的 這個新的 codec。您的屏幕應該類似圖 4:

圖 4. 按用戶名和標題查看一個 blog 條目

結束語

URI 是一個 Web 應用程序的命脈。Grails 的默認設置是一個很好的開端,但是您也應習慣於定制這 些 URI 以最好地滿足您的 Web 站點的要求。得益於您的艱苦工作,Blogito 現在具有了 Users 和 Entries。但更為重要的是,您對 URI 使用其他內容而不是主鍵來查看它們。您了解了如何通過調整控制 器代碼創建更為友好的 URI、向 UrlMappings.groovy 添加映射以及創建一個定制 codec。

下一次,您將創建一個登錄表單以便能對 Blogito Users 進行身份驗證。一旦用戶登錄,他們就能上 傳一個文件用作 blog 條目的主體 — HTML、一個圖像或是一個 MP3 文件。到那時,就可以享受精通 Grails 帶來的樂趣了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved