codeplex上不错的文章,转一个
JavaScript 的資料結構處理
只要是寫 Web 應用程式,就一定會碰觸到 JavaScript,而且一定不會只是簡單的幾行指令碼而已,尤其是在小型應用程式(Widget)盛行的今天,想要進入 Web 應用程式領域,不用 JavaScript 老實說就等於不會寫 Web 應用程式一樣,熟悉 JavaScript 可以很容易幫你減少不必要的 server 和 client 之間的 round-trip(往來)流量,也可以早一步先在用戶端做好資訊的整理,再傳回 server 繼續工作,這在很多應用程式都可以看的到它的影子。
通常在 JavaScript 中有幾種處理資料的方法,早期用的最多的就是陣列(Array),後來 JavaScript 陣營發展出特別的一種資料結構組成的方法,稱為 JSON(JavaScript Object Notation),目前也有許多的 JavaScript 函式庫(例如 jQuery)也支援剖析這種類型資料結構的能力,不過不論是一般陣列還是 JSON,都還沒有辦法把解析資料的過程簡化,例如這樣的 JSON 陣列:
[JavaScript]
Samples.People = [
{ ID: 1, FirstName: "Chris", LastName: "Pearson" },
{ ID: 2, FirstName: "Kate", LastName: "Johnson" },
{ ID: 3, FirstName: "Josh", LastName: "Sutherland" },
{ ID: 4, FirstName: "John", LastName: "Ronald" },
{ ID: 5, FirstName: "Steve", LastName: "Pinkerton" },
{ ID: 6, FirstName: "Katie", LastName: "Zimmerman" },
{ ID: 7, FirstName: "Dirk", LastName: "Anderson" },
{ ID: 8, FirstName: "Chris", LastName: "Stevenson" },
{ ID: 9, FirstName: "Bernard", LastName: "Sutherland" },
{ ID: 10, FirstName: "Kate", LastName: "Pinkerton" }
];
一般如果要剖析它的話,必須要解析它的結構,並取回它的每個欄位屬性,在許多 JavaScript 函式庫作者的努力之下,已經可以使用這樣的方法來做:
[JavaScript]
function searchPeople(keywords) {
var found = false;
for (var i = 0; i < People.length; i++) {
if (People[i].FirstName.indexOf(keywords) >= 0) {
alert("Found: " + People[i].FirstName + " " + People[i].LastName);
found = true;
}
}
if (!found)
alert("Not Found.");
}
不過,如果可以像 LINQ 一樣:
[JavaScript]
function searchPeopleWithLinq(keywords) {
var found = false;
JSLINQ(People)
.Where(function(item) {
return (
item.FirstName.indexOf(keywords) >= 0 ||
item.LastName.indexOf(keywords) >= 0);
})
.Select(function(item) {
found = true;
alert("Found: " + item.FirstName + " " + item.LastName);
});
if (!found)
alert("Not Found.");
}
那麼在撰寫以及程式碼的可讀性上會更高一些,這也是本文要介紹的,可以在 JavaScript 中使用類似 LINQ 語法能力的擴充函式庫 JSLINQ。
LINQ on JavaScript
自從 LINQ(語言集合查詢)在 .NET Framework 3.5 中被實現出來以後,受到許多的開發人員青睞,因為它將原本屬於資料庫 SQL 查詢的特性,透過一些由程式語言直接支援的方法,將它實現在撰寫程式的層次上,最終的目的是希望程式人員可以不用學習 SQL 就可以操控資料庫,或是具有集合(Collection)性質的資料,像是陣列或是 List、Dictionary 這類的集合物件,LINQ 可以將原本繁瑣的 foreach 以及條件比對優雅的隱藏起來,並透過像是 Lambda 運算式(Lambda expression)、匿名型別(anonymous types)以及擴充方法(Extended Methods)的語言層次支援,將控制集合資料的方式,呈現的像 SQL 一般的語法,例如:
[C#]
var query = from Student student in arrList
where student.Scores[0] > 95
select student;
foreach (Student s in query)
Console.WriteLine(s.LastName + ": " + s.Scores[0]);
若是有資料庫的支援時(例如 ADO.NET Entity Framework),LINQ 則可以直接查詢資料庫,或是查詢 ADO.NET 的物件(如 DataSet),開發人員不必再去撰寫 ADO.NET 的程式碼,也不用再碰觸到 SQL 指令了。
using (AdventureWorksEntities AWEntities = new AdventureWorksEntities())
{
ObjectQuery<Address> addresses = AWEntities.Address;
IQueryable<Address> query =
from address in addresses
where address.AddressLine1.Contains("Algiers Dr.")
select address;
// Addresses on Algiers Dr.
foreach (Address algiersAddress in query)
{
Console.WriteLine("Address 1: " + algiersAddress.AddressLine1);
}
}
這麼好用的工具,目前卻只有 C# 和 VB.NET 能夠享受到,這也未免太不公平了,還好,Chris Pietschmann 幫廣大的 JavaScript 使用族群發展出了一個物件,這個物件可以模擬出 LINQ 的大部份功能,讓 JavaScript 程式也可以享受到 LINQ 的便利之處,就如同上面所揭示的 JavaScript 中的 LINQ 程式一樣。
LINQ to JavaScript 目前可以支援陣列以及 JSON 的處理動作,而且它目前也已經是 jQuery 的外掛套件之一,可以 100% 和 jQuery 核心函式庫相容,將它和 jQuery 一起用會更顯它的靈活性。LINQ to JavaScript 支援下列的運算方法:
投影:Select, SelectMany
條件檢查:Where
排序:OrderBy, OrderByDescending, Reverse
轉換成陣列:ToArray
設定:Distinct, Intersect
數量詞作業:Any, All
串連資料:Concat
彙總:Count
項目作業:First, Last, FirstOrDefault, LastOrDefault, ElementAt, ElementAtOrDefault, DefaultIfEmpty
不過和一般的 LINQ 不太一樣的是,大多數的運算方法都包含一個 clause 參數,這個參數是用來傳回特定條件是否成立(例如 Where)或是處理後續程序(如 Select)所需要的函式指標,就如同在使用 jQuery 的 Callback 函數一樣。有些運算函數有自己的預設行為,但部份是一定要有的,Where 和 Select/SelectMany 運算子就是必須要有的運算函數。
使用方式
LINQ to JavaScript 所需要的核心函式庫在 Codeplex 的網站中可以下載(本文一開始即有網址),將它下載解壓縮後,在 scripts 資料夾中有兩個檔案,一個是 JSLINQ.js,這是核心程式碼,另一個 JSLINQ-vsdoc.js 則是 Intellisense 說明指令碼,若需要使用 Intellisense 時可以使用它。
將 JSLINQ.js 複製到專案中存於 script 指令碼的資料夾,然後在網頁中參照它:
<script type="text/javascript" language="javascript" src="scripts/JSLINQ.js"></script>
接著,使用 JSLINQ 物件將要用在 LINQ 上的資料結構包裝起來,這個資料結構可以是一般陣列或是 JSON 物件:
JSLINQ(array_item);
再使用它的運算子方法,如同使用 LINQ 一樣,不過它的形態比較像是使用函數呼叫用的 LINQ:
JSLINQ(singleArray)
.OrderBy(function(item) { return item; })
.Select(function(item) {
$("span").html($("span").html() + "<br />" + item);
});
上面是可以將陣列的數值排序的 LINQ to JavaScript 指令,完整的 JavaScript 程式碼為:
var singleArray = new Array(0, 2, 3, 6, 65, 30, 430, 654, 23, 103, 49, 97);
function sortArray()
{
JSLINQ(singleArray)
.OrderBy(function(item) { return item; })
.Select(function(item) {
$("span").html($("span").html() + "<br />" + item);
});
}
其執行的結果是:
再來小試一個例子,以相同的陣列,但要過濾出在 50-100 之間的數值,然後用降冪排序(Sort DESC),將上列的 sortArray() 函式修改如下:
var singleArray = new Array(0, 2, 3, 6, 65, 30, 430, 654, 23, 103, 49, 97);
function sortArray()
{
JSLINQ(singleArray)
.Where(function(item) { return (item >= 50 && item <= 100); })
.OrderByDescending(function(item) { return item; })
.Select(function(item) {
$("span").html($("span").html() + "<br />" + item);
});
}
其執行結果如下:
當然,只有這些牛刀小試是滿足不了廣大程式開發人員的胃口的,因為還沒有讓它和 JSON 一起運用,而這個也 JSLINQ 所要處理的問題之一。
實例運用:LINQ to JavaScript with ADO.NET Data Services
ADO.NET Data Service 是一個將資料直接發布在網路上的資料供應服務(Data Provisioning Service),允許用戶端以類似於 SQL 查詢的方式,向服務要求資料,它支援回傳 ATOM(RSS Feed)與 JSON 格式的資料,ATOM 可以用來實作一個定時或不定時更新的 RSS 服務,而 JSON 則可以相容於許多的 JavaScript 應用程式,本文的實例運用就是以這個為主。
NOTE
本文已假設讀者知道如何使用 Visual Studio 建立 ADO.NET 實體資料模型以及資料服務的方式。若對這方面不了解,請參考 MSDN 中的 ADO.NET Entity Framework 章節的內容,或者參考市面上的 .NET Framework 3.5 相關書籍。
Step 1. 建立 ADO.NET Data Services 服務端點(service endpoint)
首先,先建立一個範例資料庫,或是由 Codeplex 上下載 SQL Server 的範例資料庫,筆者所用的是 Northwind 資料庫,並將它附掛在 SQL Server 中(若是使用 SQL Express,則可以用動態掛載的方式來做),然後新增一個 Visual Studio 的網站或是 Web 應用程式專案,並新增一個 ADO.NET 實體資料模型,命名為 Northwind.edmx,並設定存取所有的資料表(不含檢視表與預存程序):
接著建立一個 ADO.NET Data Service,命名為 NorthwindService.svc:
接著開啟NorthwindService.svc.cs 檔案,修改內容如下:
public class NorthwindService : DataService<NorthwindEntities>
{
public static void InitializeService(IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
}
}
如此就可以使用 NorthwindService.svc 來存取 Northwind 資料庫中的資料了。
接著,建立一個網頁,可以是 HTML 或 ASP.NET 網頁,然後將下列的內容直接取代現有的網頁內容:
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
<script type="text/javascript" language="javascript" src="scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" language="javascript" src="scripts/JSLINQ.js"></script>
<script type="text/javascript" language="javascript">
var categoriesData = null;
$(function() {
$.ajax({
type: "GET",
url: "NorthwindService.svc/Categories",
contentType: "application/json",
data: "{}",
dataType: "json",
success: function(data) {
categoriesData = data.d;
},
error: function(e) {
alert("Error: " + e.description);
}
});
});
function displayCategory(dataItem) {
for (var item in dataItem)
alert("CategoryID: " + dataItem[item].CategoryID +
" Name: " + dataItem[item].CategoryName);
}
function searchCategory() {
JSLINQ(categoriesData)
.Where(function(item) {
return item.CategoryName.indexOf(
$("#txtSearchCategoryKeywords").val()) >= 0;
})
.Select(function(item) {
alert("Found CategoryID: " + item.CategoryID +
" Name: " + item.CategoryName);
});
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
Search Category: <input type="text" id="txtSearchCategoryKeywords" /><input type="button" value="Search" onclick="searchCategory()" />
</div>
</form>
</body>
</html>
這份網頁可以幫助測試 ADO.NET Data Service 是否有正常供應資料,它的畫面是:
請在 Search Category 文字方塊中隨意輸入一行文字,例如 ”Be”,按下 Search,它會呼叫 NorthwindService.svc,並傳回現有的所有 Category 資料回到用戶端(以 JSON 格式),接著使用 LINQ to JavaScript 檢查資料是否存在,若存在則會顯示,否則會顯示 Not Found:
若出現找到的訊息,表示與 ADO.NET Data Service 的服務連線是正常的。
NOTE
如果讀者對 ADO.NET Data Service 的控制較熟悉的話可以利用它的過濾方式去篩選資料,以減少網路的流量,本文因為要展示 LINQ to JavaScript 的能力,所以未特別處理網路流量的部份。
NOTE
若要要求 ADO.NET Data Service 傳回 JSON 格式的資料,則一定要在要求時使用 application/json 來設定,否則預設都會傳回 ATOM 格式的資料。
[JavaScript]
$.ajax({
type: "GET",
url: "NorthwindService.svc/Categories",
contentType: "application/json",
data: "{}",
dataType: "json",
success: function(data) {
categoriesData = data.d;
},
error: function(e) {
alert("Error: " + e.description);
}
});
Step 2. 建立客戶搜尋網頁
請在專案中新增一個 Customers.htm(HTML 網頁,不是 Web Form,因為不需要用到 ASP.NET 的東西),並且加入下列的 HTML 碼:
[HTML]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title></title>
<script type="text/javascript" language="javascript"
src="scripts/jquery-1.3.2.min.js"></script>
<script type="text/javascript" language="javascript" src="scripts/JSLINQ.js"></script>
</head>
<body>
<div class="MainForm" style="width: 100%;">
客戶資料搜尋:<br />
<br />
客戶名稱關鍵字:<input id="txtName" type="text" maxlength="50" /><br />
客戶聯絡人關鍵字:
<input id="txtContactorName" type="text" maxlength="50" /><br />
客戶地址關鍵字:<input id="txtAddress" type="text" maxlength="50" />
<input id="cmdSearch" type="button" value=" 搜尋 "
onclick="searchCustomer()" /><br />
<br />
<div class="DisplayResultGrid" style="width: 900px; ">
<div class="GridHeader; width: 900px">
<div class="Column1" style="width: 150px; float: left;">名稱</div>
<div class="Column2" style="width: 150px; float: left;">聯絡人名稱</div>
<div class="Column3" style="width: 150px; float: left;">地址</div>
<div class="Column4" style="width: 150px; float: left;">電話</div>
<div class="Column5" style="width: 150px; float: left;">傳真</div>
<div class="Column6" style="width: 150px; float: left;">動作</div>
</div>
<div class="GridRows" style="width: 900px; float: left">
</div>
</div>
<div class="GridRowTemplate" style="width: 900px; display: none">
<div class="Column1" style="width: 150px; float: left;"></div>
<div class="Column2" style="width: 150px; float: left;"></div>
<div class="Column3" style="width: 150px; float: left;"></div>
<div class="Column4" style="width: 150px; float: left;"></div>
<div class="Column5" style="width: 150px; float: left;"></div>
<div class="Column6" style="width: 150px; float: left;">
<input class="cmdCalCount" type="button" value=" 訂單數量 " />
<input class="cmdCalAmount" type="button" value=" 訂單總金額 " />
</div>
</div>
</div>
</body>
</html>
網頁執行起來會是這個樣子:
接著,撰寫自 ADO.NET Data Services 中擷取客戶資料的指令碼,依據在前一步驟建立的 NorthwindService.svc,若要取得客戶資料,則可以使用 /Customers 這個 URL 指令,因此我們可以使用下列的指令,在網頁載入時連到 Data Services 以查詢客戶的資料:
var jsonData = null; // 暫存來自 Services 上的客戶 JSON 資料集。
$(function() {
// 連接到 ADO.NET Data Services 取得客戶清單。
$.ajax({
type: "GET",
url: "NorthwindService.svc/Customers",
contentType: "application/json",
data: "{}",
dataType: "json",
success: function(data) {
jsonData = data.d;
},
error: function(e) {
alert("Error: " + e.description);
}
});
});
NOTE
通常在正規環境中,資料量通常不會太小,而且還要看網路傳輸的情況來決定,因此讀者可以試著修改這個函式,加入資料正在載入中的訊息,以提示使用者資料正在載入,這並不是本文的重點,因此筆者就將它留給讀者練習了。
再來,我們要使用 LINQ to JavaScript 來撰寫客戶搜尋的功能,並將找到的客戶資料列成表格清單,除了要使用到 LINQ 的功能外,還要用到一點 template 以及 jQuery 的函數。基本的 SELECT 的指令是:
[JavaScript]
JSLINQ(jsonData)
.Select(function(item){
// 處理查詢傳回的清單項目。
// item 裝載的即為各個符合條件的物件。
});
加上條件查詢的 SELECT 則是:
[JavaScript]
JSLINQ(jsonData)
.Where(function(item){
// 處理條件的設定,檢查每一項資料是否符合,符合則傳回 true,否則傳回 false。
})
.Select(function(item){
// 處理查詢傳回的清單項目。
// item 裝載的即為各個符合條件的物件。
});
SELECT 也可以支援多重條件查詢,例如:
[JavaScript]
JSLINQ(jsonData)
.Where(function(item){
// 處理條件 1。
})
.Where(function(item){
// 處理條件 2。
})
.Select(function(item){
// 處理查詢傳回的清單項目。
// item 裝載的即為各個符合條件的物件。
});
因此,查詢客戶資料過濾條件的 LINQ 指令,可以寫成如下的方式:
[JavaScript]
function searchCustomer() {
if ($(document.getElementById("txtName")).val() == "" &&
$(document.getElementById("txtContactorName")).val() == "" &&
$(document.getElementById("txtAddress")).val() == "") {
alert("未輸入搜尋條件。");
}
$('div.GridRows').eq(0).empty();
var result =
JSLINQ(jsonData)
.Where(function(item) {
if ($(document.getElementById("txtName")).val() != "")
return item.CompanyName.indexOf(
$(document.getElementById('txtName')).val()) >= 0;
else
return true;
})
.Where(function(item) {
if ($(document.getElementById("txtContactorName")).val() != "")
return item.CompanyName.indexOf(
$(document.getElementById('txtName')).val()) >= 0;
else
return true;
})
.Where(function(item) {
if ($(document.getElementById("txtAddress")).val() != "")
return item.Address.indexOf(
$(document.getElementById('txtAddress')).val()) >= 0;
else
return true;
})
.Select(function(item) {
// 處理結果。
});
}
讀者可以觀察上列的程式碼,使用了多重查詢的條件式,因為範例中使用了三個條件,故使用三個 Where 判斷式來處理,讀者也許會問為何不只用一個就好,當然也可以,但是其條件結果的設定工作就要由讀者自己來處理。
接著,撰寫將結果集填入表格中的指令碼,在本範例中使用 div 物件來組成表格,因此在 HTML 中有一個 <div class=”GridRowTemplate” /> 的 HTML 物件,程式將會使用這個 template 來自動產生每一個資料列,填入資料並且插入列空間區,在這裡使用到 jQuery 的 clone() 以及 append() 函式,以輔助 DOM 上的動作。
// item 是由 Select 傳入的各項符合條件的物件實體。
// 複製 template 並產生新的 jQuery 物件。
var dataRow = $('<div>' + $('div.GridRowTemplate').html() + '</div>').css('display', 'block');
// 填入資料。
dataRow.find('div').eq(0).text(item.CompanyName);
dataRow.find('div').eq(1).text(item.ContactName);
dataRow.find('div').eq(2).text(item.Address);
dataRow.find('div').eq(3).text(item.Phone);
dataRow.find('div').eq(4).text(item.Fax);
// 繫結事件處理常式,下一步會用到。
dataRow.find('div > input.cmdCalCount').bind('click', { id: item.CustomerID }, calculateOrderCount);
dataRow.find('div > input.cmdCalAmount').bind('click', { id: item.CustomerID }, calculateOrderAmount);
// 將資料列設到列空間中。
$('div.GridRows').eq(0).append(dataRow);
dataRow = null;
程式完成後,請執行 Customers.htm,並輸入要檢索的條件,系統會依輸入的關鍵字進行搜尋,並輸出結果,執行結果如下:
Step 3. 查詢客戶的訂單數量及總金額
在前一步生成表格列的指令中,有兩行是設定列中按鈕的事件常式,它們具備查詢客戶在資料庫中的訂單數量以及總金額的能力,但受限於 ADO.NET Data Services 本身的功能,雖然可以查詢訂單數量,但在 Orders Entity 中並沒有 TotalAmount 這一欄位,必須要使用 Order_Details Entity 的 Quantity 以及 UnitPrice 來計算每一項的產品後再加總,再加上無法直接由 Customers 連結到 Order_Details,因此我們需要在 Data Services 中撰寫一個方法來處理這件事。
NOTE
ADO.NET Data Services 可以支援由開發人員發展自訂的函數以提供在內建的 Entity 無法支援或提供的資料,但此類函數有幾個條件限制,諸如要用 [WebGet]/[WebInvoke] 設定由 Web 存取以外,傳回的資料還必須是 void、IEnumerable<T> 或是 IQueryable<T> 三種型別,否則會無法使用。
這方面的詳細資訊,可參考:
http://msdn.microsoft.com/zh-tw/library/cc668788.aspx
首先,請開啟 NorthwindService.svc.cs 檔案,並且在裡面加入 GetCustomerOrderItems 方法:
[C#] & [WebGet]
public IQueryable<Order_Details> GetCustomerOrderItems(string CustomerID)
{
var result = from orderDetails in this.CurrentDataSource.Order_Details
join orderInfo in this.CurrentDataSource.Orders
on orderDetails.OrderID equals orderInfo.OrderID
where orderInfo.Customers.CustomerID == CustomerID
select orderDetails;
return result;
}
這個方法所要做的是,將客戶資料所屬的訂單中的明細資料取出並擲回用戶端,所以我們使用了 LINQ to Entities,並配合 join 指令來做聯結,取回訂單的明細資料。
接著,請修改 InitializeService(),加入紅字的項目:
[C#]
public static void InitializeService(IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule(
"*", ServiceOperationRights.AllRead);
}
SetServiceOperationAccessRule() 可以設定在 Data Service 服務上每一個(或多個)服務方法的存取權限,由於範例只需要讀取資料,因此設定 AllRead 即可。
接著,請在瀏覽器中使用下列 URL 來測試服務是否正常:
[URL]
http://[你的網站根網址]/
NorthwindService.svc/GetCustomerOrderItems?CustomerID='CHOPS'
若能看到輸出的 ATOM 資料,表示服務可以正常執行:
結語
對於經常撰寫 JavaScript 處理資料的開發人員來說,LINQ to JavaScript 可以協助在集合型或是陣列型資料的處理,尤其是 JSON 的查詢上,它可以幫助過濾以及針對分項的資料做處理,以節省分析結構以及處理內部資料所要花費的時間以及心力。
MORE INFORMATION
Codeplex 上除了LINQ to JavaScript以外,還有幾個類似的函式庫可以用,像是:
JSINQ: http://jsinq.codeplex.com/
linq.js: http://linqjs.codeplex.com/
jLINQ: http://jlinq.codeplex.com/
每個作者對於 LINQ 處理的方式都不太相同,讀者也可以試著去使用它們,來找到真正符合讀者所需的。