來自 mentor-program-5th week4 的挑戰題。
解題方向 這一題的目標是印出「最受歡迎的 200 個實況列表」,會用到的資料是這兩個:
簡單來說就是先取得「遊戲 id」,再利用遊戲 id 去取得「實況資料」這樣子。
不過要注意 Get Streams 一次最多只能抓 100 筆資料,要取得 200 筆的話會變得複雜一點,所以我會建議先寫一個簡單版的,列出 100 筆實況列表就好,接著再來想要怎麼優化。
簡單版 這裡要做的事情很簡單,我們只要做兩件事就好:
根據輸入去找出遊戲 id
拿遊戲 id 去找出實況列表
這裡就直接貼原始碼,步驟都寫在註解裡面了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 require ('dotenv' ).config()const request = require ('request' )const BASE_URL = 'https://api.twitch.tv/helix' function searchGame (name, callback ) { if (!name) { return console .log('請輸入遊戲名稱' ) } request({ url : `${BASE_URL} /games?name=${name} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerGameId (err, res, body ) { if (err) { return console .log(err) } const gameId = JSON .parse(body).data[0 ].id getStreams(gameId, handlerStreams) } function getStreams (gameId, callback ) { if (!gameId){ return console .log('請輸入遊戲 id' ) } request({ url : `${BASE_URL} /streams?first=100&gamd_id=${gameId} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerStreams (err, res, body ) { if (err) { return console .log(err) } const streams = JSON .parse(body).data for (let stream of streams) { console .log('==============' ) console .log(`實況主名稱:${stream.user_name} ` ) console .log(`實況主 id:${stream.user_id} ` ) console .log(`觀看人數:${stream.viewer_count} ` ) } } searchGame(process.argv[2 ], handlerGameId)
比較重要的地方在於 執行順序 ,我們希望的順序是:handlerGameId
拿到 id 後才執行 getStreams
所以 getStreams
一定是在 handlerGameId
裡面被呼叫的,如果你寫在別的地方,結果一定是錯的,可以參考 理解什麼是 race-condition
困難版 簡單版沒有問題的話,接下來要做的事情其實也不複雜,其實就是在發一次請求去找出第二筆實況列表資料這樣子而已。
可能會碰到的一個問題是「要怎麼抓第二筆實況資料?」
關於這個問題,你可以先觀察一下,在拿到第一筆實況資料的時候,資料裡有個 pagination
的欄位:
1 2 3 4 5 6 7 8 9 10 11 { "data" : [ { "id" : "41375541868" , ... }, ], "pagination" : { "cursor" : "eyJiIjp7IkN1cnNvciI6ImV5SnpJam8zT0RNMk5TNDBORFF4TlRjMU1UY3hOU3dpWkNJNlptRnNjMlVzSW5RaU9uUnlkV1Y5In0sImEiOnsiQ3Vyc29yIjoiZXlKeklqb3hOVGs0TkM0MU56RXhNekExTVRZNU1ESXNJbVFpT21aaGJITmxMQ0owSWpwMGNuVmxmUT09In19" } }
裡面的那個 cursor
就是用來拿下一筆實況資料的時候需要用到的東西,我自己是把它當作一個「記錄點」的意思,或你也可以讀一下 API 文件的說明:
Cursor for backward pagination: tells the server where to start fetching the next set of results, in a multi-page response. The cursor value specified here is from the pagination response field of a prior query.
簡單來說就是告訴後端「從哪裡開始撈資料」的意思啦!我們剛剛不是已經抓了 100 筆嗎?,所以第二次發請求時要加上 cursor
來告訴後端「我要從這裡開始拿資料哦(第 101 筆)」,大概是這樣的概念。
沒問題後,接下來就是思考解題方向:
送出請求,拿到遊戲 id
用遊戲 id 請求第一筆實況列表
拿到實況列表後,把第一筆資料存起來
用遊戲 id 請求第二筆實況列表,這次要加上 cursor
,來從第 101 筆開始抓
拿到實況列表後,把第二筆資料存起來
把所有資料印出來
這裡要特別注意我們得新增兩個變數 gameId
、allStreams
來儲存「遊戲 id」與「所有實況列表」。
為什麼?因為你在拿到第一筆實況資料的時候,callback 的參數裡是不會有遊戲 id 的,如果要在這個 callback 裡送出第二次請求,就一定要有遊戲 id 的這個值,所以才要建立一個變數來儲存。
至於 allStreams
也一樣,我們每次都只會拿到 100 筆實況資料,所以拿到資料的時候當然得存起來,最後才有辦法湊成 200 筆。
接下來一樣參考原始碼跟註解,應該都寫得蠻清楚的了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 require ('dotenv' ).config()const request = require ('request' )const BASE_URL = 'https://api.twitch.tv/helix' let gameId = null let allStreams = []function searchGame (name, callback ) { if (!name) return console .log('請輸入遊戲名稱' ) request({ url : `${BASE_URL} /games?name=${name} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerGameId (err, res, body ) { if (err) { return console .log(err) } gameId = JSON .parse(body).data[0 ].id getFirstStreams(gameId, handlerFirstStreams) } function getFirstStreams (gameId, callback ) { if (!gameId){ return console .log('請輸入遊戲 id' ) } request({ url : `${BASE_URL} /streams?first=100&gamd_id=${gameId} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerFirstStreams (err, res, body ) { if (err) { return console .log(err) } const streams = JSON .parse(body).data const cursor = JSON .parse(body).pagination.cursor allStreams = allStreams.concat(streams) getSecondStreams(gameId, cursor, handlerSecondStreams) } function getSecondStreams (gameId, cursor, callback ) { if (!gameId || !cursor) return console .log('請輸入 id 及 記錄點' ) request({ url : `${BASE_URL} /streams?after=${cursor} &first=100&gamd_id=${gameId} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerSecondStreams (err, res, body ) { if (err) { return console .log(err) } const streams = JSON .parse(body).data allStreams = allStreams.concat(streams) for (let stream of allStreams) { console .log('=============' ) console .log(`實況主:${stream.user_name} ` ) console .log(`id:${stream.user_id} ` ) console .log(`觀看人數:${stream.viewer_count} ` ) } } searchGame(process.argv[2 ], handlerGameId)
一樣要注意執行順序,只要是非同步的 function,結果都一定要用 callback 來處理。
困難版(優化) 這裡是我慢慢修出來的,我也還不知道怎麼解釋比較好,所以請直接看註解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 require ('dotenv' ).config()const request = require ('request' )const BASE_URL = 'https://api.twitch.tv/helix' const BATCH_LIMIT = 50 const TOTAL = 200 function searchGame (name, callback ) { if (!name) return console .log('請輸入遊戲名稱' ) request({ url : `${BASE_URL} /games?name=${name} ` , headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } function handlerGameId (err, res, body ) { if (err) { return console .log(err) } const gameId = JSON .parse(body).data[0 ].id getAllStreams(gameId, BATCH_LIMIT, TOTAL, handlerAllStream) } function getAllStreams (gameId, limit, total, callback ) { let allStreams = [] function handlerStreams (err, res, body ) { if (err) { return callback(err) } const streams = JSON .parse(body).data const cursor = JSON .parse(body).pagination.cursor allStreams = allStreams.concat(streams) if (allStreams.length < total) { return getStreams(gameId, cursor, limit, handlerStreams) } return callback(null , allStreams.slice(0 , total)) } getStreams(gameId, null , limit, handlerStreams) } function handlerAllStream (err, streams ) { if (err) { return console .log(err) } for (let stream of streams) { console .log('==============' ) console .log(`實況主名稱:${stream.user_name} ` ) console .log(`實況主 id:${stream.user_id} ` ) console .log(`觀看人數:${stream.viewer_count} ` ) } } function getStreams (gameId, cursor, limit, callback ) { let url = `${BASE_URL} /streams?first=${limit} &gamd_id=${gameId} ` if (!gameId){ return console .log('請輸入遊戲 id' ) } if (cursor) { url += `&after=${cursor} ` } request({ url : url, headers : { 'Authorization' : process.env.ACCESS_TOKEN, 'Client-ID' : process.env.CLIENT_ID } }, callback) } searchGame(process.argv[2 ], handlerGameId)