在 Grails 中使用 jQuery 和 DataTables
我是 Grails 的忠實粉絲。當然,我主要是熱衷於利用命令行工具來探索和分析數據的數據從業人員。數據從業人員經常需要查看數據,這也意味著他們通常擁有優秀的數據瀏覽器。利用 Grails、jQuery,以及 DataTables jQuery 插件,我們可以製作出非常友好的表格數據瀏覽器。
DataTables 網站提供了許多「食譜式」的教程文檔,展示了如何組合一些優秀的示例應用程序,這些程序包含了完成一些非常漂亮的東西所必要的 JavaScript、HTML,以及偶爾出現的 PHP。但對於那些寧願使用 Grails 作為後端的人來說,有必要進行一些說明示教。此外,樣本程序中使用的數據是一個虛構公司的員工的單個平面表格數據,因此處理這些複雜的表關係可以作為讀者的一個練習項目。
本文中,我們將創建具有略微複雜的數據結構和 DataTables 瀏覽器的 Grails 應用程序。我們將介紹 Grails 標準,它是 Groovy 式的 Java Hibernate 標準。我已將代碼託管在 GitHub 上方便大家訪問,因此本文主要是對代碼細節的解讀。
首先,你需要配置 Java、Groovy、Grails 的使用環境。對於 Grails,我傾向於使用終端窗口和 Vim,本文也使用它們。為獲得現代的 Java 環境,建議下載並安裝 Linux 發行版提供的 Open Java Development Kit (OpenJDK)(應該是 Java 8、9、10 或 11 之一,撰寫本文時,我正在使用 Java 8)。從我的角度來看,獲取最新的 Groovy 和 Grails 的最佳方法是使用 SDKMAN!。
從未嘗試過 Grails 的讀者可能需要做一些背景資料閱讀。作為初學者,推薦文章 創建你的第一個 Grails 應用程序。
獲取員工信息瀏覽器應用程序
正如上文所提,我將本文中員工信息瀏覽器的源代碼託管在 GitHub上。進一步講,應用程序 embrow 是在 Linux 終端中用如下命令構建的:
cd Projects
grails create-app com.nuevaconsulting.embrow
域類和單元測試創建如下:
grails create-domain-class com.nuevaconsulting.embrow.Position
grails create-domain-class com.nuevaconsulting.embrow.Office
grails create-domain-class com.nuevaconsulting.embrow.Employeecd embrowgrails createdomaincom.grails createdomaincom.grails createdomaincom.
這種方式構建的域類沒有屬性,因此必須按如下方式編輯它們:
Position
域類:
package com.nuevaconsulting.embrow
class Position {
String name
int starting
static constraints = {
name nullable: false, blank: false
starting nullable: false
}
}com.Stringint startingstatic constraintsnullableblankstarting nullable
Office
域類:
package com.nuevaconsulting.embrow
class Office {
String name
String address
String city
String country
static constraints = {
name nullable: false, blank: false
address nullable: false, blank: false
city nullable: false, blank: false
country nullable: false, blank: false
}
}
Enployee
域類:
package com.nuevaconsulting.embrow
class Employee {
String surname
String givenNames
Position position
Office office
int extension
Date hired
int salary
static constraints = {
surname nullable: false, blank: false
givenNames nullable: false, blank: false
: false
office nullable: false
extension nullable: false
hired nullable: false
salary nullable: false
}
}
請注意,雖然 Position
和 Office
域類使用了預定義的 Groovy 類型 String
以及 int
,但 Employee
域類定義了 Position
和 Office
欄位(以及預定義的 Date
)。這會導致創建資料庫表,其中存儲的 Employee
實例中包含了指向存儲 Position
和 Office
實例表的引用或者外鍵。
現在你可以生成控制器,視圖,以及其他各種測試組件:
-all com.nuevaconsulting.embrow.Position
grails generate-all com.nuevaconsulting.embrow.Office
grails generate-all com.nuevaconsulting.embrow.Employeegrails generateall com.grails generateall com.grails generateall com.
此時,你已經準備好了一個基本的增刪改查(CRUD)應用程序。我在 grails-app/init/com/nuevaconsulting/BootStrap.groovy
中包含了一些基礎數據來填充表格。
如果你用如下命令來啟動應用程序:
grails run-app
在瀏覽器輸入 http://localhost:8080/
,你將會看到如下界面:
Embrow 應用程序主界面。
單擊 「OfficeController」 鏈接,會跳轉到如下界面:
Office 列表
注意,此表由 OfficeController
的 index
方式生成,並由視圖 office/index.gsp
顯示。
同樣,單擊 「EmployeeController」 鏈接 跳轉到如下界面:
employee 控制器
好吧,這很醜陋: Position 和 Office 鏈接是什麼?
上面的命令 generate-all
生成的視圖創建了一個叫 index.gsp
的文件,它使用 Grails <f:table/>
標籤,該標籤默認會顯示類名(com.nuevaconsulting.embrow.Position
)和持久化示例標識符(30
)。這個操作可以自定義用來產生更好看的東西,並且自動生成鏈接,自動生成分頁以及自動生成可排序列的一些非常簡潔直觀的東西。
但該員工信息瀏覽器功能也是有限的。例如,如果想查找 「position」 信息中包含 「dev」 的員工該怎麼辦?如果要組合排序,以姓氏為主排序關鍵字,「office」 為輔助排序關鍵字,該怎麼辦?或者,你需要將已排序的數據導出到電子表格或 PDF 文檔以便通過電子郵件發送給無法訪問瀏覽器的人,該怎麼辦?
jQuery DataTables 插件提供了這些所需的功能。允許你創建一個完成的表格數據瀏覽器。
創建員工信息瀏覽器視圖和控制器的方法
要基於 jQuery DataTables 創建員工信息瀏覽器,你必須先完成以下兩個任務:
- 創建 Grails 視圖,其中包含啟用 DataTable 所需的 HTML 和 JavaScript
- 給 Grails 控制器增加一個方法來控制新視圖。
員工信息瀏覽器視圖
在目錄 embrow/grails-app/views/employee
中,首先複製 index.gsp
文件,重命名為 browser.gsp
:
cd Projects
cd embrow/grails-app/views/employee
cp gsp browser.gsp
此刻,你自定義新的 browser.gsp
文件來添加相關的 jQuery DataTables 代碼。
通常,在可能的時候,我喜歡從內容提供商處獲得 JavaScript 和 CSS;在下面這行後面:
<title><g:message code="default.list.label" args="[entityName]" /></title>
插入如下代碼:
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/scroller/1.4.4/css/scroller.dataTables.min.css">
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/scroller/1.4.4/js/dataTables.scroller.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.flash.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.html5.min.js"></script>
<script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.print.min.js "></script>
然後刪除 index.gsp
中提供數據分頁的代碼:
<div id="list-employee" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<f:table collection="${employeeList}" />
<div class="pagination">
<g:paginate total="${employeeCount ?: 0}" />
</div>
</div>
並插入實現 jQuery DataTables 的代碼。
要插入的第一部分是 HTML,它將創建瀏覽器的基本表格結構。DataTables 與後端通信的應用程序來說,它們只提供表格頁眉和頁腳;DataTables JavaScript 則負責表中內容。
<div id="employee-browser" class="content" role="main">
<h1>Employee Browser</h1>
<table id="employee_dt" class="display compact" style="width:99%;">
<thead>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</thead>
<tfoot>
<tr>
<th>Surname</th>
<th>Given name(s)</th>
<th>Position</th>
<th>Office</th>
<th>Extension</th>
<th>Hired</th>
<th>Salary</th>
</tr>
</tfoot>
</table>
</div>
接下來,插入一個 JavaScript 塊,它主要提供三個功能:它設置頁腳中顯示的文本框的大小,以進行列過濾,建立 DataTables 表模型,並創建一個處理程序來進行列過濾。
<g:javascript>
$('#employee_dt tfoot th').each( function() {javascript
下面的代碼處理表格列底部的過濾器框的大小:
var title = $(this).text();
if (title == 'Extension' || title == 'Hired')
$(this).html('<input type="text" size="5" placeholder="' + title + '?" />');
else
$(this).html('<input type="text" size="15" placeholder="' + title + '?" />');
});titletitletitletitletitle
接下來,定義表模型。這是提供所有表選項的地方,包括界面的滾動,而不是分頁,根據 DOM 字元串提供的裝飾,將數據導出為 CSV 和其他格式的能力,以及建立與伺服器的 AJAX 連接。 請注意,使用 Groovy GString 調用 Grails createLink()
的方法創建 URL,在 EmployeeController
中指向 browserLister
操作。同樣有趣的是表格列的定義。此信息將發送到後端,後端查詢資料庫並返回相應的記錄。
var table = $('#employee_dt').DataTable( {
"scrollY": 500,
"deferRender": true,
"scroller": true,
"dom": "Brtip",
"buttons": [ 'copy', 'csv', 'excel', 'pdf', 'print' ],
"processing": true,
"serverSide": true,
"ajax": {
"url": "${createLink(controller: 'employee', action: 'browserLister')}",
"type": "POST",
},
"columns": [
{ "data": "surname" },
{ "data": "givenNames" },
{ "data": "position" },
{ "data": "office" },
{ "data": "extension" },
{ "data": "hired" },
{ "data": "salary" }
]
});
最後,監視過濾器列以進行更改,並使用它們來應用過濾器。
table.columns().every(function() {
var that = this;
$('input', this.footer()).on('keyup change', function(e) {
if (that.search() != this.value && 8 < e.keyCode && e.keyCode < 32)
that.search(this.value).draw();
});
這就是 JavaScript,這樣就完成了對視圖代碼的更改。
});
</g:javascript>
以下是此視圖創建的UI的屏幕截圖:
這是另一個屏幕截圖,顯示了過濾和多列排序(尋找 「position」 包括字元 「dev」 的員工,先按 「office」 排序,然後按姓氏排序):
這是另一個屏幕截圖,顯示單擊 CSV 按鈕時會發生什麼:
最後,這是一個截圖,顯示在 LibreOffice 中打開的 CSV 數據:
好的,視圖部分看起來非常簡單;因此,控制器必須做所有繁重的工作,對吧? 讓我們來看看……
控制器 browserLister 操作
回想一下,我們看到過這個字元串:
"${createLink(controller: 'employee', action: 'browserLister')}"
對於從 DataTables 模型中調用 AJAX 的 URL,是在 Grails 伺服器上動態創建 HTML 鏈接,其 Grails 標記背後通過調用 createLink() 的方法實現的。這會最終產生一個指向 EmployeeController
的鏈接,位於:
embrow/grails-app/controllers/com/nuevaconsulting/embrow/EmployeeController.groovy
特別是控制器方法 browserLister()
。我在代碼中留了一些 print
語句,以便在運行時能夠在終端看到中間結果。
def browserLister() {
// Applies filters and sorting to return a list of desired employees
首先,列印出傳遞給 browserLister()
的參數。我通常使用此代碼開始構建控制器方法,以便我完全清楚我的控制器正在接收什麼。
println "employee browserLister params $params"
println()
接下來,處理這些參數以使它們更加有用。首先,jQuery DataTables 參數,一個名為 jqdtParams
的 Groovy 映射:
def jqdtParams = [:]
params.each { key, value ->
def keyFields = key.replace(']','').split(/[/)
def table = jqdtParams
for (int f = 0; f < keyFields.size() - 1; f++) {
def keyField = keyFields[f]
if (!table.containsKey(keyField))
table[keyField] = [:]
table = table[keyField]
}
table[keyFields[-1]] = value
}
println "employee dataTableParams $jqdtParams"
println()
接下來,列數據,一個名為 columnMap
的 Groovy 映射:
def columnMap = jqdtParams.columns.collectEntries { k, v ->
def whereTerm = null
switch (v.data) {
case 'extension':
case 'hired':
case 'salary':
if (v.search.value ==~ /d+(,d+)*/)
whereTerm = v.search.value.split(',').collect { it as Integer }
break
default:
if (v.search.value ==~ /[A-Za-z0-9 ]+/)
whereTerm = "%${v.search.value}%" as String
break
}
[(v.data): [where: whereTerm]]
}
println "employee columnMap $columnMap"
println()
接下來,從 columnMap
中檢索的所有列表,以及在視圖中應如何排序這些列表,Groovy 列表分別稱為 allColumnList
和 orderList
:
def allColumnList = columnMap.keySet() as List
println "employee allColumnList $allColumnList"
def orderList = jqdtParams.order.collect { k, v -> [allColumnList[v.column as Integer], v.dir] }
println "employee orderList $orderList"
我們將使用 Grails 的 Hibernate 標準實現來實際選擇要顯示的元素以及它們的排序和分頁。標準要求過濾器關閉;在大多數示例中,這是作為標準實例本身的創建的一部分給出的,但是在這裡我們預先定義過濾器閉包。請注意,在這種情況下,「date hired」 過濾器的相對複雜的解釋被視為一年並應用於建立日期範圍,並使用 createAlias
以允許我們進入相關類別 Position
和 Office
:
def filterer = {
createAlias 'position', 'p'
createAlias 'office', 'o'
if (columnMap.surname.where) ilike 'surname', columnMap.surname.where
if (columnMap.givenNames.where) ilike 'givenNames', columnMap.givenNames.where
if (columnMap.position.where) ilike 'p.name', columnMap.position.where
if (columnMap.office.where) ilike 'o.name', columnMap.office.where
if (columnMap.extension.where) inList 'extension', columnMap.extension.where
if (columnMap.salary.where) inList 'salary', columnMap.salary.where
if (columnMap.hired.where) {
if (columnMap.hired.where.size() > 1) {
or {
columnMap.hired.where.each {
between 'hired', Date.parse('yyyy/MM/dd',"${it}/01/01" as String),
Date.parse('yyyy/MM/dd',"${it}/12/31" as String)
}
}
} else {
between 'hired', Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/01/01" as String),
Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/12/31" as String)
}
}
}
是時候應用上述內容了。第一步是獲取分頁代碼所需的所有 Employee
實例的總數:
def recordsTotal = Employee.count()
println "employee recordsTotal $recordsTotal"
接下來,將過濾器應用於 Employee
實例以獲取過濾結果的計數,該結果將始終小於或等於總數(同樣,這是針對分頁代碼):
def c = Employee.createCriteria()
def recordsFiltered = c.count {
filterer.delegate = delegate
filterer()
}
println "employee recordsFiltered $recordsFiltered"
獲得這兩個計數後,你還可以使用分頁和排序信息獲取實際過濾的實例。
def orderer = Employee.withCriteria {
filterer.delegate = delegate
filterer()
orderList.each { oi ->
switch (oi[0]) {
case 'surname': order 'surname', oi[1]; break
case 'givenNames': order 'givenNames', oi[1]; break
case 'position': order 'p.name', oi[1]; break
case 'office': order 'o.name', oi[1]; break
case 'extension': order 'extension', oi[1]; break
case 'hired': order 'hired', oi[1]; break
case 'salary': order 'salary', oi[1]; break
}
}
maxResults (jqdtParams.length as Integer)
firstResult (jqdtParams.start as Integer)
}
要完全清楚,JTable 中的分頁代碼管理三個計數:數據集中的記錄總數,應用過濾器後得到的數字,以及要在頁面上顯示的數字(顯示是滾動還是分頁)。 排序應用於所有過濾的記錄,並且分頁應用於那些過濾的記錄的塊以用於顯示目的。
接下來,處理命令返回的結果,在每行中創建指向 Employee
、Position
和 Office
實例的鏈接,以便用戶可以單擊這些鏈接以獲取相關實例的所有詳細信息:
def dollarFormatter = new DecimalFormat('$##,###.##')
def employees = orderer.collect { employee ->
['surname': "<a href='${createLink(controller: 'employee', action: 'show', id: employee.id)}'>${employee.surname}</a>",
'givenNames': employee.givenNames,
'position': "<a href='${createLink(controller: 'position', action: 'show', id: employee.position?.id)}'>${employee.position?.name}</a>",
'office': "<a href='${createLink(controller: 'office', action: 'show', id: employee.office?.id)}'>${employee.office?.name}</a>",
'extension': employee.extension,
'hired': employee.hired.format('yyyy/MM/dd'),
'salary': dollarFormatter.format(employee.salary)]
}
最後,創建要返回的結果並將其作為 JSON 返回,這是 jQuery DataTables 所需要的。
def result = [draw: jqdtParams.draw, recordsTotal: recordsTotal, recordsFiltered: recordsFiltered, data: employees]
render(result as JSON)
}
大功告成。
如果你熟悉 Grails,這可能看起來比你原先想像的要多,但這裡沒有火箭式的一步到位方法,只是很多分散的操作步驟。但是,如果你沒有太多接觸 Grails(或 Groovy),那麼需要了解很多新東西 - 閉包,代理和構建器等等。
在那種情況下,從哪裡開始? 最好的地方是了解 Groovy 本身,尤其是 Groovy closures 和 Groovy delegates and builders。然後再去閱讀上面關於 Grails 和 Hibernate 條件查詢的建議閱讀文章。
結語
jQuery DataTables 為 Grails 製作了很棒的表格數據瀏覽器。對視圖進行編碼並不是太棘手,但 DataTables 文檔中提供的 PHP 示例提供的功能僅到此位置。特別是,它們不是用 Grails 程序員編寫的,也不包含探索使用引用其他類(實質上是查找表)的元素的更精細的細節。
我使用這種方法製作了幾個數據瀏覽器,允許用戶選擇要查看和累積記錄計數的列,或者只是瀏覽數據。即使在相對適度的 VPS 上的百萬行表中,性能也很好。
一個警告:我偶然發現了 Grails 中暴露的各種 Hibernate 標準機制的一些問題(請參閱我的其他 GitHub 代碼庫),因此需要謹慎和實驗。如果所有其他方法都失敗了,另一種方法是動態構建 SQL 字元串並執行它們。在撰寫本文時,我更喜歡使用 Grails 標準,除非我遇到雜亂的子查詢,但這可能只反映了我在 Hibernate 中對子查詢的相對缺乏經驗。
我希望 Grails 程序員發現本文的有趣性。請隨時在下面留下評論或建議。
via: https://opensource.com/article/18/9/using-grails-jquery-and-datatables
作者:Chris Hermansen 選題:lujun9972 譯者:jrg 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive