[อัพเดท 2018: บทความนี้ผมเขียนไว้หลายปีแล้ว อาจใช้ไม่ได้ในปัจจุบัน]
ตอนที่ผมเรียนคอร์ส udacity FEND มี project นึงที่ผมเรียกภาพจาก instagram API มาแสดง วันนี้เอา code ส่วนนั้นมาแชร์กันครับ ผิดถูกประการใดแนะนำด้วยนะครับ ^.^ เรื่อง promise นี่ผมก็ยังไม่ค่อยเข้าใจนัก
Project
Project ที่จะทำในวันนี้หน้าตาเป็นแบบนี้ครับ

มี spec คือ เมื่อคลิกที่ชื่อ location ที่เราต้องการ จะไปดึงภาพ instagram ที่อยู่รอบๆ latitude, longtiude นั้นมา โดยภาพที่จะดึงได้ เจ้าของภาพต้องตั้งเป็น public (เราไม่สามารถดึงภาพของ private account มาไม่ได้ครับ)
Instagram API
ก่อนอื่นเลยเราต้องสมัครใช้งาน instagram API ก่อนครับที่เว็บ Instagram Developer สำหรับเรื่องนี้ พี่ DayDev เขียนไว้ละเอียดกว่าผม ลองเข้าไปอ่านกันได้ที่ Instagram API เบื้องต้นกับการดึงรูปภาพมาแสดงโดยใช้ Tags อย่างง่าย หรือใครอยากอ่านภาษาอังกฤษ ลองไปที่ Tutsplus ได้ครับ Introduction to the Instagram API

อธิบายคร่าวๆ นะครับ คือ Instagram API จะมี endpoint url ให้เราหลายๆ แบบ แต่ละแบบจะต้องการ Authentication ต่างๆ กันไป ลองเข้าไปดูได้ที่ Instagram API Endpoints ซึ่งถ้าดูจาก Location Endpoints แล้ว เราต้องใช้ Access Token
ซึ่งการจะได้ Access Token มา ทำได้ 2 แบบ คือ server-side และ client-side เพื่อความง่าย ในโปรเจคท์นี้ผมจึงใช้ client-side ไปก่อนนะครับ (แต่มันจะมีข้อจำกัดหลายๆ อย่างในวิธีนี้) ซึ่งสร้างได้โดยวิธีต่อไปนี้
- เข้าไปแล้วสมัครสมาชิก เลือก Register new Client ID กรอกข้อมูลอะไรไป
- ตรง Valid redirect URIs ตั้งเป็น http://localhost ไปก่อน
- ไม่ต้องติ๊กช่อง Disable implicit OAuth
- ใส่ url ต่อไปนี้ลงไปใน address bar “https://instagram.com/oauth/authorize/?client_id=[CLIENT_ID_HERE]&redirect_uri=http://localhost&response_type=token” โดยเปลี่ยน [CLIENT_ID] ให้เป็น Client ID ที่เขาให้มาในข้อ 1
- สังเกตที่ url เราจะได้ access token มาครับ “http://localhost/#access_token=[ACCESS_TOKEN]” เก็บ Token นี้ไว้ใช้ต่อไป
เริ่มกันเลย
1. เตรียม HTML, CSS, JS ไฟล์เบื้องต้น
ข้อนี้ไม่ขอบรรยายเยอะนะครับ ผมใช้ library เป็น bootstrap และ jQuery ส่วน HTML ก็มีโครงสร้างไม่กี่อย่าง ที่เหลือเป็น JavaScript เรียกภาพมา append เข้าไปใน DOM เฉยๆ ครับ ไฟล์มีอยู่สามไฟล์

index.html
<body> <div class="container main"> <div class="row"> <div class="col-sm-12 title-div"> <h1 class="text-center">Instagram API demo</h1> <h2 class="text-center">Get image around latitude and longtitude</h2> </div> </div> <div class="row"> <div class="col-sm-12"> <ul class="list-inline location-list"> <li class="location-list-item">Wat Phra Kaew</li> <li class="location-list-item">Wat Pho</li> <li class="location-list-item">Wat Arun</li> <li class="location-list-item">Wat Ratchabophit</li> <li class="location-list-item">Wat Ratchanaddaram</li> <li class="location-list-item">Wat Ratchapradit</li> <li class="location-list-item">Wat Saket and the Golden Mount</li> <li class="location-list-item">Wat Suthat and the Giant Swing</li> <li class="location-list-item">Wat Thepthidaram</li> </ul> </div> </div> <div class="row"> <div class="col-sm-12 ig" id="ig"> <h4>Some Instagram pictures in 100 metre around this area</h4> <p id="ig-info" class="bg-info">Please select temple to view.</p> <div id="image-area"></div> </div> </div> </div> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="js/app.js"></script> </body>
style.css
body { background: #bbaa95; } .main { background: #fff; } .title-div { margin-bottom: 40px; } .location-list { text-align: center; } .location-list-item { margin: 0 10px 10px 0; padding: 10px; background: #eee; cursor: pointer; transition: all .1s ease-in; } .location-list-item:hover { background: #92D8FF; } .ig-div { display: inline-block; width:50%; } .ig-div img{ width:100%; padding: 5px; } @media only screen and (min-width: 768px) { .ig-div { display: inline-block; width:20%; } }
2. เตรียมข้อมูลวัด
สเตปแรกคือเตรียมข้อมูล latitude และ longtitude ของวัดก่อน อันนี้ต้องไปหาเอาเอง (ยกเว้นจะเรียก Google Maps API ให้หาให้นะครับ แต่ก็จะเพิ่มความซับซ้อน)
app.js
var placeData = [ { name: 'Wat Phra Kaew', lat: 13.751399, lng: 100.492541, }, { name: 'Wat Pho', lat: 13.746575, lng: 100.492728, }, { name: 'Wat Arun', lat: 13.743703, lng: 100.489005, }, { name: 'Wat Ratchabophit', lat: 13.749143, lng: 100.497345, }, { name: 'Wat Ratchanaddaram', lat: 13.754767, lng: 100.504301, }, { name: 'Wat Ratchapradit', lat: 13.749453, lng: 100.495848, }, { name: 'Wat Saket and the Golden Mount', lat: 13.753865, lng: 100.508134, }, { name: 'Wat Suthat and the Giant Swing', lat: 13.751042, lng: 100.501006, }, { name: 'Wat Thepthidaram', lat: 13.753385, lng: 100.504221, } ];
3. bind Event ให้เรียก function เวลาเราคลิกที่ลิสท์
app.js
$('.location-list-item').click(function() { var index = $(this).index(); var lat = placeData[index].lat; var lng = placeData[index].lng; getIgPhoto(lat, lng); });
แต่ละ li จะมี class ‘.location-list-item’ อยู่ ใช้ class นั้นเป็น selector เมื่อคลิกที่ element นั้น
- ได้ index ของ element ที่เราคลิก จากนั้นนำ index นั้นไปเอาค่า lat, lng ที่เราเตรียมไว้มาจาก placeData array
- เรียก function ‘getIgPhoto’ โดยใช้ lat, lng ของ placeData นั้นเป็น arguments
4. AJAX call ครั้งแรก
ในการจะทำ project นี้ เราต้องเรียก instagram API 2 ครั้งครับ เมื่อเราใส่ latitude และ longtitude ไป เราจะได้ Array ของ location-id กลับมา จากนั้นจึงนำ location-id นั้นไปเรียกอีกครั้ง จึงจะได้ image object จริงๆ กลับมา
อธิบายหลักการก็คือ instagram เก็บข้อมูลสถานที่ไว้ในฐานข้อมูลชุดหนึ่ง เวลาเราอัพรูปใน instagram แล้วเราเลือกให้แสดง location ด้วย ตัว location ต่างๆ ที่ขึ้นมาให้เลือกนี่แหละครับคือพิกัดที่ instagram เก็บข้อมูลไว้ สำหรับ location search endpoint ก็คือเราให้พิกัด latitude(LAT) longtitude(LNG) ไป แล้วกำหนดระยะทาง(DISTANCE) รอบๆ พิกัดนี้ instagram จะไปดึง location ทั้งหมดในฐานข้อมูล ที่อยู่รอบๆ พิกัดนี้ในระยะที่เรากำหนดมา เช่น รอบพิกัด 1,000 เมตร รอบวัดพระแก้ว อาจจะมี 15 location เป็นต้น

แสดง location search endpoints
เช่น ในตัวอย่างของ API Docs คือ ถ้าให้พิกัดฟอไอเฟลไป ก็จะได้ response กลับมาแบบนี้

เพราะฉะนั้น code เราจึงเป็นแบบนี้
// เตรียมตัวแปลและ select element ที่ต้องใช้รอไว้ var locationURLList = [], imageObjList = [], imageList = [], infoBox = $('#ig-info'), imgDiv = $('.ig-div'); // เอารูปเดิมออก และบอก user ว่ากำลังโหลดรูปนะ imgDiv.remove(); infoBox.show().removeClass('bg-danger').addClass('bg-info').text("Loading..."); // Make AJAX call // เพื่อจะได้ array ของ location-id $.ajax({ type: 'GET', dataType: 'jsonp', cache: true, url: 'https://api.instagram.com/v1/locations/search?lat=' + lat + '&lng=' + lng + '&distance=100&access_token=[ใส่ ACCESS TOKEN]' }).done(function(data) { // ขั้นตอนต่อไป
กลับไปดูที่ API docs กันอีกครั้ง ถ้าเราอยากได้รูปที่อยู่รอบๆ location-id หนึ่งๆ เราต้องเตรียม endpoint format ดังต่อไปนี้

นำ object ที่ response กลับมา มาสร้าง array ของ url สำหรับการ call ครั้งที่สอง
}).done(function(data) { // เมื่อทุกอย่างเสร็จสิ้น loop ผ่านแต่ละ item ใน array เพื่อสร้างเป็น url // ที่เราต้อง call ในครั้งที่สอง for (var i = 0; i < data.data.length; i++) { // เตรียม url จาก location_id var targetURL = 'https://api.instagram.com/v1/locations/' + data.data[i].id + '/media/recent?access_token=[ใส่ ACCESS TOKEN]'; locationURLList.push(targetURL); } // เนื่องจากบางทีรอบๆ พิกัดหนึ่งๆ มีหลาย location มาก จึงเอามาแค่ 10 อัน // แต่ถ้าใครต้องการทั้งหมดสามารถตัดบรรทัดนี้ทิ้งได้ครับ locationURLList = locationURLList.slice(0, 10); }).done(function(){ // ขั้นตอนต่อไป
เมื่อเสร็จขั้นตอนนี้ ใน locationURLlist ของเราจะมี utl ใน format นี้ ‘https://api.instagram.com/v1/locations/{location-id}/media/recent?access_token=ACCESS-TOKEN’ จำนวน 10 url
5. AJAX call ครั้งที่สอง
ต้องบอกก่อนว่าเรื่อง promise นี่ผมไม่แม่นจริงๆ นะครับ แฮ่ๆ อธิบายตามที่เข้าใจก็คือ เราต้องการ call AJAX พร้อมๆ กันหลายอันแบบขนาน (parallel) เราจึงใช้ $.when แต่เนื่องจาก $.when รับค่าเป็น deferred object เราจึงต้อง map แต่ละ url ให้ return มาเป็น array ของ deferred object แล้ว apply มาใส่ $.when จากนั้นเมื่อ done แล้วจึงทำขั้นตอนต่อไป ซึ่งไม่มีอะไรซับซ้อนแล้วหลังจากตรงนี้
ลองอ่านเพิ่มเติมได้ที่ link ต่อไปนี้นะครับ
}).done(function(){ // เรียก AKAX ครั้งที่สอง ด้วยแต่ละ url ใน array $.when.apply($, locationURLList.map(function(url) { return $.ajax({ type: "GET", dataType: "jsonp", cache: true, url: url }); })).done(function(){ // ซ่อนแถบที่เอาไว้บอกว่า loading... infoBox.hide(); // loop ผ่าน object ที่ response กลับมาแล้วเอา image object นั้นไป concat // เข้าไปใน imageObjList ซึ่งเป็น array ของ image object for (var i = 0; i < arguments.length; i++) { imageObjList = imageObjList.concat(arguments[i][0].data); } // ถ้าอยากได้รูปแค่ 10 รูป uncomment บรรทัดนี้ // imageObjList = imageObjList.slice(0, 10); // เตรียม image div สำหรับใส่รูป var imageContainer = $('<div>'); // นำแต่ละ image object มาหาค่าที่เราต้องการ for (var j = 0; j < imageObjList.length; j++) { // หาเวลาที่ user สร้างภาพขึ้นมา var time = new Date(parseInt(imageObjList[j].created_time) * 1000); var fullDate = time.getDate() + '-' + time.getMonth() + '-' + time.getFullYear(); // append เข้าไปใน element ที่เราต้องการ imageContainer.append('<div class="ig-div"><a href="' + imageObjList[j].link + '"><img src="' + imageObjList[j].images.low_resolution.url + '" /></a>' + fullDate +'</div>'); } $('#image-area').html(imageContainer); // ไม่ต้องแสดง error message แล้ว clearTimeout(instagramRequestTimeout); }); }); // ถ้ามีปัญหาอะไรใน AJAX call ให้แสดง error message var instagramRequestTimeout = setTimeout(function() { infoBox.removeClass('bg-info').addClass('bg-danger').text("Fail to get instagram resources"); }, 8000);
ก็หมดแล้วครับ
6. สรุปภาพรวมของ JS ทั้งไฟล์
app.js ก็อปไปแล้วเปลี่ยนตรง [ACCESS_TOKEN] ก็ใช้ได้เลยครับ
var placeData = [ { name: 'Wat Phra Kaew', lat: 13.751399, lng: 100.492541, }, { name: 'Wat Pho', lat: 13.746575, lng: 100.492728, }, { name: 'Wat Arun', lat: 13.743703, lng: 100.489005, }, { name: 'Wat Ratchabophit', lat: 13.749143, lng: 100.497345, }, { name: 'Wat Ratchanaddaram', lat: 13.754767, lng: 100.504301, }, { name: 'Wat Ratchapradit', lat: 13.749453, lng: 100.495848, }, { name: 'Wat Saket and the Golden Mount', lat: 13.753865, lng: 100.508134, }, { name: 'Wat Suthat and the Giant Swing', lat: 13.751042, lng: 100.501006, }, { name: 'Wat Thepthidaram', lat: 13.753385, lng: 100.504221, } ]; $('.location-list-item').click(function() { var index = $(this).index(); var lat = placeData[index].lat; var lng = placeData[index].lng; getIgPhoto(lat, lng); }); function getIgPhoto(lat, lng) { // Prepare variable var locationURLList = [], imageObjList = [], imageList = [], infoBox = $('#ig-info'), imgDiv = $('.ig-div'); // Remove old image and tell the user we're loading images. imgDiv.remove(); infoBox.show().removeClass('bg-danger').addClass('bg-info').text("Loading..."); // Make AJAX call // The first call will get an array of location ID. We have to use those ID to make url for call again to get real image objects. $.ajax({ type: 'GET', dataType: 'jsonp', cache: true, url: 'https://api.instagram.com/v1/locations/search?lat=' + lat + '&lng=' + lng + '&distance=100&access_token=[ACCESS_TOKEN]' }).done(function(data) { // If request done, continue the next process // loop through data from instagram and make URL for second call. for (var i = 0; i < data.data.length; i++) { // Create target URL from location ID and push to the URL list array var targetURL = 'https://api.instagram.com/v1/locations/' + data.data[i].id + '/media/recent?access_token=[ACCESS_TOKEN]'; locationURLList.push(targetURL); } // Just 10 location URL is enough. When make a request to each URL, will get a lot of images. locationURLList = locationURLList.slice(0, 10); }).done(function(){ // Make an AJAX call with each URL in array. $.when.apply($, locationURLList.map(function(url) { return $.ajax({ type: "GET", dataType: "jsonp", cache: true, url: url }); })).done(function(){ // If got all data from each URL then hide the info box. infoBox.hide(); // Loop through returned data and make an array of image objects. for (var i = 0; i < arguments.length; i++) { imageObjList = imageObjList.concat(arguments[i][0].data); } // We want only first 10 images to display. // imageObjList = imageObjList.slice(0, 10); var imageContainer = $('<div>'); // Append images to the page for (var j = 0; j < imageObjList.length; j++) { var time = new Date(parseInt(imageObjList[j].created_time) * 1000); var fullDate = time.getDate() + '-' + time.getMonth() + '-' + time.getFullYear(); imageContainer.append('<div class="ig-div"><a href="' + imageObjList[j].link + '"><img src="' + imageObjList[j].images.low_resolution.url + '" /></a>' + fullDate +'</div>'); } $('#image-area').html(imageContainer); // Do not display error message clearTimeout(instagramRequestTimeout); }); }); // But if there're any problem in the AJAX call process, tell the user. var instagramRequestTimeout = setTimeout(function() { infoBox.removeClass('bg-info').addClass('bg-danger').text("Fail to get instagram resources"); }, 8000); }
อยากทำอะไรมากไปกว่านี้
เนื่องจากที่ผมทำเป็นแค่โปรเจคท์คร่าวๆ เฉยๆครับ ถ้าอยากทำมากกว่านี้มีไอเดียหลายอย่างที่ทำได้
Search bar
เราไม่ต้องเตรียมข้อมูลพิกัดไว้ก่อน แต่เราสามารถให้ user ป้อน address ที่ต้องการแล้วเราใช้ Google Maps Geocoding API เปลี่ยนเป็น latitude longtitude ได้ ลองดูเพิ่มเติมได้ครับ
สุดท้ายจะออกมาเหมือนเว็บนี้ Worldcam
Cache
เนื่องจากตอนนี้ทุกครั้งที่เราคลิก เว็บจะต้องเรียก instagram API ใหม่ทุกครั้ง ซึ่งมันทำให้ช้า และตัวเว็บก็ไม่ได้ต้องการความ real-time ขนาดนั้น ดังนั้นถ้าเรา cache รูปไว้ซักระยะหนึ่งก่อน เช่น load แค่ครั้งแรกครั้งเดียว ก็คงจะเร็วขึ้นครับ
Sort images by time
เนื่องจากเราเรียก AJAX ตาม location-id เพราะฉะนั้น response ที่กลับมาจึงเรียงตาม location-id แต่ response ที่กลับมาก็มี created_time เป็น property อยู่แล้ว ดังนั้นก่อนจะ append เข้าไปใน DOM เราสามารถ sort object ใน array ได้ ลองดู Array.prototype.sort() ครับ
Display image in Google Maps
เนื่องจากแต่ละ location-id มันก็มี latitude, longitude ของตัวเองอยู่แล้ว จริงๆ ก็เอามา pin ใน google maps ได้
และอื่นๆ อีกมากมายครับ
ปิดท้าย
คือถึงจะเขียนมาจนจบแล้ว ผมก็ยังนึก use case ของ project นี้จริงๆ ไม่ค่อยออกครับ ออกแนว gimmick มากกว่า เพราะ ที่ใช้บ่อยจริงๆ น่าจะเป็นดึง instagram image ตาม hashtag มากกว่า งาน event ใช้กันพรึ่บแน่นอน 55 แต่ก็ไม่เป็นไรครับ ถือว่าฝึกฝีมือ หวังว่าจะเป็นประโยชน์นะครับ 🙂