GitHub API 提供了海量的信息給開(kāi)發(fā)者消費(fèi)。大多數(shù)時(shí)候,您甚至?xí)l(fā)現(xiàn)您自己要求太多信息,為了防止我們勞累的服務(wù)器出現(xiàn)不測(cè),API 會(huì)自動(dòng)對(duì)要求的數(shù)據(jù)進(jìn)行分頁(yè)。
在這個(gè)指南中,我們會(huì)調(diào)用 GitHub 搜索 API,并通過(guò)分頁(yè)對(duì)結(jié)果進(jìn)行迭代。您可以在 platform-samples 存儲(chǔ)庫(kù)中找到本次示例工程的完整源代碼。
在您開(kāi)始之前,有幾個(gè)關(guān)于接收分頁(yè)數(shù)據(jù)的重要事實(shí)您需要了解:
關(guān)于分頁(yè)的信息請(qǐng)參照一次 API 調(diào)用的鏈接頭。 例如,我們發(fā)一個(gè) curl 請(qǐng)求給搜索 API,來(lái)查出 Mozilla 工程一共用了多少次短語(yǔ) addClass
:
curl -I "https://api.github.com/search/code?q=addClass+user:mozilla"
上文中的參數(shù) -I
表示我們只關(guān)心鏈接頭,不關(guān)心具體內(nèi)容。在我們探討結(jié)果的時(shí)候,您將會(huì)發(fā)現(xiàn)一些鏈接頭內(nèi)的信息會(huì)像下面這樣:
Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"
把這個(gè)信息分解來(lái)看,rel="next"
部分說(shuō)明了下一頁(yè)是 page=2
。 這講得通,畢竟默認(rèn)情況下,所有分頁(yè)的請(qǐng)求都會(huì)從頁(yè)面 1
開(kāi)始 。而 rel="last"
部分則提供了更多信息,說(shuō)明了結(jié)果的最后一頁(yè)是在 34
頁(yè)。也就是說(shuō),還有 33 頁(yè)關(guān)于 addClass
的信息可供消費(fèi),真爽!
需要注意的是您應(yīng)該永遠(yuǎn)依賴對(duì)方提供給您的鏈接關(guān)系,不要自己試圖猜測(cè)或者構(gòu)建 URL,例如列出一個(gè)存儲(chǔ)庫(kù)內(nèi)的所有 commit 中,分頁(yè)結(jié)果是根據(jù) SHA 散列值生成的,而不是頁(yè)碼。
現(xiàn)在您知道了有多少個(gè)頁(yè)面需要接收,下一步可以開(kāi)始導(dǎo)航頁(yè)面來(lái)觀看結(jié)果。您可以通過(guò)傳遞 page
參數(shù)來(lái)完成這件事。默認(rèn)情況下, page
參數(shù)總是從 1
開(kāi)始。讓我們直接跳到第 14 頁(yè)看看會(huì)發(fā)生什么事情:
curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&page=14"
然后再一次得到了類似下文的鏈接頭:
Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=15>; rel="next",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=1>; rel="first",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=13>; rel="prev"
和預(yù)想中的一樣,rel="next"
指向第 15 頁(yè),而 rel="last"
則仍然指向第 34 頁(yè)。不過(guò)這次我們得到了一些別的信息:rel="first"
,提供了_第一頁(yè)_的 URL;更重要的是,rel="prev"
能讓您知道上一頁(yè)的頁(yè)碼。有了這些信息,您就能構(gòu)造一些用戶界面來(lái)讓用戶在只需一次 API 調(diào)用的情況下就實(shí)現(xiàn)“第一頁(yè)”、“最后一頁(yè)”、“上一頁(yè)”、“下一頁(yè)”的隨意跳轉(zhuǎn)。
通過(guò)傳遞 per_page
參數(shù),您可以指定每頁(yè)返回多少個(gè)項(xiàng)目了,上限是 100。我們來(lái)試試要求關(guān)于 addClass
的 50 個(gè)項(xiàng)目的檢索結(jié)果:
curl -I "https://api.github.com/search/code?q=addClass+user:mozilla&per_page=50"
請(qǐng)注意這個(gè)語(yǔ)句對(duì)鏈接頭回應(yīng)的影響:
Link: <https://api.github.com/search/code?q=addClass+user%3Amozilla&per_page=50&page=2>; rel="next",
<https://api.github.com/search/code?q=addClass+user%3Amozilla&per_page=50&page=20>; rel="last"
也許您已經(jīng)猜到,rel="last"
信息顯示出最后一頁(yè)現(xiàn)在變成第 20 頁(yè)了。這顯然是我們要求在每個(gè)頁(yè)面內(nèi)容納更多的信息所導(dǎo)致的。
您一般不會(huì)僅僅為了處理數(shù)據(jù)分頁(yè)而費(fèi)力使用低級(jí)的 cURL 調(diào)用,所以我們來(lái)寫(xiě)一個(gè)小小的 Ruby 腳本來(lái)完成上文提到的所有事情。 和往常一樣,首先我們會(huì)需要GitHub 的 Octokit.rb Ruby 庫(kù),還要提供我們的個(gè)人訪問(wèn)令牌:
require 'octokit'
# 在真正的應(yīng)用內(nèi)永遠(yuǎn)不要用硬編碼把值寫(xiě)死 !
# 而是設(shè)置環(huán)境變量并測(cè)試,和下例所示
client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']
接下來(lái),我們利用 Octokit的 search_code
方法來(lái)執(zhí)行搜索。和使用 curl
不同,用 search_code
方法還能立刻得到結(jié)果的數(shù)量,立刻試試:
results = client.search_code('addClass user:mozilla')
total_count = results.total_count
現(xiàn)在來(lái)獲取最后一頁(yè)的頁(yè)碼。和鏈接頭的 page=34>; rel="last"
信息相類似,Octokit.rb 支持通過(guò)一個(gè)叫做“超媒體鏈接關(guān)系”的方法來(lái)返回分頁(yè)信息,我們不會(huì)對(duì)這個(gè)東西作過(guò)多解釋,不過(guò),可以這么說(shuō),在 results
變量中的每個(gè)元素都有一個(gè)叫做 rels
的哈希值,這個(gè)哈希值可以包含和 :next
、:last
、:first
、:prev
相關(guān)的信息,這取決于您收到的結(jié)果類型。這些關(guān)系也包含了結(jié)果的 URL,稱為 rels[:last].href
。
知道這些之后,我們來(lái)獲取最后一個(gè)結(jié)果所在的頁(yè)碼,然后將所有這些信息展現(xiàn)給用戶:
last_response = client.last_response
number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]
puts "There are #{total_count} results, on #{number_of_pages} pages!"
最后,我們來(lái)迭代結(jié)果。您雖然可以通過(guò)一個(gè) for i in 1..number_of_pages.to_i
循環(huán)來(lái)做到這點(diǎn),不過(guò)這次,不妨跟隨 rels[:next]
頭來(lái)接收每一頁(yè)的信息。為了簡(jiǎn)便起見(jiàn),這次只獲取每一頁(yè)中的第一個(gè)文件的路徑。要實(shí)現(xiàn)這些需要使用循環(huán),在到達(dá)每次循環(huán)的結(jié)尾時(shí),都可以通過(guò)跟隨 rels[:next]
信息來(lái)得到下一個(gè)頁(yè)面的數(shù)據(jù)集,這個(gè)循環(huán)會(huì)一直運(yùn)行直到再也沒(méi)有 rels[:next]
可供消費(fèi)為止(換句話說(shuō),這循環(huán)會(huì)執(zhí)行到 rels[:last]
)。代碼應(yīng)該看上去像下面這樣:
puts last_response.data.items.first.path
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
puts last_response.data.items.first.path
end
在 Octokit.rb 中,更改每頁(yè)顯示的項(xiàng)目數(shù)量是極端簡(jiǎn)單的。只需要傳遞一個(gè) per_page
選項(xiàng) hash 給初始的客戶端構(gòu)造器。除此之外,您的代碼應(yīng)該還是原來(lái)的樣子:
require 'octokit'
# 在真正的應(yīng)用內(nèi)永遠(yuǎn)不要用硬編碼把值寫(xiě)死 !
# 而是設(shè)置環(huán)境變量并測(cè)試,像下例所示
client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']
results = client.search_code('addClass user:mozilla', :per_page => 100)
total_count = results.total_count
last_response = client.last_response
number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]
puts last_response.rels[:last].href
puts "There are #{total_count} results, on #{number_of_pages} pages!"
puts "And here's the first path for every set"
puts last_response.data.items.first.path
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
puts last_response.data.items.first.path
end
正常情況下,當(dāng)使用分頁(yè)時(shí),您的目標(biāo)不是去串聯(lián)所有可能的結(jié)果,而是去制作一套類似下圖這樣的導(dǎo)航器:
http://wiki.jikexueyuan.com/project/github-developer-guides/images/pagination_sample.png" alt="Sample of pagination links" />
我們來(lái)勾勒出這個(gè)功能的一個(gè)微型版本。
從上文提到的代碼中,我們已經(jīng)知道能夠在第一次調(diào)用得到的分頁(yè)結(jié)果中獲取 number_of_pages
值:
require 'octokit'
# 在真正的應(yīng)用內(nèi)永遠(yuǎn)不要用硬編碼把值寫(xiě)死 !
# 而是設(shè)置環(huán)境變量并測(cè)試,和下例所示
client = Octokit::Client.new :access_token => ENV['MY_PERSONAL_TOKEN']
results = client.search_code('addClass user:mozilla')
total_count = results.total_count
last_response = client.last_response
number_of_pages = last_response.rels[:last].href.match(/page=(\d+)$/)[1]
puts last_response.rels[:last].href
puts "There are #{total_count} results, on #{number_of_pages} pages!"
通過(guò)下面的代碼,可以構(gòu)造一個(gè)漂亮的 ASCII 風(fēng)格數(shù)字框:
numbers = ""
for i in 1..number_of_pages.to_i
numbers << "[#{i}] "
end
puts numbers
我們通過(guò)隨機(jī)數(shù)來(lái)模擬用戶點(diǎn)擊了其中任意一個(gè)數(shù)字框。
random_page = Random.new
random_page = random_page.rand(1..number_of_pages.to_i)
puts "A User appeared, and clicked number #{random_page}!"
我們現(xiàn)在有了頁(yè)碼,可以使用 Octokit 通過(guò)傳遞 :page
選項(xiàng)來(lái)顯式地接收目標(biāo)頁(yè)面了:
`clicked_results = client.search_code('addClass user:mozilla', :page => random_page)`
如果想做的更精致一些,我們也可以把上一頁(yè)和下一頁(yè)的鏈接也弄過(guò)來(lái)。要?jiǎng)?chuàng)建“上一頁(yè)”(<<
)和“下一頁(yè)”(>>
)元素,可以參考以下代碼:
prev_page_href = client.last_response.rels[:prev] ? client.last_response.rels[:prev].href : "(none)"
next_page_href = client.last_response.rels[:next] ? client.last_response.rels[:next].href : "(none)"
puts "The prev page link is #{prev_page_href}"
puts "The next page link is #{next_page_href}"