程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 精通Grails: 使用Ajax實現多對多關系

精通Grails: 使用Ajax實現多對多關系

編輯:關於JAVA

在 Web 應用程序中,多對多(m:m)關系很難處理。在 精通 Grails 系列的這一期文章中,Scott Davis 將向您展示如何在 Grails 中成功實現 m:m 關系。了解如何通過 Grails 對象關系映射(Grails Object Relational Mapping,GORM)API 和後端數據庫處理多對多關系。學習如何使用 Ajax (Asynchronous JavaScript + XML)流線化用戶界面。

軟件開發就是使用代碼來模擬現實世界。例如,書籍都有作者和出版商。在 Grails 應用程序中,要 為每個元素創建一個域類。GORM 為每個類創建對應的數據庫表,搭建功能(scaffolding)提供基本的 Create/Retrieve/Update/Delete (CRUD) Web 界面。

接下來定義這些類之間的關系。一個出版商通常會出版多部圖書,因此出版商和他的圖書之間的關系 就是一個簡單的一對多(1:m)關系:一個 Publisher 出版多個 Book。通過在 Publisher 類中加入 static hasMany = [books:Book],創建 1:m 關系。在 Book 類中放入 static belongsTo = Publisher ,可以向關系添加另一個方面 —— 級聯更新和刪除。如果刪除一個 Publisher,所有對應的 Book 也會 被刪除。

很容易在底層數據庫中模擬 1:m 關系型。每個表有一個用作主鍵的 id 字段。當 GORM 向 book 表添 加一個 publisher_id 字段時,就在兩個表之間建立了一個 1:m 關系。在前端,Grails 也能夠很好地處 理 1:m 關系。創建一個新 Book 時,自動生成的(搭建而成)HTML 表單提供一個下拉組合框,將您的選 擇限制在現有的 Publisher 列表中。自本系列的 第一篇文章 以來,我們已經展示了許多 1:m 關系的示 例。

現在來看看一個稍微復雜一些的關系 — 多對多(m:m)關系。與 Book 和 Publisher 之間的關系相 比,模擬 Book 與 Author 之間的關系要復雜得多。一部圖書可以有多個作者,一個作者也可以編寫多部 圖書。這是一個典型的 m:m 關系。在真實世界中,m:m 關系很常見。一個人可以有多個支票帳號,一個 支票帳號也可以由多個人來管理。一個顧問可以為多個項目工作,一個項目也可以有多個顧問。本文將向 您展示如何使用 Grails 實現 m:m 關系,我們在本系列中開發的 trip-planner 應用程序上進行構建。 在討論 trip-planner 應用程序之前,我還想談談圖書的例子,以幫助您理解一個要點。

第三個類

在數據庫中,用三個表表示 m:m 關系:前兩個表您已經知道了(Book 和 Author),第三個是一個連 接表(BookAuthor)。GORM 向 BookAuthor 連接表添加 book_id 和 author_id,而不是向 Book 或 Author 表添加一個外鍵。連接表允許將具有一個作者的圖書和具有多個作者的圖書持久化。它還能夠表 示編寫多部圖書的作者。Author 和 Book 外鍵的每種惟一組合都會在連接表中有惟一的記錄。這種方法 具有極大的靈活性:一本書可以有任意數量的作者,而一個作者可以編寫任意數目的圖書。

Dierk Koenig 曾經告訴我,“如果您認為只有兩個對象共享一個簡單的多對多關系,那麼您對多對多 關系的了解還不夠透徹。還存在第三個對象,這個對象有其自己的屬性和生命周期”。確實如此,Book 和 Author 之間的關系已經超出了簡單連接表的范圍。例如,Dierk 是 Groovy in Action(Manning Publications,2007 年 1 月)的第一作者。第一作者的身份應該表示為 Author 與 Book 之間的關系中 的一個字段。其他各種因素也應如此:作者會按特定順序在封面上列出;每個作者編寫圖書的特定章節; 而且每個作者的報酬也因貢獻不同而異。您可以看到,Author 與 Book 之間的關系比最初計劃的更加微 妙。在真實世界中,每個作者都會簽訂一份合同,使用明確的條款詳細描述他與這部圖書之間的關系。或 許應該創建一個一級類 Contract 來更好地表示 Book 與 Author 之間的關系。

簡單來講,這意味著一個 m:m 關系實際上就是兩個 1:m 關系。如果兩個類有可能共享一個 m:m 關系 ,那麼您應該更深入地研究一下,確定出具有兩個 1:m 關系的第三個類並顯式地定義它。

模擬航空公司和機場

現在回到 trip-planner 應用程序,首先回顧一下域模型,並查看有沒有潛在的 m:m 關系。在 第一 篇文章 中,我創建了一個 Trip 類,如清單 1 所示:

清單 1. Trip 類

class Trip {
  String name
  String city
  Date startDate
  Date endDate
}

在 第二篇文章 中,我添加了一個 Airline 類(如清單 2 所示),以演示一個簡單的 1:m 關系:

清單 2. Airline 類

class Airline {
  static hasMany = [trip:Trip]
  String name
  String frequentFlier
}

這些類在當時具有其特定的用途 — 用於說明某一點的簡單占位符 — 但是它們並不是一個嚴格的域 模型。現在對最初的類進行改進,並添加一些更加健壯的內容。

我使用以前的方法創建了 Trip 類,因為當時看起來還不錯。我當時說 “我正計劃到芝加哥旅行” 或 “我計劃在下個月 15 號到 20 號去紐約”。city、startDate 和 endDate 這樣的字段似乎是 Trip 的自然屬性。但是,現在看來,Trip 可能還會涉及到更多因素。

我住在科羅拉多州丹佛市 — 美國聯合航空公司的中心城市。這意味著我通常可以直接飛到最終的目 的地,但有時候需要中轉多次。有時候一次旅行涉及到多個城市:“我正飛往波士頓,星期一到星期五我 要在那裡教課。當我在東海岸時,我需要在華盛頓附近參加星期六的一場會議。我將在星期天下午飛回來 ”。即使我幸運地找到了到一個特定城市的直達航班,而且我不會飛到其他城市,我的旅行涉及到的航班 仍然不止一次 — 飛往目的地的航班和返回的航班。一個 Trip 可以包含多個 Flight。清單 3 定義 Trip 與 Flight 之間的關系:

清單 3. Trip 與 Flight 之間的 1:m 關系

class Trip{
  static hasMany = [flights:Flight]
  String name
}
class Flight{
  static belongsTo = Trip
  String flightNumber
  Date departureDate
  Date arrivalDate
}

請記住,使用 belongsTo 字段設置關系意味著,如果刪除 Trip,也會刪除所有相關的 Flight。如果 我為空中交通管制員構建一個系統,我可能希望制定不同的架構決策。或者如果我嘗試為同乘一個航班的 多位乘客構建一個系統(一個 Flight 可以有多個 Passengers,一個 Passenger 也可以有多個 Flights ),那麼將一個航班綁定到一個特定的乘客可能是一個問題。但是我不會嘗試為數百萬乘客模擬全世界每 天運行的數千次航班。在我的簡單例子中,一個 Flight 的所有任務就是進一步描述一個 Trip。如果對 於我來說,某個 Trip 不再重要,那麼每個對應的 Flight 也是如此。

現在,我應該使用 Airline 類做什麼呢?一個 Trip 可能涉及到多個不同的 Airline,而一個 Airline 又可以用於多個不同的 Trip。這兩個類之間是一種明確的 m:m 關系,但是 Flight 似乎是添加 Airline 的恰當位置,如清單 4 所示。一個 Airline 可以有多個 Flight,而一個 Flight 只能有一個 Airline。

清單 4. 將 Airline 與 Flight 關聯

class Airline{
  static hasMany = [flights:Flight]
  String name
  String iata
  String frequentFlier
}
class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Date arrivalDate
}

您應該注意到兩點。首先,Flight 中的 belongsTo 字段由一個值轉變為多個值的散列映射(hashmap )。一個 Trip 可以有多個 Flight,一個 Airline 也可以有多個 Flight。

其次,我向 Airline 添加了一個新的 iata 字段。這個字段用於填寫 International Air Transport Association (IATA) 編碼。IATA 為每個航空公司分配了一個惟一的編碼 — UAL 表示 United Airlines 、COA 表示 Continental、DAL 表示 Delta,等等(參見 參考資料,獲取 IATA 編碼的完整列表)。

最後,您應該注意到我制定了另一個架構決策,這次加入了 Airline 和常飛顧客(frequent-flier) 編號之間的關系。由於我假設只有一個用戶使用這個系統,因此可以將 FrequentFlier 作為 Airline 類 的一個屬性。每個航空公司最多只有一個 frequent-flier 編號,所以這是最簡單的可能的解決方案。如 果這次旅行計劃的需求發生了更改,而且我需要支持多個用戶,那麼就出現了另一個 m:m 關系。一個乘 客可以有多個 frequent-flier 編號,一個航空公司也可以有多個 frequent-flier 編號。創建一個連接 表來管理這個關系會非常適合。現在我將使用簡單的解決方案,但是如果需求發生了更改,我會將 FrequentFlier 字段標記為未來的一個重構點。

城市還是機場?

現在將 City 添加到代碼中 — 或許不用添加。盡管您可能會說,“飛往芝加哥” 理論上講是飛往一 個機場。我是飛往芝加哥的 O'Hare 機場還是 Midway 機場?當我飛往紐約時,是飛往 LaGuardia 還是 JFK?顯然我需要一個 Airport 類來替代簡單的 City 字段。清單 5 展示了 Airport 類:

清單 5. Airport 類

class Airport{
  static hasMany = [flights:Flight]
  String name
  String iata
  String city
  String state
  String country
}

在清單 5 中可以看到,iata 字段又回來了。這次 DEN 表示 Denver International Airport,ORD 表示 Chicago O'Hare,MDW 表示 Chicago Midway,等等。您也許想創建一個 State 類並設置一個簡單 的 1:m 關系,或者甚至創建一個 Location 類來封裝 city、state 和 country。我將把這個困難的任務 留給您自己來完成。

現在我將 Airport 添加到 Flight 類,如清單 6 所示:

清單 6. 將 Airport 關聯到 Flight

class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Airport departureAirport
  Date arrivalDate
  Airport arrivalAirport
}

但是,這一次我顯式地創建 departureAirport 和 arrivalAirport 字段,而不是隱式地使用 belongsTo 字段。用戶界面看起來沒有任何不同 — 這些字段都將使用組合框來顯示 — 但是類之間的關 系稍微有些不同。刪除一個 Airport 不會連帶刪除相關聯的 Flight,而刪除一個 Trip 或 Airline 則 會刪除相關聯的 Flight。我在此處提供了兩種方法,以說明將各種類關聯起來的不同方式。實際上,您 可以自己決定是否希望類保持嚴格的引用完整性(換句話說,所有刪除操作都是級聯的)或者允許更松散 的關系。

多對多關系的實際效用

現在,就緒的對象模型可以很好地模擬真實世界。我一年中要經歷許多旅行,經歷許多不同的航空公 司,飛往許多不同的機場。將所有這些關系聯系起來的就是一個 Flight。

查看一下底層數據庫,我只看到了期望看到的表,如清單 7 中的 MySQL show tables 命令的輸出所 示:

清單 7. 底層數據庫表

mysql> show tables;
+----------------+
| Tables_in_trip |
+----------------+
| airline    |
| airport    |
| flight     |
| trip      |
+----------------+

airline、airport 和 trip 表中的所有列均與對應的域類中的字段匹配。flight 是連接表,表示其 他表之間的復雜關系。清單 8 展示了 Flight 表中的字段:

清單 8. Flight 表中的字段

mysql> desc flight;
+----------------------+--------------+------+-----+
| Field        | Type     | Null | Key |
+----------------------+--------------+------+-----+
| id          | bigint(20)  | NO  | PRI |
| version       | bigint(20)  | NO  |   |
| airline_id      | bigint(20)  | YES | MUL |
| arrival_airport_id  | bigint(20)  | NO  | MUL |
| arrival_date     | datetime   | NO  |   |
| departure_airport_id | bigint(20)  | NO  | MUL |
| departure_date    | datetime   | NO  |   |
| flight_number    | varchar(255) | NO  |   |
| trip_id       | bigint(20)  | YES | MUL |
+----------------------+--------------+------+-----+

用於創建新 Flight 的搭建的 HTML 頁面為所有的相關表提供了組合框,如圖 1 所示:

圖 1. 用於添加航班而搭建的 HTML 頁面

調優用戶界面

迄今為止,m:m 討論的焦點一直圍繞如何使用類和數據庫表模擬關系。我希望您能夠看到,科學就是 一門藝術。作為 Grails 開發人員,您可以利用許多出色的技巧來改進關系的行為和副作用。現在將焦點 轉移到用戶界面上,您將會看到一些非常優秀的方法,使用它們調整 m:m 關系的顯示。

正如我在前一節中演示的,默認情況下,Grails 使用選擇字段來顯示 1:m 關系。這個起點還不錯, 但是您可能想在不同的環境中使用其他 HTML 控件。選擇字段只顯示當前值;您必須下拉該列表才能查看 所有可能的值。盡管這在可用屏幕空間非常有限的情況下是最佳選擇,但您可能會覺得使所有的選項都顯 示出來是一種更好的解決方案。單選按鈕適合於顯示所有可能的選擇並將選擇限制為單個值。復選框顯示 所有可能的選擇並允許選擇多個選項。

所有這些控件都適合顯示數量有限的選擇,但它們不能擴展到數百或數千個可能值。例如,如果我需 要向最終用戶提供全世界所有的航空公司(大約 650 家),沒有一種標准 HTML 控件能夠處理這麼大的 數據量。這時就需要開發人員做出判斷了。對於這個應用程序,我不需要顯示所有 650 家航空公司。我 一生中飛過的不同航空公司可能還不到 12 家。在一些情況下,使用選擇字段顯示航空公司選項很可能就 足夠了。

要了解 Grails 如何為 Airline 創建選擇字段,請輸入 grails generate-views Flight。查看一下 grails-app/views/flight/create.gsp。選擇字段是使用 <g:select> 標記在一行代碼中生成的。 如果不熟悉 Grails TagLibs,請參閱 上個月的文章。清單 9 展示了使用中的 <g:select> 字段 :

清單 9. 使用中的 <g:select> 字段

<g:select optionKey="id"
      from="${Airline.list()}"
      name="airline.id"
      value="${flight?.airline?.id}" ></g:select>

在 Web 浏覽器中選擇 View > Source 查看這是如何呈現的,如清單 10 所示:

清單 10. 呈現的選擇字段

<select name="airline.id" id="airline.id" >
<option value="1" >UAL - United Airlines</option>
<option value="2" >DAL - Delta</option>
<option value="3" >COA - Continental</option>
</select>

<g:select> 標記的 optionKey 屬性指定一個 類的哪個字段將會存儲在關系的另一端的多個 字段的值中。Airline 表的主鍵(airline.id)會作為 Flight 表的外鍵。在選擇字段中,請注意 airline.id 是可選值(調用 Airline.toString() 方法來顯示值)。如果您想要更改選項的排列順序, 可以將 GORM 調用由 Airline.list() 更改為 Airline.listOrderByIata()、 Airline.listOrderByName() 或想要使用的任何其他字段。

使用 Ajax 處理大量選項

默認的選擇控件是顯示實際航空公司數量的不錯選擇。不幸的是,對於機場情況則有所不同。我在一 年中可能會到達 40 或 50 個不同的機場。依我的經驗,在一個字段中提供超過 15 或 20 個選擇就有點 令人討厭了。

幸運的是,機場的 IATA 編碼在行業中得到了廣泛應用。我研究航班的時候會見到它們。在預訂航班 時也會 在收據上顯示。甚至在機票上也能見到。與要求用戶滾動查找數百個可能的機場相比,要求他們 輸入 IATA 編碼一個不錯的替代方法。

回顧一下我在本文開始部分介紹的 Book 示例。Amazon.com 在其主頁上提供了顯示所有庫存圖書的選 擇字段了嗎?沒有 — 它提供了一個文本字段,您可以在其中輸入圖書的標題、作者,如果您喜歡,甚至 還可以輸入國際標准圖書編號(International Standard Book Number,ISBN)。我將在此處使用相同的 技巧來處理 trip-planner 應用程序中的機場。

將控件由一個選擇字段更改為一個文本字段非常簡單。但是,在我繼續使用這種解決方案的方法之前 ,我想要花一些時間處理它的語義。iata 字段是一個沒有形式限制的文本字段,但是我不能對用戶輸入 的任何值都接受(如果您寫錯了您的名字,應用程序不會責備您;但是如果您輸錯了 IATA 編碼,它就需 要給出警告)。我希望這種反饋回立即發生,因為沒有什麼事情比每次輸入無效值之後重復提交整個 HTML 表單更令人沮喪。

所以,我不希望只是為了驗證一個單個字段要在服務器間往返通信整個表單,或者每次都需要將數千 個機場的 IATA 編碼下載到客戶端。該解決方案將數據保存在服務器上,並針對每個字段執行一個細粒度 的 HTTP 請求,而不是對整個表單執行一個粗粒度的請求。這種技術稱為執行 Ajax (Asynchronous JavaScript + XML) 請求(參見 參考資料,獲取 Ajax 的介紹)。

要使我的 Grails 應用程序支持 Ajax,我需要對 AirportController 進行調整,以接受 Ajax 請求 ,還要對視圖進行調整,以執行 Ajax 請求。我將從 AirportController 入手。

AirportController 已經擁有搭建好的閉包,用於返回一個 Airport 列表,並顯示一個單獨的 Airport。但是,這些現有的閉包返回的值是 HTML 格式的。我將添加一個返回原始數據的新閉包。一個 選擇是完全將 POGO 序列化,但是我的客戶機是一個 Web 浏覽器。不幸的是,JavaScript — 不是 Groovy — 才是 Web 浏覽器支持的語言(Mozilla Foundation,您注意到了嗎?)

Ajax 中的 x 提醒了我,我可以返回 XML。如果將 grails.converters 包導入 AirportController 中,返回 XML 只需要一行代碼,如清單 11 所示:

清單 11. 從 controller 返回 XML

import grails.converters.*
class AirportController {
  def scaffold = Airport

  def getXml = {
   render Airport.findByIata(params.iata) as XML
  } 
}

這種解決方案的惟一問題是,JavaScript 對 XML 的原生支持要遜於 Groovy。對象關系映射器(比如 GORM)的好處在於,它可以將數據從非原生格式(存儲在關系數據庫中)無縫地轉換為 Groovy。這個練 習的 JavaScript 版本是將 Groovy 數據轉換為 JavaScript Object Notation (JSON)(參見 參考資料 )。幸運的是,與轉換為 XML 代碼一樣,可以使用一行代碼轉換為 JSON。在清單 12 中,我向 getJson 閉包添加了一些錯誤處理,但在其他方面與 getXml 閉包等效:

清單 12. 從 controller 返回 JSON

def getJson = {
  def airport = Airport.findByIata(params.iata)

  if(!airport){
   airport = new Airport(iata:params.iata, name:"Not found")
  }

  render airport as JSON
}

要驗證 JSON 轉換是否生效,可以在 Web 浏覽器中輸入 http://localhost:9090/trip/airport/getJson?iata=den。應該會得到清單 13 中顯示的響應(您也許 需要在浏覽器中選擇 View > Source 查看 JSON 響應)。

清單 13. JSON 響應

{"id":1,"class":"Airport","city":
  "Denver","country":"US","iata":
"DEN","name":"Denver International Airport","state":"CO"}

返回一組航空公司的過程非常簡單:render Airline.list() as JSON。

現在生成了 JSON,是時候使用它了。我將把 departureAirport 的現有 <g:select> 注釋掉, 並替換為清單 14 中的 4 行代碼:

清單 14. 使用一個文本字段替換選擇字段

<div id="departureAirportText">[Type an Airport IATA Code]</div>
<input type="hidden" name="departureAirport.id" value="-1"
  id="departureAirport.id"/>
<input type="text" name="departureAirportIata" id="departureAirportIata"/>
<input type="button" value="Find" onClick="get('departureAirport')"/>

第一行是一個只讀顯示區域。注意,它具有一個 id。ID 在整個 HTML Document Object Model (DOM) 中必須是惟一的。稍後我將使用句柄 departureAirportText 寫出 JSON 調用的結果。

提交表單時,<div> 不會被發送回服務器;但表單控件(比如輸入和選擇控件)會被發送回服 務器。當整個表單提交到服務器時,隱藏的文本字段提供了一個存儲 Airport 的 id 的位置。

用戶將在名為 departureAirportIata 的文本字段中輸入 IATA 編碼。為名稱和 ID 都提供相同的值 可能不是很好,但 HTML 機制需要這樣做。提交表單時,名稱將會傳回到服務器。ID 是我調用 getJson 閉包的條件。

最後,最後一行代碼是一個按鈕,單擊它時,會調用一個名為 get 的 JavaScript 函數。稍後我將會 展示 get 函數的實現。圖 2 展示了新表單的外觀:

圖 2. 改進的表單

將 Prototype 用於 Ajax 調用

Grails 附帶了一個名為 Prototype 的 JavaScript 庫(參見 參考資料)。Prototype 提供了一種執 行 Ajax 調用的通用方法,這種方法兼容所有主流浏覽器。get 函數構建您在之前輸入浏覽器的 URL,然 後對服務器執行異步調用。如果調用成功(返回 HTTP 200),則會調用 update 函數。清單 15 使用 Prototype 進行 Ajax 調用:

清單 15. 將 Prototype 用於 Ajax 調用

<g:javascript library="prototype" />
<script type="text/javascript">
  function get(airportField){
   var baseUrl = "${createLink(controller:'airport', action:'getJson')}"
   var url = baseUrl + "?iata=" + $F(airportField + "Iata")
   new Ajax.Request(url, {
    method: 'get',
    asynchronous: true,
    onSuccess: function(req) {update(req.responseText, airportField)}
   })
  }

  ...
</script>

update 函數讀取 JSON 調用的結果,更新 <div> 的顯示,並將隱藏字段的值更改為機場的主 鍵(如果找到的話),或者更改為 -1(如果未找到主鍵)。清單 16 展示了 update 函數:

清單 16. 使用 JSON 數據更新字段

function update(json, airportField){
  var airport = eval( "(" + json + ")" )
  var output = $(airportField + "Text")
  output.innerHTML = airport.iata + " - " + airport.name
  var hiddenField = $(airportField + ".id")
  airport.id == null ? hiddenField.value = -1 : hiddenField.value = airport.id
}

圖 3 展示了兩次成功執行 Ajax 調用之後 Flight 表單的外觀:

圖 3. 通過 Ajax 調用填充的表單

客戶端驗證

最後,還需要執行一些客戶端驗證,以確保 departureAirport 和 arrivalAirport 的無效值不會提 交回服務器(事實上,在為用戶提供選擇字段或一組復選框時,不可能輸入無效值。由於我允許用戶輸入 任意格式的文本,所以我需要留意他們的輸入質量)。

將 onSubmit 添加到 g:form 標記:

<g:form action="save" method="post" onsubmit="return validate()" >

如果 validate 返回 true,則表單被提交到服務器。如果返回 false,則提交被取消。清單 17 展示 了 validate 函數:

清單 17. validate 函數

function validate(){
  if( $F("departureAirport.id") == -1 ){
   alert("Please supply a valid Departure Airport")
   return false
  }

  if( $F("arrivalAirport.id") == -1 ){
   alert("Please supply a valid Arrival Airport")
   return false
  }

  return true
}

如果您覺得將選擇字段轉換為文本字段需要更多的工作,我同意您的觀點。我沒有執行這個更改以便 更容易操作 — 我努力使最終用戶更容易操作。但是請注意:Grails 提供的搭建功能為我了做了很多初 始工作,我可以對各個部分進行一些調優。搭建功能並不意味著是全部完成。它只是避免了所有令人厭煩 的事情,您可以專注於更有趣的工作。

結束語

除了簡單的多對多關系以外,希望您還能從本文了解一些創建多對多關系的出色技巧。有時候您希望 刪除操作能夠實現級聯;有時候不希望這樣。任何時候您認為兩個類之間存在著簡單的 m:m 關系時,可 能還需要從中發現第三個類。

在表示方面,選擇字段是 m:m 關系的默認選擇,但它們不是惟一的選擇。如果選項很少,那麼單選按 鈕和復選框也是值得考慮的選擇。如果選項很多,文本字段和 Ajax 能夠很好地完成任務。

下個月將向您展示如何將這些航班添加到交互式 Google 地圖上。我還會討論 Grails 服務。盡情享 受精通 Grails 的樂趣吧。

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