鍍金池/ 教程/ Java/ 遍歷分頁(yè)
集成自動(dòng)化部署
架設(shè) CI 服務(wù)器
探索用戶資源
SSH agent 轉(zhuǎn)發(fā)
使用評(píng)論
身份認(rèn)證基礎(chǔ)
管理部署密鑰
準(zhǔn)備開(kāi)始
傳遞部署
遍歷分頁(yè)
整合者的最佳做法
數(shù)據(jù)渲染成圖表

遍歷分頁(yè)

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ù)中找到本次示例工程的完整源代碼。

分頁(yè)基礎(chǔ)

在您開(kāi)始之前,有幾個(gè)關(guān)于接收分頁(yè)數(shù)據(jù)的重要事實(shí)您需要了解:

  1. 不同的 API 調(diào)用會(huì)得到不同的默認(rèn)返回格式的值。舉例來(lái)說(shuō),調(diào)用 列出 GitHub 所有公共存儲(chǔ)庫(kù) 會(huì)得到分頁(yè)之后的數(shù)據(jù),每頁(yè)30個(gè)項(xiàng)目;但如果您調(diào)用 GitHub Search API,每個(gè)分頁(yè)則會(huì)有100個(gè)項(xiàng)目。
  2. 您可以指定一個(gè)頁(yè)面有多少個(gè)項(xiàng)目(上限是100);但是,
  3. 由于技術(shù)原因, 不是所有端點(diǎn)都會(huì)有一致的結(jié)果。舉例來(lái)說(shuō),events 就不會(huì)讓您指定接收頁(yè)面的最大項(xiàng)目數(shù)。所以請(qǐng)務(wù)必閱讀目標(biāo)端點(diǎn)的關(guān)于如何處理分頁(yè)結(jié)果的文檔。

關(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è)碼。

在頁(yè)面中導(dǎo)航

現(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)。

更改接收的項(xiàng)目數(shù)

通過(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)致的。

消費(fèi)信息

您一般不會(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

構(gòu)造分頁(yè)鏈接

正常情況下,當(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}"